diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e36159580ec6e9a50c78d5a1080d3420dcb195e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/big_modeling.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/big_modeling.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97a59c73f8a972d54a2bdd9aa51856ce65096523 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/big_modeling.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/checkpointing.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/checkpointing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcfe14f05bd8667d159c764a1af3c579b8aa60b3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/checkpointing.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/data_loader.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/data_loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a1f5f3f6adeb99e18d7424fd9b24f3359bdd8cb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/data_loader.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/hooks.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/hooks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c449405b85128483170d5e24a6a92fb60b1f5b9d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/hooks.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/inference.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/inference.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e53c804ac1e7de1c9718ef70956429495da82e7a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/inference.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/launchers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/launchers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0158112a5d30340eb802b5c17417d45b6262ff5e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/launchers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/local_sgd.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/local_sgd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d83c4f8713e6248e2e959388bb0bedf87624205f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/local_sgd.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/logging.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/logging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0df1cc1ae75ea2e1ec2280f060777bf43bfdf61f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/logging.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/memory_utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/memory_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94f30930a5de114c1f880c0f516ebfa0e7993efa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/memory_utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/optimizer.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/optimizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a21a62e7fecc636bfc87b11c780555af685e959 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/optimizer.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/parallelism_config.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/parallelism_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..248a535e5539c123ccd2e1f55f8ed0ac3d7f115c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/parallelism_config.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/scheduler.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/scheduler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59987f4aac97d71482c7b9a2ebdc7c6d3a396e00 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/scheduler.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/state.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/state.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6d73f36190a6335827c9ef9dcf29005bba89b63 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/state.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/tracking.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/tracking.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77cb395f5023593b809d9652d5130828b9e5cdb0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/__pycache__/tracking.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c9cbe26c257b515f657c05e1996d517e69613972 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a5767f3250672a5bba04c9c7bb69d5469b2b219 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/accelerate_cli.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/accelerate_cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a419bd8052248e530c29f3c680a7ade5455facb5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/accelerate_cli.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/env.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..612ad19ad2a0a85dff286d46c0307753d9dd3761 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/env.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/estimate.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/estimate.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52377ca5ddd845e2cfbb9c3cb0f96702635210ed Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/estimate.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/launch.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/launch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8565a7d4f17a566ef134d1d0ec3db60f179f1700 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/launch.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/merge.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/merge.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70379a364996f76c1384c10cd9079a497900fd55 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/merge.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/test.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/test.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8572591f06dab2cc542b6f760c4ab428cfb36066 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/test.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/to_fsdp2.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/to_fsdp2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e47f42a97e6e4c1ab62faf5624e7362104f1233c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/to_fsdp2.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/tpu.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/tpu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e644b7dbd09d369d16bd33bd022aa018b285467 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/tpu.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3965946c6907b165c23040b1f62ecc35f1339c75 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/__pycache__/utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/accelerate_cli.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/accelerate_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..b878c8debd874e1418b946775b11568c7487ad72 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/accelerate_cli.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from accelerate.commands.config import get_config_parser +from accelerate.commands.env import env_command_parser +from accelerate.commands.estimate import estimate_command_parser +from accelerate.commands.launch import launch_command_parser +from accelerate.commands.merge import merge_command_parser +from accelerate.commands.test import test_command_parser +from accelerate.commands.to_fsdp2 import to_fsdp2_command_parser +from accelerate.commands.tpu import tpu_command_parser +from accelerate.commands.utils import CustomArgumentParser + + +def main(): + parser = CustomArgumentParser("Accelerate CLI tool", usage="accelerate []", allow_abbrev=False) + subparsers = parser.add_subparsers(help="accelerate command helpers") + + # Register commands + get_config_parser(subparsers=subparsers) + estimate_command_parser(subparsers=subparsers) + env_command_parser(subparsers=subparsers) + launch_command_parser(subparsers=subparsers) + merge_command_parser(subparsers=subparsers) + tpu_command_parser(subparsers=subparsers) + test_command_parser(subparsers=subparsers) + to_fsdp2_command_parser(subparsers=subparsers) + + # Let's go + args = parser.parse_args() + + if not hasattr(args, "func"): + parser.print_help() + exit(1) + + # Run + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..649a15888cccd070b3d4ca9a600457c6ad59d4d3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from .config import config_command_parser +from .config_args import default_config_file, load_config_from_file # noqa: F401 +from .default import default_command_parser +from .update import update_command_parser + + +def get_config_parser(subparsers=None): + parent_parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False) + # The main config parser + config_parser = config_command_parser(subparsers) + # The subparser to add commands to + subcommands = config_parser.add_subparsers(title="subcommands", dest="subcommand") + + # Then add other parsers with the parent parser + default_command_parser(subcommands, parents=[parent_parser]) + update_command_parser(subcommands, parents=[parent_parser]) + + return config_parser + + +def main(): + config_parser = get_config_parser() + args = config_parser.parse_args() + + if not hasattr(args, "func"): + config_parser.print_help() + exit(1) + + # Run + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c4beb63486be3c2fae59dc43a6fb9068a38e3fd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/cluster.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/cluster.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc6ecefa5ae6eba377bc1d2e1c60d319e784b17f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/cluster.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c644f5604a317eb0a85138e867c6fcbf026880a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_args.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_args.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5656d86e5b665191c71238d80cda2a124411036 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_args.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4996dee27df4e35864c10ec24b95082e43ae6d9c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/config_utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/default.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/default.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..672a509a3e7264b087bad7e3e73c4fd23c09e207 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/default.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/sagemaker.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/sagemaker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a67def697c81e4982f5282f411e45fe684965566 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/sagemaker.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/update.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/update.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a62900fab13b8eb3ec94718c6f17c915baaa0606 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/__pycache__/update.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/cluster.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..fce80efdcbbb6e36572716d38b3d74491ebc918f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/cluster.py @@ -0,0 +1,939 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from ...utils import ( + ComputeEnvironment, + DistributedType, + is_deepspeed_available, + is_fp8_available, + is_hpu_available, + is_mlu_available, + is_mps_available, + is_msamp_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_torchao_available, + is_transformer_engine_available, + is_transformers_available, + is_xpu_available, +) +from ...utils.constants import ( + DEEPSPEED_MULTINODE_LAUNCHERS, + FSDP2_STATE_DICT_TYPE, + FSDP_AUTO_WRAP_POLICY, + FSDP_BACKWARD_PREFETCH, + FSDP_SHARDING_STRATEGY, + FSDP_STATE_DICT_TYPE, + TORCH_DYNAMO_MODES, +) +from .config_args import ClusterConfig +from .config_utils import ( + DYNAMO_BACKENDS, + _ask_field, + _ask_options, + _convert_distributed_mode, + _convert_dynamo_backend, + _convert_fp8_backend, + _convert_mixed_precision, + _convert_yes_no_to_bool, +) + + +def get_cluster_input(): + distributed_type = _ask_options( + "Which type of machine are you using?", + [ + "No distributed training", + "multi-CPU", + "multi-XPU", + "multi-HPU", + "multi-GPU", + "multi-NPU", + "multi-MLU", + "multi-SDAA", + "multi-MUSA", + "multi-NEURON", + "TPU", + ], + _convert_distributed_mode, + ) + + machine_rank = 0 + num_machines = 1 + num_processes = 1 + gpu_ids = None + main_process_ip = None + main_process_port = None + rdzv_backend = "static" + same_network = True + debug = False + + if distributed_type in [ + DistributedType.MULTI_GPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_CPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ]: + num_machines = _ask_field( + "How many different machines will you use (use more than 1 for multi-node training)? [1]: ", + int, + default=1, + ) + if num_machines > 1: + machine_rank = _ask_options( + "What is the rank of this machine?", + list(range(num_machines)), + int, + ) + main_process_ip = _ask_field( + "What is the IP address of the machine that will host the main process? ", + ) + main_process_port = _ask_field( + "What is the port you will use to communicate with the main process? ", + int, + ) + same_network = _ask_field( + "Are all the machines on the same local network? Answer `no` if nodes are on the cloud and/or on different network hosts [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + if not same_network: + rdzv_backend = _ask_field( + "What rendezvous backend will you use? ('static', 'c10d', ...): ", default="static" + ) + debug = _ask_field( + "Should distributed operations be checked while running for errors? This can avoid timeout issues but will be slower. [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + if distributed_type == DistributedType.NO: + use_cpu = _ask_field( + "Do you want to run your training on CPU only (even if a GPU / Apple Silicon / Ascend NPU device is available)? [yes/NO]:", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + elif distributed_type == DistributedType.MULTI_CPU: + use_cpu = True + else: + use_cpu = False + + mpirun_config = {} + + if use_cpu: + if distributed_type == DistributedType.MULTI_CPU: + use_mpirun = _ask_field( + "Do you want accelerate to launch mpirun? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_mpirun: + mpirun_hostfile = _ask_field( + "Please enter the path to the hostfile to use with mpirun [~/hostfile]: ", + str, + default="~/hostfile", + ) + mpirun_config["mpirun_hostfile"] = os.path.expanduser(mpirun_hostfile.strip()) + + dynamo_config = {} + use_dynamo = _ask_field( + "Do you wish to optimize your script with torch dynamo?[yes/NO]:", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_dynamo: + prefix = "dynamo_" + dynamo_config[prefix + "backend"] = _ask_options( + "Which dynamo backend would you like to use?", + [x.lower() for x in DYNAMO_BACKENDS], + _convert_dynamo_backend, + default=2, + ) + use_custom_options = _ask_field( + "Do you want to customize the defaults sent to torch.compile? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + if use_custom_options: + dynamo_config[prefix + "mode"] = _ask_options( + "Which mode do you want to use?", + TORCH_DYNAMO_MODES, + lambda x: TORCH_DYNAMO_MODES[int(x)], + default=0, + ) + dynamo_config[prefix + "use_fullgraph"] = _ask_field( + "Do you want the fullgraph mode or it is ok to break model into several subgraphs? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + dynamo_config[prefix + "use_dynamic"] = _ask_field( + "Do you want to enable dynamic shape tracing? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + dynamo_config[prefix + "use_regional_compilation"] = _ask_field( + "Do you want to enable regional compilation? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + use_mps = not use_cpu and is_mps_available() + deepspeed_config = {} + if ( + distributed_type + in [ + DistributedType.MULTI_GPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NEURON, + DistributedType.NO, + ] + and not use_mps + ): + use_deepspeed = _ask_field( + "Do you want to use DeepSpeed? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_deepspeed: + if distributed_type is DistributedType.MULTI_NEURON: + raise RuntimeError("DeepSpeed is not supported on Neuron devices.") + + distributed_type = DistributedType.DEEPSPEED + assert is_deepspeed_available(), ( + "DeepSpeed is not installed => run `pip3 install deepspeed` or build it from source" + ) + + if distributed_type == DistributedType.DEEPSPEED: + use_deepspeed_config = _ask_field( + "Do you want to specify a json file to a DeepSpeed config? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_deepspeed_config: + deepspeed_config["deepspeed_config_file"] = _ask_field( + "Please enter the path to the json DeepSpeed config file: ", + str, + default="none", + ) + else: + deepspeed_config["zero_stage"] = _ask_options( + "What should be your DeepSpeed's ZeRO optimization stage?", + [0, 1, 2, 3], + int, + default=2, + ) + + deepspeed_devices = ["none", "cpu", "nvme"] + if deepspeed_config["zero_stage"] >= 2: + deepspeed_config["offload_optimizer_device"] = _ask_options( + "Where to offload optimizer states?", deepspeed_devices, lambda x: deepspeed_devices[int(x)] + ) + deepspeed_config["offload_param_device"] = _ask_options( + "Where to offload parameters?", deepspeed_devices, lambda x: deepspeed_devices[int(x)] + ) + if deepspeed_config["offload_param_device"] == "nvme": + deepspeed_config["offload_param_nvme_path"] = _ask_field( + "Nvme Path to offload parameters?", + str, + default="/nvme", + ) + if deepspeed_config["offload_optimizer_device"] == "nvme": + deepspeed_config["offload_optimizer_nvme_path"] = _ask_field( + "Nvme Path to offload optimizer states?", + str, + default="/nvme", + ) + deepspeed_config["gradient_accumulation_steps"] = _ask_field( + "How many gradient accumulation steps you're passing in your script? [1]: ", + int, + default=1, + ) + use_gradient_clipping = _ask_field( + "Do you want to use gradient clipping? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_gradient_clipping: + deepspeed_config["gradient_clipping"] = _ask_field( + "What is the gradient clipping value? [1.0]: ", + float, + default=1.0, + ) + if deepspeed_config["zero_stage"] == 3: + deepspeed_config["zero3_save_16bit_model"] = _ask_field( + "Do you want to save 16-bit model weights when using ZeRO Stage-3? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + deepspeed_config["zero3_init_flag"] = _ask_field( + "Do you want to enable `deepspeed.zero.Init` when using ZeRO Stage-3 for constructing massive models? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if deepspeed_config["zero3_init_flag"]: + if not is_transformers_available(): + raise Exception( + "When `zero3_init_flag` is set, it requires Transformers to be installed. " + "Please run `pip3 install transformers`." + ) + use_moe = _ask_field( + "Do you want to enable Mixture-of-Experts training (MoE)? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_moe: + deepspeed_config["deepspeed_moe_layer_cls_names"] = _ask_field( + "Specify the comma-separated list of transformers MoE layer class names (case-sensitive), e.g : " + " `MixtralSparseMoeBlock`, `Qwen2MoeSparseMoeBlock`, `JetMoEAttention,JetMoEBlock` ... : ", + str, + ) + + if num_machines > 1: + launcher_query = "Which Type of launcher do you want to use?" + deepspeed_config["deepspeed_multinode_launcher"] = _ask_options( + launcher_query, + DEEPSPEED_MULTINODE_LAUNCHERS, + lambda x: DEEPSPEED_MULTINODE_LAUNCHERS[int(x)], + ) + + if deepspeed_config["deepspeed_multinode_launcher"] != DEEPSPEED_MULTINODE_LAUNCHERS[1]: + deepspeed_config["deepspeed_hostfile"] = _ask_field( + "DeepSpeed configures multi-node compute resources with hostfile. " + "Each row is of the format `hostname slots=[num_gpus]`, e.g., `localhost slots=2`; " + "for more information please refer official [documentation]" + "(https://www.deepspeed.ai/getting-started/#resource-configuration-multi-node). " + "Please specify the location of hostfile: ", + str, + ) + + is_exclusion_filter = _ask_field( + "Do you want to specify exclusion filter string? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if is_exclusion_filter: + deepspeed_config["deepspeed_exclusion_filter"] = _ask_field( + "DeepSpeed exclusion filter string: ", + str, + ) + + is_inclusion_filter = _ask_field( + "Do you want to specify inclusion filter string? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if is_inclusion_filter: + deepspeed_config["deepspeed_inclusion_filter"] = _ask_field( + "DeepSpeed inclusion filter string: ", + str, + ) + + fsdp_config = {} + + if distributed_type in [ + DistributedType.MULTI_GPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ]: + use_fsdp = _ask_field( + "Do you want to use FullyShardedDataParallel? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_fsdp: + if distributed_type is DistributedType.MULTI_NEURON: + raise NotImplementedError("FSDP is not currently supported on Neuron devices.") + distributed_type = DistributedType.FSDP + + if distributed_type == DistributedType.FSDP: + fsdp_config["fsdp_version"] = _ask_options( + "What should be your FSDP version? [2]: ", + [1, 2], + lambda x: int(x) + 1, + default=1, + ) + fsdp_version = fsdp_config["fsdp_version"] # extract to a variable to simplify usage later + + if fsdp_version == 1: + sharding_strategy_query = "What should be your sharding strategy?" + fsdp_config["fsdp_reshard_after_forward"] = _ask_options( + sharding_strategy_query, + FSDP_SHARDING_STRATEGY, + lambda x: FSDP_SHARDING_STRATEGY[int(x)], + ) + else: + fsdp_config["fsdp_reshard_after_forward"] = _ask_field( + "Do you want to enable resharding after forward? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + + fsdp_config["fsdp_offload_params"] = _ask_field( + "Do you want to offload parameters and gradients to CPU? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + fsdp_wrap_query = "What should be your auto wrap policy?" + fsdp_config["fsdp_auto_wrap_policy"] = _ask_options( + fsdp_wrap_query, + FSDP_AUTO_WRAP_POLICY, + lambda x: FSDP_AUTO_WRAP_POLICY[int(x)], + ) + if fsdp_config["fsdp_auto_wrap_policy"] == FSDP_AUTO_WRAP_POLICY[0]: + use_no_split_modules = _ask_field( + "Do you want to use the model's `_no_split_modules` to wrap. Only applicable for 🤗 Transformers [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if not use_no_split_modules: + fsdp_config["fsdp_transformer_layer_cls_to_wrap"] = _ask_field( + "Specify the comma-separated list of transformer layer class names (case-sensitive) to wrap ,e.g, :" + "`BertLayer`, `GPTJBlock`, `T5Block`, `BertLayer,BertEmbeddings,BertSelfOutput` ...? : ", + str, + ) + elif fsdp_config["fsdp_auto_wrap_policy"] == FSDP_AUTO_WRAP_POLICY[1]: + fsdp_config["fsdp_min_num_params"] = _ask_field( + "What should be your FSDP's minimum number of parameters for Default Auto Wrapping Policy? [1e8]: ", + int, + default=100000000, + ) + # Removed in FSDP2, ask for user input for FSDP1 + if fsdp_version == 1: + fsdp_backward_prefetch_query = "What should be your FSDP's backward prefetch policy?" + fsdp_config["fsdp_backward_prefetch"] = _ask_options( + fsdp_backward_prefetch_query, + FSDP_BACKWARD_PREFETCH, + lambda x: FSDP_BACKWARD_PREFETCH[int(x)], + ) + + fsdp_state_dict_type_query = "What should be your FSDP's state dict type?" + fsdp_config["fsdp_state_dict_type"] = _ask_options( + fsdp_state_dict_type_query, + FSDP_STATE_DICT_TYPE if fsdp_version == 1 else FSDP2_STATE_DICT_TYPE, + lambda x: FSDP_STATE_DICT_TYPE[int(x)] if fsdp_version == 1 else FSDP2_STATE_DICT_TYPE[int(x)], + default=0, + ) + # Not implemented in FSDP2, ask for user input for FSDP1 + if fsdp_version == 1: + fsdp_config["fsdp_forward_prefetch"] = _ask_field( + "Do you want to enable FSDP's forward prefetch policy? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + # Obsolete in FSDP2, ask for user input for FSDP1 + if fsdp_version == 1: + fsdp_config["fsdp_use_orig_params"] = _ask_field( + "Do you want to enable FSDP's `use_orig_params` feature? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + fsdp_config["fsdp_cpu_ram_efficient_loading"] = _ask_field( + "Do you want to enable CPU RAM efficient model loading? Only applicable for 🤗 Transformers models. [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + # Obsolete in FSDP2, ask for user input for FSDP1 + if fsdp_version == 1: + if fsdp_config["fsdp_cpu_ram_efficient_loading"]: + fsdp_config["fsdp_sync_module_states"] = True + else: + fsdp_config["fsdp_sync_module_states"] = _ask_field( + "Do you want each individually wrapped FSDP unit to broadcast module parameters from rank 0 at the start? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + fsdp_config["fsdp_activation_checkpointing"] = _ask_field( + "Do you want to enable FSDP activation checkpointing? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + parallelism_config = {} + + if fsdp_config.get("fsdp_version", 1) == 2: + use_parallelism_config = _ask_field( + "Do you want to use the parallelism config? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + if use_parallelism_config: + prefix = "parallelism_config_" + parallelism_config[prefix + "dp_replicate_size"] = _ask_field( + "What is the data parallelism replicate size? [1]: ", + int, + default=1, + error_message="Please enter an integer.", + ) + + parallelism_config[prefix + "dp_shard_size"] = _ask_field( + "What is the FSDP shard size? [1]: ", + int, + default=1, + error_message="Please enter an integer.", + ) + + parallelism_config[prefix + "tp_size"] = _ask_field( + "What is the tensor parallelism size? [1]: ", + int, + default=1, + error_message="Please enter an integer.", + ) + + parallelism_config[prefix + "cp_size"] = _ask_field( + "What is the context parallelism size? [1]: ", + int, + default=1, + error_message="Please enter an integer.", + ) + if parallelism_config[prefix + "cp_size"] > 1: + parallelism_config[prefix + "cp_comm_strategy"] = _ask_options( + "What is the compute parallelism communication strategy?", + ["allgather", "alltoall"], + lambda x: ["allgather", "alltoall"][int(x)], + default=0, + ) + + megatron_lm_config = {} + if distributed_type in [DistributedType.MULTI_GPU]: + use_megatron_lm = _ask_field( + "Do you want to use Megatron-LM ? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_megatron_lm: + distributed_type = DistributedType.MEGATRON_LM + if distributed_type == DistributedType.MEGATRON_LM: + prefix = "megatron_lm_" + megatron_lm_config[prefix + "tp_degree"] = _ask_field( + "What is the Tensor Parallelism degree/size? [1]:", + int, + default=1, + error_message="Please enter an integer.", + ) + if megatron_lm_config[prefix + "tp_degree"] > 1: + megatron_lm_config[prefix + "sequence_parallelism"] = _ask_field( + "Do you want to enable Sequence Parallelism? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + + megatron_lm_config[prefix + "pp_degree"] = _ask_field( + "What is the Pipeline Parallelism degree/size? [1]:", + int, + default=1, + error_message="Please enter an integer.", + ) + if megatron_lm_config[prefix + "pp_degree"] > 1: + megatron_lm_config[prefix + "num_micro_batches"] = _ask_field( + "What is the number of micro-batches? [1]:", + int, + default=1, + error_message="Please enter an integer.", + ) + + megatron_lm_config[prefix + "recompute_activations"] = _ask_field( + "Do you want to enable selective activation recomputation? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + + megatron_lm_config[prefix + "use_distributed_optimizer"] = _ask_field( + "Do you want to use distributed optimizer " + "which shards optimizer state and gradients across data parallel ranks? [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + error_message="Please enter yes or no.", + ) + + megatron_lm_config[prefix + "gradient_clipping"] = _ask_field( + "What is the gradient clipping value based on global L2 Norm (0 to disable)? [1.0]: ", + float, + default=1.0, + ) + # TPU specific defaults + tpu_commands = None + tpu_command_file = None + tpu_downcast_bf16 = "no" + tpu_env = [] + tpu_name = None + tpu_vm = None + tpu_zone = None + tpu_use_sudo = False + tpu_use_cluster = False + + if distributed_type in [ + DistributedType.MULTI_CPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_GPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NPU, + DistributedType.MULTI_NEURON, + DistributedType.XLA, + ]: + machine_type = str(distributed_type).split(".")[1].replace("MULTI_", "") + if machine_type in ["TPU", "NEURON"]: + machine_type += " cores" + elif machine_type == "CPU": + machine_type = "processes" + else: + machine_type += "(s)" + num_processes = _ask_field( + f"How many {machine_type} should be used for distributed training? [1]:", + int, + default=1, + error_message="Please enter an integer.", + ) + elif distributed_type in [DistributedType.FSDP, DistributedType.DEEPSPEED, DistributedType.MEGATRON_LM]: + num_processes = _ask_field( + "How many GPU(s) should be used for distributed training? [1]:", + int, + default=1, + error_message="Please enter an integer.", + ) + else: + num_processes = 1 + + if (distributed_type == DistributedType.MULTI_GPU) and (num_machines == 1) and (num_processes == 1): + raise ValueError( + f"Specified distributed type {distributed_type} but only using 1 GPU on a single machine. Please select `No distributed training` for the type of machine you are using." + ) + + if ( + distributed_type + in [ + DistributedType.MULTI_GPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + DistributedType.NO, + ] + and not use_cpu + and not use_mps + ): + if is_npu_available(): + machine_type = "NPU(s)" + elif is_mlu_available(): + machine_type = "MLU(s)" + elif is_sdaa_available(): + machine_type = "SDAA(s)" + elif is_musa_available(): + machine_type = "MUSA(s)" + elif is_xpu_available(): + machine_type = "XPU(s)" + elif is_hpu_available(): + machine_type = "HPU(s)" + elif is_neuron_available(): + machine_type = "Neuron cores" + else: + machine_type = "GPU(s)" + gpu_ids = _ask_field( + f"What {machine_type} (by id) should be used for training on this machine as a comma-separated list? [all]:", + default="all", + ) + + # CPU affinity is only supported on NVIDIA hardware for now + enable_cpu_affinity = False + if distributed_type in (DistributedType.NO, DistributedType.MULTI_GPU) and not use_cpu and not use_mps: + enable_cpu_affinity = _ask_field( + "Would you like to enable numa efficiency? (Currently only supported on NVIDIA hardware). [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + fp8_config = None + if distributed_type == DistributedType.XLA: + mixed_precision = "no" + main_training_function = _ask_field( + "What is the name of the function in your script that should be launched in all parallel scripts? [main]: ", + default="main", + ) + tpu_use_cluster = _ask_field( + "Are you using a TPU cluster? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if tpu_use_cluster: + tpu_name = _ask_field( + "What is the name of your TPU cluster? ", + default=None, + error_message="Please enter the name of your TPU cluster.", + ) + tpu_zone = _ask_field( + "What is the zone of your TPU cluster? ", + default=None, + error_message="Please enter the zone of your TPU cluster.", + ) + tpu_use_sudo = _ask_field( + "To run a python script in a TPU pod, should `sudo` be used? [yes/NO]: ", + default=False, + error_message="Please enter yes or no.", + ) + run_commands = _ask_field( + "Do you have code you wish to run on startup in each pod? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if run_commands: + use_command_file = _ask_field( + "Is this code located in a bash script? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_command_file: + tpu_command_file = _ask_field( + "What is the path to your bash script? ", + default=None, + error_message="Please enter the path to your bash script.", + ) + tpu_command_file = os.path.abspath(tpu_command_file) + else: + print("Please enter each command separately you wish to run on startup in each pod.") + tpu_commands = [] + another_command = True + while another_command: + tpu_commands.append( + _ask_field( + "Please enter a single command to be ran ", + default=None, + error_message="Please enter the commands you wish to run on startup in each pod as a single string.", + ) + ) + another_command = _ask_field( + "Do you wish to add another command? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + tpu_vm = _ask_field( + "If not using an instance group, what are the names of the Compute VM instances to be used, separated by a comma: ", + default="", + ).split(",") + tpu_env = _ask_field( + "What environment variables do you wish to set in each pod, separated by a comma: ", + default="", + ).split(",") + + else: + main_training_function = "main" + if distributed_type == DistributedType.DEEPSPEED and use_deepspeed_config: + mixed_precision = None + else: + mixed_precision = _ask_options( + "Do you wish to use mixed precision?", + ["no", "fp16", "bf16", "fp8"], + _convert_mixed_precision, + ) + if mixed_precision == "fp8": + if not is_fp8_available(): + raise ValueError( + "FP8 (either torchao, Transformer Engine or MSAMP) is not installed on this machine." + ) + fp8_config = {} + fp8_config["backend"] = _ask_options( + "Which FP8 backend do you want to use?", + ["ao", "te", "msamp"], + _convert_fp8_backend, + ) + if fp8_config["backend"] == "TE": + if not is_transformer_engine_available(): + raise ValueError("TransformersEngine was selected, but it is not installed on this machine.") + fp8_config["use_autocast_during_eval"] = _ask_field( + "Do you want to use FP8 autocast during eval mode? Generally better metrics are found when this is disabled [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + ) + fp8_config["margin"] = _ask_field( + "What margin should be used for gradient scaling? [0]: ", + int, + default=0, + ) + fp8_config["interval"] = _ask_field( + "What interval should be used for for how often the scaling factor is recomputed? [1]: ", + int, + default=1, + ) + fp8_config["fp8_format"] = _ask_options( + "Which weight format should be used?", + ["HYBRID", "E4M3", "E5M2"], + lambda i: ["HYBRID", "E4M3", "E5M2"][i], + default=0, + ) + fp8_config["amax_history_length"] = _ask_field( + "What length of history should be used for the amax scaling factor computation? [1024]: ", + int, + default=1024, + ) + fp8_config["amax_compute_algorithm"] = _ask_options( + "Which algorithm should be used for the amax scaling factor computation?", + ["max", "most_recent"], + lambda x: "max" if x == 0 else "most_recent", + default=0, + ) + fp8_config["override_linear_precision"] = _ask_field( + "Do you want to to execute `fprop`, `dgrad`, and `wgrad` GEMMS in higher precision? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + ) + if fp8_config["override_linear_precision"]: + fprop = _ask_field( + "Should `fprop` be executed in higher precision? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + ) + dgrad = _ask_field( + "Should `dgrad` be executed in higher precision? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + ) + wgrad = _ask_field( + "Should `wgrad` be executed in higher precision? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + ) + fp8_config["override_linear_precision"] = (fprop, dgrad, wgrad) + else: + fp8_config["override_linear_precision"] = (False, False, False) + + elif fp8_config["backend"] == "MSAMP": + if not is_msamp_available(): + raise ValueError("MSAMP was selected, but it is not installed on this machine.") + fp8_config["optimization_level"] = _ask_options( + "Which optimization level should be used?", + ["O1", "O2"], + lambda x: "O1" if x == 0 else "O2", + default=1, + ) + + elif fp8_config["backend"] == "AO": + if not is_torchao_available(): + raise ValueError("torchao was selected, but it is not installed on this machine.") + fp8_config["enable_fsdp_float8_all_gather"] = _ask_field( + "Do you want to enable FSDP2 float8 all gather? This is recommended for better performance if using FSDP2. [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + ) + fp8_config["pad_inner_dim"] = _ask_field( + "Do you want to pad the inner dimension of weight matrices before float8 matmuls? This is required for _scaled_mm which has strict alignment requirements. Note: padding may cause memory spikes. [YES/no]: ", + _convert_yes_no_to_bool, + default=True, + ) + + if use_dynamo and mixed_precision == "no" and not use_cpu: + print( + "Torch dynamo used without mixed precision requires TF32 to be efficient. Accelerate will enable it by default when launching your scripts." + ) + + if distributed_type == DistributedType.XLA and mixed_precision == "bf16": + tpu_downcast_bf16 = _ask_field( + "Should `torch.float` be cast as `bfloat16` and `torch.double` remain `float32` on TPUs?", default="no" + ) + + return ClusterConfig( + compute_environment=ComputeEnvironment.LOCAL_MACHINE, + distributed_type=distributed_type, + num_processes=num_processes, + gpu_ids=gpu_ids, + mixed_precision=mixed_precision, + downcast_bf16=tpu_downcast_bf16, + machine_rank=machine_rank, + num_machines=num_machines, + main_process_ip=main_process_ip, + main_process_port=main_process_port, + main_training_function=main_training_function, + fp8_config=fp8_config, + deepspeed_config=deepspeed_config, + fsdp_config=fsdp_config, + parallelism_config=parallelism_config, + megatron_lm_config=megatron_lm_config, + mpirun_config=mpirun_config, + use_cpu=use_cpu, + rdzv_backend=rdzv_backend, + same_network=same_network, + commands=tpu_commands, + command_file=tpu_command_file, + tpu_env=tpu_env, + tpu_name=tpu_name, + tpu_vm=tpu_vm, + tpu_zone=tpu_zone, + tpu_use_sudo=tpu_use_sudo, + tpu_use_cluster=tpu_use_cluster, + dynamo_config=dynamo_config, + debug=debug, + enable_cpu_affinity=enable_cpu_affinity, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config.py new file mode 100644 index 0000000000000000000000000000000000000000..72414f2abe62d76bd5133f4b0ed99bf34133f6f6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os + +from accelerate.utils import ComputeEnvironment + +from .cluster import get_cluster_input +from .config_args import cache_dir, default_config_file, default_yaml_config_file, load_config_from_file # noqa: F401 +from .config_utils import _ask_field, _ask_options, _convert_compute_environment # noqa: F401 +from .sagemaker import get_sagemaker_input + + +description = "Launches a series of prompts to create and save a `default_config.yaml` configuration file for your training system. Should always be ran first on your machine" + + +def get_user_input(): + compute_environment = _ask_options( + "In which compute environment are you running?", + ["This machine", "AWS (Amazon SageMaker)"], + _convert_compute_environment, + ) + if compute_environment == ComputeEnvironment.AMAZON_SAGEMAKER: + config = get_sagemaker_input() + else: + config = get_cluster_input() + return config + + +def config_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("config", description=description) + else: + parser = argparse.ArgumentParser("Accelerate config command", description=description) + + parser.add_argument( + "--config_file", + default=None, + help=( + "The path to use to store the config file. Will default to a file named default_config.yaml in the cache " + "location, which is the content of the environment `HF_HOME` suffixed with 'accelerate', or if you don't have " + "such an environment variable, your cache directory ('~/.cache' or the content of `XDG_CACHE_HOME`) suffixed " + "with 'huggingface'." + ), + ) + + if subparsers is not None: + parser.set_defaults(func=config_command) + return parser + + +def config_command(args): + config = get_user_input() + if args.config_file is not None: + config_file = args.config_file + else: + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) + config_file = default_yaml_config_file + + if config_file.endswith(".json"): + config.to_json_file(config_file) + else: + config.to_yaml_file(config_file) + print(f"accelerate configuration saved at {config_file}") + + +def main(): + parser = config_command_parser() + args = parser.parse_args() + config_command(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_args.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_args.py new file mode 100644 index 0000000000000000000000000000000000000000..9a50fade655da8e1ef766817e013185728c6b7bc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_args.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from dataclasses import dataclass +from enum import Enum +from typing import Optional, Union + +import yaml + +from ...utils import ComputeEnvironment, DistributedType, SageMakerDistributedType +from ...utils.constants import SAGEMAKER_PYTHON_VERSION, SAGEMAKER_PYTORCH_VERSION, SAGEMAKER_TRANSFORMERS_VERSION + + +hf_cache_home = os.path.expanduser( + os.environ.get("HF_HOME", os.path.join(os.environ.get("XDG_CACHE_HOME", "~/.cache"), "huggingface")) +) +cache_dir = os.path.join(hf_cache_home, "accelerate") +default_json_config_file = os.path.join(cache_dir, "default_config.yaml") +default_yaml_config_file = os.path.join(cache_dir, "default_config.yaml") + +# For backward compatibility: the default config is the json one if it's the only existing file. +if os.path.isfile(default_yaml_config_file) or not os.path.isfile(default_json_config_file): + default_config_file = default_yaml_config_file +else: + default_config_file = default_json_config_file + + +def load_config_from_file(config_file): + if config_file is not None: + if not os.path.isfile(config_file): + raise FileNotFoundError( + f"The passed configuration file `{config_file}` does not exist. " + "Please pass an existing file to `accelerate launch`, or use the default one " + "created through `accelerate config` and run `accelerate launch` " + "without the `--config_file` argument." + ) + else: + config_file = default_config_file + with open(config_file, encoding="utf-8") as f: + if config_file.endswith(".json"): + if ( + json.load(f).get("compute_environment", ComputeEnvironment.LOCAL_MACHINE) + == ComputeEnvironment.LOCAL_MACHINE + ): + config_class = ClusterConfig + else: + config_class = SageMakerConfig + return config_class.from_json_file(json_file=config_file) + else: + if ( + yaml.safe_load(f).get("compute_environment", ComputeEnvironment.LOCAL_MACHINE) + == ComputeEnvironment.LOCAL_MACHINE + ): + config_class = ClusterConfig + else: + config_class = SageMakerConfig + return config_class.from_yaml_file(yaml_file=config_file) + + +@dataclass +class BaseConfig: + compute_environment: ComputeEnvironment + distributed_type: Union[DistributedType, SageMakerDistributedType] + mixed_precision: str + use_cpu: bool + debug: bool + + def to_dict(self): + result = self.__dict__ + # For serialization, it's best to convert Enums to strings (or their underlying value type). + + def _convert_enums(value): + if isinstance(value, Enum): + return value.value + if isinstance(value, dict): + if not bool(value): + return None + for key1, value1 in value.items(): + value[key1] = _convert_enums(value1) + return value + + for key, value in result.items(): + result[key] = _convert_enums(value) + result = {k: v for k, v in result.items() if v is not None} + return result + + @staticmethod + def process_config(config_dict): + """ + Processes `config_dict` and sets default values for any missing keys + """ + if "compute_environment" not in config_dict: + config_dict["compute_environment"] = ComputeEnvironment.LOCAL_MACHINE + if "distributed_type" not in config_dict: + raise ValueError("A `distributed_type` must be specified in the config file.") + if "num_processes" not in config_dict and config_dict["distributed_type"] == DistributedType.NO: + config_dict["num_processes"] = 1 + if "mixed_precision" not in config_dict: + config_dict["mixed_precision"] = "fp16" if ("fp16" in config_dict and config_dict["fp16"]) else None + if "fp16" in config_dict: # Convert the config to the new format. + del config_dict["fp16"] + if "dynamo_backend" in config_dict: # Convert the config to the new format. + dynamo_backend = config_dict.pop("dynamo_backend") + config_dict["dynamo_config"] = {} if dynamo_backend == "NO" else {"dynamo_backend": dynamo_backend} + if "use_cpu" not in config_dict: + config_dict["use_cpu"] = False + if "debug" not in config_dict: + config_dict["debug"] = False + if "enable_cpu_affinity" not in config_dict: + config_dict["enable_cpu_affinity"] = False + return config_dict + + @classmethod + def from_json_file(cls, json_file=None): + json_file = default_json_config_file if json_file is None else json_file + with open(json_file, encoding="utf-8") as f: + config_dict = json.load(f) + config_dict = cls.process_config(config_dict) + extra_keys = sorted(set(config_dict.keys()) - set(cls.__dataclass_fields__.keys())) + if len(extra_keys) > 0: + raise ValueError( + f"The config file at {json_file} had unknown keys ({extra_keys}), please try upgrading your `accelerate`" + " version or fix (and potentially remove) these keys from your config file." + ) + + return cls(**config_dict) + + def to_json_file(self, json_file): + with open(json_file, "w", encoding="utf-8") as f: + content = json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n" + f.write(content) + + @classmethod + def from_yaml_file(cls, yaml_file=None): + yaml_file = default_yaml_config_file if yaml_file is None else yaml_file + with open(yaml_file, encoding="utf-8") as f: + config_dict = yaml.safe_load(f) + config_dict = cls.process_config(config_dict) + extra_keys = sorted(set(config_dict.keys()) - set(cls.__dataclass_fields__.keys())) + if len(extra_keys) > 0: + raise ValueError( + f"The config file at {yaml_file} had unknown keys ({extra_keys}), please try upgrading your `accelerate`" + " version or fix (and potentially remove) these keys from your config file." + ) + return cls(**config_dict) + + def to_yaml_file(self, yaml_file): + with open(yaml_file, "w", encoding="utf-8") as f: + yaml.safe_dump(self.to_dict(), f) + + def __post_init__(self): + if isinstance(self.compute_environment, str): + self.compute_environment = ComputeEnvironment(self.compute_environment) + if isinstance(self.distributed_type, str): + if self.compute_environment == ComputeEnvironment.AMAZON_SAGEMAKER: + self.distributed_type = SageMakerDistributedType(self.distributed_type) + else: + self.distributed_type = DistributedType(self.distributed_type) + if getattr(self, "dynamo_config", None) is None: + self.dynamo_config = {} + + +@dataclass +class ClusterConfig(BaseConfig): + num_processes: int = -1 # For instance if we use SLURM and the user manually passes it in + machine_rank: int = 0 + num_machines: int = 1 + gpu_ids: Optional[str] = None + main_process_ip: Optional[str] = None + main_process_port: Optional[int] = None + rdzv_backend: Optional[str] = "static" + same_network: Optional[bool] = False + main_training_function: str = "main" + enable_cpu_affinity: bool = False + + # args for FP8 training + fp8_config: Optional[dict] = None + # args for deepspeed_plugin + deepspeed_config: Optional[dict] = None + # args for fsdp + fsdp_config: Optional[dict] = None + # args for parallelism config + parallelism_config: Optional[dict] = None + # args for megatron_lm + megatron_lm_config: Optional[dict] = None + # args for mpirun + mpirun_config: Optional[dict] = None + # args for TPU + downcast_bf16: bool = False + + # args for TPU pods + tpu_name: Optional[str] = None + tpu_zone: Optional[str] = None + tpu_use_cluster: bool = False + tpu_use_sudo: bool = False + command_file: Optional[str] = None + commands: list[str] = None + tpu_vm: list[str] = None + tpu_env: list[str] = None + + # args for dynamo + dynamo_config: Optional[dict] = None + + def __post_init__(self): + if self.deepspeed_config is None: + self.deepspeed_config = {} + if self.fsdp_config is None: + self.fsdp_config = {} + if self.megatron_lm_config is None: + self.megatron_lm_config = {} + if self.mpirun_config is None: + self.mpirun_config = {} + if self.fp8_config is None: + self.fp8_config = {} + if self.parallelism_config is None: + self.parallelism_config = {} + return super().__post_init__() + + +@dataclass +class SageMakerConfig(BaseConfig): + ec2_instance_type: str + iam_role_name: str + image_uri: Optional[str] = None + profile: Optional[str] = None + region: str = "us-east-1" + num_machines: int = 1 + gpu_ids: str = "all" + base_job_name: str = f"accelerate-sagemaker-{num_machines}" + pytorch_version: str = SAGEMAKER_PYTORCH_VERSION + transformers_version: str = SAGEMAKER_TRANSFORMERS_VERSION + py_version: str = SAGEMAKER_PYTHON_VERSION + sagemaker_inputs_file: Optional[str] = None + sagemaker_metrics_file: Optional[str] = None + additional_args: Optional[dict] = None + dynamo_config: Optional[dict] = None + enable_cpu_affinity: bool = False diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d29bf4726a8205e1d01fb5f279ddbc7d7160b944 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/config_utils.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from ...utils.dataclasses import ( + ComputeEnvironment, + DistributedType, + DynamoBackend, + FP8BackendType, + PrecisionType, + SageMakerDistributedType, +) +from ..menu import BulletMenu + + +DYNAMO_BACKENDS = [ + "EAGER", + "AOT_EAGER", + "INDUCTOR", + "AOT_TS_NVFUSER", + "NVPRIMS_NVFUSER", + "CUDAGRAPHS", + "OFI", + "FX2TRT", + "ONNXRT", + "TENSORRT", + "AOT_TORCHXLA_TRACE_ONCE", + "TORHCHXLA_TRACE_ONCE", + "TVM", +] + + +def _ask_field(input_text, convert_value=None, default=None, error_message=None): + ask_again = True + while ask_again: + result = input(input_text) + try: + if default is not None and len(result) == 0: + return default + return convert_value(result) if convert_value is not None else result + except Exception: + if error_message is not None: + print(error_message) + + +def _ask_options(input_text, options=[], convert_value=None, default=0): + menu = BulletMenu(input_text, options) + result = menu.run(default_choice=default) + return convert_value(result) if convert_value is not None else result + + +def _convert_compute_environment(value): + value = int(value) + return ComputeEnvironment(["LOCAL_MACHINE", "AMAZON_SAGEMAKER"][value]) + + +def _convert_distributed_mode(value): + value = int(value) + return DistributedType( + [ + "NO", + "MULTI_CPU", + "MULTI_XPU", + "MULTI_HPU", + "MULTI_GPU", + "MULTI_NPU", + "MULTI_MLU", + "MULTI_SDAA", + "MULTI_MUSA", + "MULTI_NEURON", + "XLA", + ][value] + ) + + +def _convert_dynamo_backend(value): + value = int(value) + return DynamoBackend(DYNAMO_BACKENDS[value]).value + + +def _convert_mixed_precision(value): + value = int(value) + return PrecisionType(["no", "fp16", "bf16", "fp8"][value]) + + +def _convert_sagemaker_distributed_mode(value): + value = int(value) + return SageMakerDistributedType(["NO", "DATA_PARALLEL", "MODEL_PARALLEL"][value]) + + +def _convert_fp8_backend(value): + value = int(value) + return FP8BackendType(["AO", "TE", "MSAMP"][value]) + + +def _convert_yes_no_to_bool(value): + return {"yes": True, "no": False}[value.lower()] + + +class SubcommandHelpFormatter(argparse.RawDescriptionHelpFormatter): + """ + A custom formatter that will remove the usage line from the help message for subcommands. + """ + + def _format_usage(self, usage, actions, groups, prefix): + usage = super()._format_usage(usage, actions, groups, prefix) + usage = usage.replace(" [] ", "") + return usage diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/default.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/default.py new file mode 100644 index 0000000000000000000000000000000000000000..c505d8be937a1b59151e58f9298ef920e9f06f25 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/default.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +import torch + +from ...utils import ( + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_xpu_available, +) +from .config_args import ClusterConfig, default_json_config_file +from .config_utils import SubcommandHelpFormatter + + +description = "Create a default config file for Accelerate with only a few flags set." + + +def write_basic_config(mixed_precision="no", save_location: str = default_json_config_file): + """ + Creates and saves a basic cluster config to be used on a local machine with potentially multiple GPUs. Will also + set CPU if it is a CPU-only machine. + + Args: + mixed_precision (`str`, *optional*, defaults to "no"): + Mixed Precision to use. Should be one of "no", "fp16", or "bf16" + save_location (`str`, *optional*, defaults to `default_json_config_file`): + Optional custom save location. Should be passed to `--config_file` when using `accelerate launch`. Default + location is inside the huggingface cache folder (`~/.cache/huggingface`) but can be overridden by setting + the `HF_HOME` environmental variable, followed by `accelerate/default_config.yaml`. + """ + path = Path(save_location) + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + print( + f"Configuration already exists at {save_location}, will not override. Run `accelerate config` manually or pass a different `save_location`." + ) + return False + mixed_precision = mixed_precision.lower() + if mixed_precision not in ["no", "fp16", "bf16", "fp8"]: + raise ValueError( + f"`mixed_precision` should be one of 'no', 'fp16', 'bf16', or 'fp8'. Received {mixed_precision}" + ) + config = { + "compute_environment": "LOCAL_MACHINE", + "mixed_precision": mixed_precision, + } + if is_mlu_available(): + num_mlus = torch.mlu.device_count() + config["num_processes"] = num_mlus + config["use_cpu"] = False + if num_mlus > 1: + config["distributed_type"] = "MULTI_MLU" + else: + config["distributed_type"] = "NO" + if is_sdaa_available(): + num_sdaas = torch.sdaa.device_count() + config["num_processes"] = num_sdaas + config["use_cpu"] = False + if num_sdaas > 1: + config["distributed_type"] = "MULTI_SDAA" + else: + config["distributed_type"] = "NO" + elif is_musa_available(): + num_musas = torch.musa.device_count() + config["num_processes"] = num_musas + config["use_cpu"] = False + if num_musas > 1: + config["distributed_type"] = "MULTI_MUSA" + else: + config["distributed_type"] = "NO" + elif is_hpu_available(): + num_hpus = torch.hpu.device_count() + config["num_processes"] = num_hpus + config["use_cpu"] = False + if num_hpus > 1: + config["distributed_type"] = "MULTI_HPU" + else: + config["distributed_type"] = "NO" + elif torch.cuda.is_available(): + num_gpus = torch.cuda.device_count() + config["num_processes"] = num_gpus + config["use_cpu"] = False + if num_gpus > 1: + config["distributed_type"] = "MULTI_GPU" + else: + config["distributed_type"] = "NO" + elif is_xpu_available(): + num_xpus = torch.xpu.device_count() + config["num_processes"] = num_xpus + config["use_cpu"] = False + if num_xpus > 1: + config["distributed_type"] = "MULTI_XPU" + else: + config["distributed_type"] = "NO" + elif is_npu_available(): + num_npus = torch.npu.device_count() + config["num_processes"] = num_npus + config["use_cpu"] = False + if num_npus > 1: + config["distributed_type"] = "MULTI_NPU" + else: + config["distributed_type"] = "NO" + elif is_neuron_available(): + num_neuron_cores = torch.neuron.device_count() + config["num_processes"] = num_neuron_cores + config["use_cpu"] = False + if num_neuron_cores > 1: + config["distributed_type"] = "MULTI_NEURON" + else: + config["distributed_type"] = "NO" + else: + num_xpus = 0 + config["use_cpu"] = True + config["num_processes"] = 1 + config["distributed_type"] = "NO" + config["debug"] = False + config["enable_cpu_affinity"] = False + config = ClusterConfig(**config) + config.to_json_file(path) + return path + + +def default_command_parser(parser, parents): + parser = parser.add_parser("default", parents=parents, help=description, formatter_class=SubcommandHelpFormatter) + parser.add_argument( + "--config_file", + default=default_json_config_file, + help=( + "The path to use to store the config file. Will default to a file named default_config.yaml in the cache " + "location, which is the content of the environment `HF_HOME` suffixed with 'accelerate', or if you don't have " + "such an environment variable, your cache directory ('~/.cache' or the content of `XDG_CACHE_HOME`) suffixed " + "with 'huggingface'." + ), + dest="save_location", + ) + + parser.add_argument( + "--mixed_precision", + choices=["no", "fp16", "bf16"], + type=str, + help="Whether or not to use mixed precision training. " + "Choose between FP16 and BF16 (bfloat16) training. " + "BF16 training is only supported on Nvidia Ampere GPUs and PyTorch 1.10 or later.", + default="no", + ) + parser.set_defaults(func=default_config_command) + return parser + + +def default_config_command(args): + config_file = write_basic_config(args.mixed_precision, args.save_location) + if config_file: + print(f"accelerate configuration saved at {config_file}") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/sagemaker.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/sagemaker.py new file mode 100644 index 0000000000000000000000000000000000000000..5092ef31fc4715f901be6c1e7bfe80c0b140d767 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/sagemaker.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import os + +from ...utils.constants import SAGEMAKER_PARALLEL_EC2_INSTANCES, TORCH_DYNAMO_MODES +from ...utils.dataclasses import ComputeEnvironment, SageMakerDistributedType +from ...utils.imports import is_boto3_available +from .config_args import SageMakerConfig +from .config_utils import ( + DYNAMO_BACKENDS, + _ask_field, + _ask_options, + _convert_dynamo_backend, + _convert_mixed_precision, + _convert_sagemaker_distributed_mode, + _convert_yes_no_to_bool, +) + + +if is_boto3_available(): + import boto3 # noqa: F401 + + +def _create_iam_role_for_sagemaker(role_name): + iam_client = boto3.client("iam") + + sagemaker_trust_policy = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Principal": {"Service": "sagemaker.amazonaws.com"}, "Action": "sts:AssumeRole"} + ], + } + try: + # create the role, associated with the chosen trust policy + iam_client.create_role( + RoleName=role_name, AssumeRolePolicyDocument=json.dumps(sagemaker_trust_policy, indent=2) + ) + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "sagemaker:*", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:GetAuthorizationToken", + "cloudwatch:PutMetricData", + "cloudwatch:GetMetricData", + "cloudwatch:GetMetricStatistics", + "cloudwatch:ListMetrics", + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "s3:CreateBucket", + "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:PutObject", + ], + "Resource": "*", + } + ], + } + # attach policy to role + iam_client.put_role_policy( + RoleName=role_name, + PolicyName=f"{role_name}_policy_permission", + PolicyDocument=json.dumps(policy_document, indent=2), + ) + except iam_client.exceptions.EntityAlreadyExistsException: + print(f"role {role_name} already exists. Using existing one") + + +def _get_iam_role_arn(role_name): + iam_client = boto3.client("iam") + return iam_client.get_role(RoleName=role_name)["Role"]["Arn"] + + +def get_sagemaker_input(): + credentials_configuration = _ask_options( + "How do you want to authorize?", + ["AWS Profile", "Credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) "], + int, + ) + aws_profile = None + if credentials_configuration == 0: + aws_profile = _ask_field("Enter your AWS Profile name: [default] ", default="default") + os.environ["AWS_PROFILE"] = aws_profile + else: + print( + "Note you will need to provide AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY when you launch you training script with," + "`accelerate launch --aws_access_key_id XXX --aws_secret_access_key YYY`" + ) + aws_access_key_id = _ask_field("AWS Access Key ID: ") + os.environ["AWS_ACCESS_KEY_ID"] = aws_access_key_id + + aws_secret_access_key = _ask_field("AWS Secret Access Key: ") + os.environ["AWS_SECRET_ACCESS_KEY"] = aws_secret_access_key + + aws_region = _ask_field("Enter your AWS Region: [us-east-1]", default="us-east-1") + os.environ["AWS_DEFAULT_REGION"] = aws_region + + role_management = _ask_options( + "Do you already have an IAM Role for executing Amazon SageMaker Training Jobs?", + ["Provide IAM Role name", "Create new IAM role using credentials"], + int, + ) + if role_management == 0: + iam_role_name = _ask_field("Enter your IAM role name: ") + else: + iam_role_name = "accelerate_sagemaker_execution_role" + print(f'Accelerate will create an iam role "{iam_role_name}" using the provided credentials') + _create_iam_role_for_sagemaker(iam_role_name) + + is_custom_docker_image = _ask_field( + "Do you want to use custom Docker image? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + docker_image = None + if is_custom_docker_image: + docker_image = _ask_field("Enter your Docker image: ", lambda x: str(x).lower()) + + is_sagemaker_inputs_enabled = _ask_field( + "Do you want to provide SageMaker input channels with data locations? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + sagemaker_inputs_file = None + if is_sagemaker_inputs_enabled: + sagemaker_inputs_file = _ask_field( + "Enter the path to the SageMaker inputs TSV file with columns (channel_name, data_location): ", + lambda x: str(x).lower(), + ) + + is_sagemaker_metrics_enabled = _ask_field( + "Do you want to enable SageMaker metrics? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + sagemaker_metrics_file = None + if is_sagemaker_metrics_enabled: + sagemaker_metrics_file = _ask_field( + "Enter the path to the SageMaker metrics TSV file with columns (metric_name, metric_regex): ", + lambda x: str(x).lower(), + ) + + distributed_type = _ask_options( + "What is the distributed mode?", + ["No distributed training", "Data parallelism"], + _convert_sagemaker_distributed_mode, + ) + dynamo_config = {} + use_dynamo = _ask_field( + "Do you wish to optimize your script with torch dynamo?[yes/NO]:", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + if use_dynamo: + prefix = "dynamo_" + dynamo_config[prefix + "backend"] = _ask_options( + "Which dynamo backend would you like to use?", + [x.lower() for x in DYNAMO_BACKENDS], + _convert_dynamo_backend, + default=2, + ) + use_custom_options = _ask_field( + "Do you want to customize the defaults sent to torch.compile? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + if use_custom_options: + dynamo_config[prefix + "mode"] = _ask_options( + "Which mode do you want to use?", + TORCH_DYNAMO_MODES, + lambda x: TORCH_DYNAMO_MODES[int(x)], + default="default", + ) + dynamo_config[prefix + "use_fullgraph"] = _ask_field( + "Do you want the fullgraph mode or it is ok to break model into several subgraphs? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + dynamo_config[prefix + "use_dynamic"] = _ask_field( + "Do you want to enable dynamic shape tracing? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + dynamo_config[prefix + "use_regional_compilation"] = _ask_field( + "Do you want to enable regional compilation? [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + ec2_instance_query = "Which EC2 instance type you want to use for your training?" + if distributed_type != SageMakerDistributedType.NO: + ec2_instance_type = _ask_options( + ec2_instance_query, SAGEMAKER_PARALLEL_EC2_INSTANCES, lambda x: SAGEMAKER_PARALLEL_EC2_INSTANCES[int(x)] + ) + else: + ec2_instance_query += "? [ml.p3.2xlarge]:" + ec2_instance_type = _ask_field(ec2_instance_query, lambda x: str(x).lower(), default="ml.p3.2xlarge") + + debug = False + if distributed_type != SageMakerDistributedType.NO: + debug = _ask_field( + "Should distributed operations be checked while running for errors? This can avoid timeout issues but will be slower. [yes/NO]: ", + _convert_yes_no_to_bool, + default=False, + error_message="Please enter yes or no.", + ) + + num_machines = 1 + if distributed_type in (SageMakerDistributedType.DATA_PARALLEL, SageMakerDistributedType.MODEL_PARALLEL): + num_machines = _ask_field( + "How many machines do you want use? [1]: ", + int, + default=1, + ) + + mixed_precision = _ask_options( + "Do you wish to use FP16 or BF16 (mixed precision)?", + ["no", "fp16", "bf16", "fp8"], + _convert_mixed_precision, + ) + + if use_dynamo and mixed_precision == "no": + print( + "Torch dynamo used without mixed precision requires TF32 to be efficient. Accelerate will enable it by default when launching your scripts." + ) + + return SageMakerConfig( + image_uri=docker_image, + compute_environment=ComputeEnvironment.AMAZON_SAGEMAKER, + distributed_type=distributed_type, + use_cpu=False, + dynamo_config=dynamo_config, + ec2_instance_type=ec2_instance_type, + profile=aws_profile, + region=aws_region, + iam_role_name=iam_role_name, + mixed_precision=mixed_precision, + num_machines=num_machines, + sagemaker_inputs_file=sagemaker_inputs_file, + sagemaker_metrics_file=sagemaker_metrics_file, + debug=debug, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/update.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/update.py new file mode 100644 index 0000000000000000000000000000000000000000..369bb638e60516cb1ef46dbe53497c915834cbe4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/config/update.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path + +from .config_args import default_config_file, load_config_from_file +from .config_utils import SubcommandHelpFormatter + + +description = "Update an existing config file with the latest defaults while maintaining the old configuration." + + +def update_config(args): + """ + Update an existing config file with the latest defaults while maintaining the old configuration. + """ + config_file = args.config_file + if config_file is None and Path(default_config_file).exists(): + config_file = default_config_file + elif not Path(config_file).exists(): + raise ValueError(f"The passed config file located at {config_file} doesn't exist.") + config = load_config_from_file(config_file) + + if config_file.endswith(".json"): + config.to_json_file(config_file) + else: + config.to_yaml_file(config_file) + return config_file + + +def update_command_parser(parser, parents): + parser = parser.add_parser("update", parents=parents, help=description, formatter_class=SubcommandHelpFormatter) + parser.add_argument( + "--config_file", + default=None, + help=( + "The path to the config file to update. Will default to a file named default_config.yaml in the cache " + "location, which is the content of the environment `HF_HOME` suffixed with 'accelerate', or if you don't have " + "such an environment variable, your cache directory ('~/.cache' or the content of `XDG_CACHE_HOME`) suffixed " + "with 'huggingface'." + ), + ) + + parser.set_defaults(func=update_config_command) + return parser + + +def update_config_command(args): + config_file = update_config(args) + print(f"Successfully updated the configuration file at {config_file}.") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/env.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/env.py new file mode 100644 index 0000000000000000000000000000000000000000..22790c40bfb7c489dda7e5e5aa172998f80fd45b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/env.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python + +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import platform +import subprocess + +import numpy as np +import psutil +import torch + +from accelerate import __version__ as version +from accelerate.commands.config import default_config_file, load_config_from_file + +from ..utils import ( + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_xpu_available, +) + + +def env_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("env") + else: + parser = argparse.ArgumentParser("Accelerate env command") + + parser.add_argument( + "--config_file", default=None, help="The config file to use for the default values in the launching script." + ) + + if subparsers is not None: + parser.set_defaults(func=env_command) + return parser + + +def env_command(args): + pt_version = torch.__version__ + pt_cuda_available = torch.cuda.is_available() + pt_xpu_available = is_xpu_available() + pt_mlu_available = is_mlu_available() + pt_sdaa_available = is_sdaa_available() + pt_musa_available = is_musa_available() + pt_npu_available = is_npu_available() + pt_neuron_available = is_neuron_available() + + accelerator = "N/A" + if pt_cuda_available: + accelerator = "CUDA" + elif pt_xpu_available: + accelerator = "XPU" + elif pt_mlu_available: + accelerator = "MLU" + elif pt_sdaa_available: + accelerator = "SDAA" + elif pt_musa_available: + accelerator = "MUSA" + elif pt_npu_available: + accelerator = "NPU" + elif pt_neuron_available: + accelerator = "NEURON" + + accelerate_config = "Not found" + # Get the default from the config file. + if args.config_file is not None or os.path.isfile(default_config_file): + accelerate_config = load_config_from_file(args.config_file).to_dict() + + # if we can run which, get it + command = None + bash_location = "Not found" + if os.name == "nt": + command = ["where", "accelerate"] + elif os.name == "posix": + command = ["which", "accelerate"] + if command is not None: + bash_location = subprocess.check_output(command, text=True, stderr=subprocess.STDOUT).strip() + info = { + "`Accelerate` version": version, + "Platform": platform.platform(), + "`accelerate` bash location": bash_location, + "Python version": platform.python_version(), + "Numpy version": np.__version__, + "PyTorch version": f"{pt_version}", + "PyTorch accelerator": accelerator, + "System RAM": f"{psutil.virtual_memory().total / 1024**3:.2f} GB", + } + if pt_cuda_available: + info["GPU type"] = torch.cuda.get_device_name() + elif pt_xpu_available: + info["XPU type"] = torch.xpu.get_device_name() + elif pt_mlu_available: + info["MLU type"] = torch.mlu.get_device_name() + elif pt_sdaa_available: + info["SDAA type"] = torch.sdaa.get_device_name() + elif pt_musa_available: + info["MUSA type"] = torch.musa.get_device_name() + elif pt_neuron_available: + info["NEURON type"] = torch.neuron.get_device_name() + elif pt_npu_available: + info["CANN version"] = torch.version.cann + + print("\nCopy-and-paste the text below in your GitHub issue\n") + print("\n".join([f"- {prop}: {val}" for prop, val in info.items()])) + + print("- `Accelerate` default config:" if args.config_file is None else "- `Accelerate` config passed:") + accelerate_config_str = ( + "\n".join([f"\t- {prop}: {val}" for prop, val in accelerate_config.items()]) + if isinstance(accelerate_config, dict) + else f"\t{accelerate_config}" + ) + print(accelerate_config_str) + + info["`Accelerate` configs"] = accelerate_config + + return info + + +def main() -> int: + parser = env_command_parser() + args = parser.parse_args() + env_command(args) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/estimate.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/estimate.py new file mode 100644 index 0000000000000000000000000000000000000000..c1a8b41cead9d0d309bf482bde5a8f558b18b6fd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/estimate.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python + +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +import torch +from huggingface_hub import model_info +from huggingface_hub.utils import GatedRepoError, RepositoryNotFoundError + +from accelerate import init_empty_weights +from accelerate.commands.utils import CustomArgumentParser +from accelerate.utils import ( + calculate_maximum_sizes, + convert_bytes, + is_timm_available, + is_transformers_available, +) + + +if is_transformers_available(): + import transformers + from transformers import AutoConfig, AutoModel + +if is_timm_available(): + import timm + + +def verify_on_hub(repo: str, token: Optional[str] = None): + "Verifies that the model is on the hub and returns the model info." + try: + return model_info(repo, token=token) + except (OSError, GatedRepoError): + return "gated" + except RepositoryNotFoundError: + return "repo" + + +def check_has_model(error): + """ + Checks what library spawned `error` when a model is not found + """ + if is_timm_available() and isinstance(error, RuntimeError) and "Unknown model" in error.args[0]: + return "timm" + elif ( + is_transformers_available() + and isinstance(error, OSError) + and "does not appear to have a file named" in error.args[0] + ): + return "transformers" + else: + return "unknown" + + +def create_empty_model( + model_name: str, library_name: str, trust_remote_code: bool = False, access_token: Optional[str] = None +): + """ + Creates an empty model in full precision from its parent library on the `Hub` to calculate the overall memory + consumption. + + Args: + model_name (`str`): + The model name on the Hub + library_name (`str`): + The library the model has an integration with, such as `transformers`. Will be used if `model_name` has no + metadata on the Hub to determine the library. + trust_remote_code (`bool`, `optional`, defaults to `False`): + Whether or not to allow for custom models defined on the Hub in their own modeling files. This option + should only be set to `True` for repositories you trust and in which you have read the code, as it will + execute code present on the Hub on your local machine. + access_token (`str`, `optional`, defaults to `None`): + The access token to use to access private or gated models on the Hub. (for use on the Gradio app) + + Returns: + `torch.nn.Module`: The torch model that has been initialized on the `meta` device. + + """ + model_info = verify_on_hub(model_name, access_token) + # Simplified errors + if model_info == "gated": + raise OSError( + f"Repo for model `{model_name}` is gated. You must be authenticated to access it. Please run `huggingface-cli login`." + ) + elif model_info == "repo": + raise OSError( + f"Repo for model `{model_name}` does not exist on the Hub. If you are trying to access a private repo," + " make sure you are authenticated via `huggingface-cli login` and have access." + ) + if library_name is None: + library_name = getattr(model_info, "library_name", False) + if not library_name: + raise ValueError( + f"Model `{model_name}` does not have any library metadata on the Hub, please manually pass in a `--library_name` to use (such as `transformers`)" + ) + if library_name == "transformers": + if not is_transformers_available(): + raise ImportError( + f"To check `{model_name}`, `transformers` must be installed. Please install it via `pip install transformers`" + ) + print(f"Loading pretrained config for `{model_name}` from `transformers`...") + if model_info.config is None: + raise RuntimeError(f"Tried to load `{model_name}` with `transformers` but it does not have any metadata.") + + auto_map = model_info.config.get("auto_map", False) + config = AutoConfig.from_pretrained(model_name, trust_remote_code=trust_remote_code, token=access_token) + with init_empty_weights(): + # remote code could specify a specific `AutoModel` class in the `auto_map` + constructor = AutoModel + if isinstance(auto_map, dict): + value = None + for key in auto_map.keys(): + if key.startswith("AutoModelFor"): + value = key + break + if value is not None: + constructor = getattr(transformers, value) + # we need to pass the dtype, otherwise it is going to use the torch_dtype that is saved in the config + model = constructor.from_config(config, torch_dtype=torch.float32, trust_remote_code=trust_remote_code) + elif library_name == "timm": + if not is_timm_available(): + raise ImportError( + f"To check `{model_name}`, `timm` must be installed. Please install it via `pip install timm`" + ) + print(f"Loading pretrained config for `{model_name}` from `timm`...") + with init_empty_weights(): + model = timm.create_model(model_name, pretrained=False) + else: + raise ValueError( + f"Library `{library_name}` is not supported yet, please open an issue on GitHub for us to add support." + ) + return model + + +def create_ascii_table(headers: list, rows: list, title: str): + "Creates a pretty table from a list of rows, minimal version of `tabulate`." + sep_char, in_between = "│", "─" + column_widths = [] + for i in range(len(headers)): + column_values = [row[i] for row in rows] + [headers[i]] + max_column_width = max(len(value) for value in column_values) + column_widths.append(max_column_width) + + formats = [f"%{column_widths[i]}s" for i in range(len(rows[0]))] + + pattern = f"{sep_char}{sep_char.join(formats)}{sep_char}" + diff = 0 + + def make_row(left_char, middle_char, right_char): + return f"{left_char}{middle_char.join([in_between * n for n in column_widths])}{in_between * diff}{right_char}" + + separator = make_row("├", "┼", "┤") + if len(title) > sum(column_widths): + diff = abs(len(title) - len(separator)) + column_widths[-1] += diff + + # Update with diff + separator = make_row("├", "┼", "┤") + initial_rows = [ + make_row("┌", in_between, "┐"), + f"{sep_char}{title.center(len(separator) - 2)}{sep_char}", + make_row("├", "┬", "┤"), + ] + table = "\n".join(initial_rows) + "\n" + column_widths[-1] += diff + centered_line = [text.center(column_widths[i]) for i, text in enumerate(headers)] + table += f"{pattern % tuple(centered_line)}\n{separator}\n" + for i, line in enumerate(rows): + centered_line = [t.center(column_widths[i]) for i, t in enumerate(line)] + table += f"{pattern % tuple(centered_line)}\n" + table += f"└{'┴'.join([in_between * n for n in column_widths])}┘" + + return table + + +def estimate_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("estimate-memory") + else: + parser = CustomArgumentParser( + description="Model size estimator for fitting a model onto device(e.g. cuda, xpu) memory." + ) + + parser.add_argument("model_name", type=str, help="The model name on the Hugging Face Hub.") + parser.add_argument( + "--library_name", + type=str, + help="The library the model has an integration with, such as `transformers`, needed only if this information is not stored on the Hub.", + choices=["timm", "transformers"], + ) + parser.add_argument( + "--dtypes", + type=str, + nargs="+", + default=["float32", "float16", "int8", "int4"], + help="The dtypes to use for the model, must be one (or many) of `float32`, `float16`, `int8`, and `int4`", + choices=["float32", "float16", "int8", "int4"], + ) + parser.add_argument( + "--trust_remote_code", + action="store_true", + help="""Whether or not to allow for custom models defined on the Hub in their own modeling files. This flag + should only be used for repositories you trust and in which you have read the code, as it will execute + code present on the Hub on your local machine.""", + default=False, + ) + + if subparsers is not None: + parser.set_defaults(func=estimate_command) + return parser + + +def estimate_training_usage(bytes: int, mixed_precision: str, msamp_config: Optional[str] = None) -> dict: + """ + Given an amount of `bytes` and `mixed_precision`, calculates how much training memory is needed for a batch size of + 1. + + Args: + bytes (`int`): + The size of the model being trained. + mixed_precision (`str`): + The mixed precision that would be ran. + msamp_config (`str`): + The msamp config to estimate the training memory for if `mixed_precision` is set to `"fp8"`. + """ + memory_sizes = {"model": -1, "optimizer": -1, "gradients": -1, "step": -1} + fp32_size = bytes + fp16_size = bytes // 2 + + if mixed_precision == "float32": + memory_sizes["model"] = fp32_size + memory_sizes["gradients"] = fp32_size + memory_sizes["optimizer"] = fp32_size * 2 + memory_sizes["step"] = fp32_size * 4 + elif mixed_precision in ("float16", "bfloat16") or (mixed_precision == "fp8" and msamp_config is None): + # With native `TransformersEngine`, there is no memory savings with FP8 + # With mixed precision training, the model has weights stored + # in FP16 and FP32 + memory_sizes["model"] = fp32_size + # 1.5 from weight gradient + computation (GEMM) + memory_sizes["gradients"] = fp32_size + fp16_size + # 2x from optimizer states + memory_sizes["optimizer"] = fp32_size * 2 # Optimizer states + memory_sizes["step"] = memory_sizes["optimizer"] + return memory_sizes + + +def gather_data(args): + "Creates an empty model and gathers the data for the sizes" + try: + model = create_empty_model( + args.model_name, library_name=args.library_name, trust_remote_code=args.trust_remote_code + ) + except (RuntimeError, OSError) as e: + library = check_has_model(e) + if library != "unknown": + raise RuntimeError( + f"Tried to load `{args.model_name}` with `{library}` but a possible model to load was not found inside the repo." + ) + raise e + + total_size, largest_layer = calculate_maximum_sizes(model) + + data = [] + + for dtype in args.dtypes: + dtype_total_size = total_size + dtype_largest_layer = largest_layer[0] + dtype_training_size = estimate_training_usage(dtype_total_size, dtype) + if dtype == "float16": + dtype_total_size /= 2 + dtype_largest_layer /= 2 + elif dtype == "int8": + dtype_total_size /= 4 + dtype_largest_layer /= 4 + elif dtype == "int4": + dtype_total_size /= 8 + dtype_largest_layer /= 8 + data.append([dtype, dtype_largest_layer, dtype_total_size, dtype_training_size]) + return data + + +def estimate_command(args): + data = gather_data(args) + for row in data: + for i, item in enumerate(row): + if isinstance(item, (int, float)): + row[i] = convert_bytes(item) + elif isinstance(item, dict): + training_usage = max(item.values()) + row[i] = convert_bytes(training_usage) if training_usage != -1 else "N/A" + + headers = ["dtype", "Largest Layer", "Total Size", "Training using Adam"] + + title = f"Memory Usage for loading `{args.model_name}`" + table = create_ascii_table(headers, data, title) + print(table) + + +def main(): + parser = estimate_command_parser() + args = parser.parse_args() + estimate_command(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/launch.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/launch.py new file mode 100644 index 0000000000000000000000000000000000000000..3b7becf1b0c7f864b04e626b5313bae7647774cf --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/launch.py @@ -0,0 +1,1415 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import importlib +import logging +import os +import subprocess +import sys +from pathlib import Path + +import torch + +from accelerate.commands.config import default_config_file, load_config_from_file +from accelerate.commands.config.config_args import SageMakerConfig +from accelerate.commands.config.config_utils import DYNAMO_BACKENDS +from accelerate.commands.utils import CustomArgumentParser +from accelerate.state import get_int_from_env +from accelerate.utils import ( + ComputeEnvironment, + DistributedType, + PrepareForLaunch, + _filter_args, + check_cuda_p2p_ib_support, + convert_dict_to_env_variables, + is_bf16_available, + is_deepspeed_available, + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_rich_available, + is_sagemaker_available, + is_sdaa_available, + is_torch_xla_available, + is_xpu_available, + patch_environment, + prepare_deepspeed_cmd_env, + prepare_multi_gpu_env, + prepare_sagemager_args_inputs, + prepare_simple_launcher_cmd_env, + prepare_tpu, + str_to_bool, +) +from accelerate.utils.constants import DEEPSPEED_MULTINODE_LAUNCHERS, TORCH_DYNAMO_MODES + + +if is_rich_available(): + from rich import get_console + from rich.logging import RichHandler + + FORMAT = "%(message)s" + logging.basicConfig(format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) + + +logger = logging.getLogger(__name__) + + +options_to_group = { + "multi_gpu": "Distributed GPUs", + "tpu": "TPU", + "use_deepspeed": "DeepSpeed Arguments", + "use_fsdp": "FSDP Arguments", + "use_megatron_lm": "Megatron-LM Arguments", + "fp8_backend": "FP8 Arguments", +} + + +def clean_option(option): + "Finds all cases of - after the first two characters and changes them to _" + if "fp8_backend" in option: + option = "--fp8_backend" + if option.startswith("--"): + return option[2:].replace("-", "_") + + +class CustomHelpFormatter(argparse.HelpFormatter): + """ + This is a custom help formatter that will hide all arguments that are not used in the command line when the help is + called. This is useful for the case where the user is using a specific platform and only wants to see the arguments + for that platform. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.titles = [ + "Hardware Selection Arguments", + "Resource Selection Arguments", + "Training Paradigm Arguments", + "positional arguments", + "optional arguments", + ] + + def add_argument(self, action: argparse.Action): + if "accelerate" in sys.argv[0] and "launch" in sys.argv[1:]: + args = sys.argv[2:] + else: + args = sys.argv[1:] + + if len(args) > 1: + args = list(map(clean_option, args)) + used_platforms = [arg for arg in args if arg in options_to_group.keys()] + used_titles = [options_to_group[o] for o in used_platforms] + if action.container.title not in self.titles + used_titles: + action.help = argparse.SUPPRESS + elif action.container.title == "Hardware Selection Arguments": + if set(action.option_strings).isdisjoint(set(args)): + action.help = argparse.SUPPRESS + else: + action.help = action.help + " (currently selected)" + elif action.container.title == "Training Paradigm Arguments": + if set(action.option_strings).isdisjoint(set(args)): + action.help = argparse.SUPPRESS + else: + action.help = action.help + " (currently selected)" + + action.option_strings = [s for s in action.option_strings if "-" not in s[2:]] + super().add_argument(action) + + def end_section(self): + if len(self._current_section.items) < 2: + self._current_section.items = [] + self._current_section.heading = "" + super().end_section() + + +def launch_command_parser(subparsers=None): + description = "Launch a python script in a distributed scenario. Arguments can be passed in with either hyphens (`--num-processes=2`) or underscores (`--num_processes=2`)" + if subparsers is not None: + parser = subparsers.add_parser( + "launch", description=description, add_help=False, allow_abbrev=False, formatter_class=CustomHelpFormatter + ) + else: + parser = CustomArgumentParser( + "Accelerate launch command", + description=description, + add_help=False, + allow_abbrev=False, + formatter_class=CustomHelpFormatter, + ) + + parser.add_argument("-h", "--help", action="help", help="Show this help message and exit.") + + parser.add_argument( + "--config_file", + default=None, + help="The config file to use for the default values in the launching script.", + ) + parser.add_argument( + "--quiet", + "-q", + action="store_true", + help="Silence subprocess errors from the launch stack trace and only show the relevant tracebacks. (Only applicable to DeepSpeed and single-process configurations)", + ) + # Hardware selection arguments + hardware_args = parser.add_argument_group( + "Hardware Selection Arguments", "Arguments for selecting the hardware to be used." + ) + hardware_args.add_argument( + "--cpu", default=False, action="store_true", help="Whether or not to force the training on the CPU." + ) + hardware_args.add_argument( + "--multi_gpu", + default=False, + action="store_true", + help="Whether or not this should launch a distributed GPU training.", + ) + hardware_args.add_argument( + "--tpu", default=False, action="store_true", help="Whether or not this should launch a TPU training." + ) + # Resource selection arguments + resource_args = parser.add_argument_group( + "Resource Selection Arguments", "Arguments for fine-tuning how available hardware should be used." + ) + resource_args.add_argument( + "--mixed_precision", + type=str, + choices=["no", "fp16", "bf16", "fp8"], + help="Whether or not to use mixed precision training. " + "Choose between FP16 and BF16 (bfloat16) training. " + "BF16 training is only supported on Nvidia Ampere GPUs and PyTorch 1.10 or later.", + ) + resource_args.add_argument( + "--num_processes", type=int, default=None, help="The total number of processes to be launched in parallel." + ) + resource_args.add_argument( + "--num_machines", type=int, default=None, help="The total number of machines used in this training." + ) + resource_args.add_argument( + "--num_cpu_threads_per_process", + type=int, + default=None, + help="The number of CPU threads per process. Can be tuned for optimal performance.", + ) + resource_args.add_argument( + "--enable_cpu_affinity", + default=False, + action="store_true", + help="Whether or not CPU affinity and balancing should be enabled. Currently only supported on NVIDIA hardware.", + ) + # Dynamo arguments + resource_args.add_argument( + "--dynamo_backend", + type=str, + choices=["no"] + [b.lower() for b in DYNAMO_BACKENDS], + help="Choose a backend to optimize your training with dynamo, see more at " + "https://github.com/pytorch/torchdynamo.", + ) + resource_args.add_argument( + "--dynamo_mode", + type=str, + default="default", + choices=TORCH_DYNAMO_MODES, + help="Choose a mode to optimize your training with dynamo.", + ) + resource_args.add_argument( + "--dynamo_use_fullgraph", + default=False, + action="store_true", + help="Whether to use full graph mode for dynamo or it is ok to break model into several subgraphs", + ) + resource_args.add_argument( + "--dynamo_use_dynamic", + default=False, + action="store_true", + help="Whether to enable dynamic shape tracing.", + ) + resource_args.add_argument( + "--dynamo_use_regional_compilation", + default=False, + action="store_true", + help="Whether to enable regional compilation.", + ) + + # Training Paradigm arguments + paradigm_args = parser.add_argument_group( + "Training Paradigm Arguments", "Arguments for selecting which training paradigm to be used." + ) + paradigm_args.add_argument( + "--use_deepspeed", + default=False, + action="store_true", + help="Whether to use deepspeed.", + ) + paradigm_args.add_argument( + "--use_fsdp", + default=False, + action="store_true", + help="Whether to use fsdp.", + ) + paradigm_args.add_argument( + "--use_parallelism_config", + default=False, + action="store_true", + help="Whether to use the parallelism config to configure the N-d distributed training.", + ) + paradigm_args.add_argument( + "--use_megatron_lm", + default=False, + action="store_true", + help="Whether to use Megatron-LM.", + ) + + # distributed GPU training arguments + distributed_args = parser.add_argument_group("Distributed GPUs", "Arguments related to distributed GPU training.") + distributed_args.add_argument( + "--gpu_ids", + default=None, + help="What GPUs (by id) should be used for training on this machine as a comma-separated list", + ) + distributed_args.add_argument( + "--same_network", + default=False, + action="store_true", + help="Whether all machines used for multinode training exist on the same local network.", + ) + distributed_args.add_argument( + "--machine_rank", type=int, default=None, help="The rank of the machine on which this script is launched." + ) + distributed_args.add_argument( + "--main_process_ip", type=str, default=None, help="The IP address of the machine of rank 0." + ) + distributed_args.add_argument( + "--main_process_port", + type=int, + default=None, + help="The port to use to communicate with the machine of rank 0.", + ) + distributed_args.add_argument( + "-t", + "--tee", + default="0", + type=str, + help="Tee std streams into a log file and also to console.", + ) + distributed_args.add_argument( + "--log_dir", + type=str, + default=None, + help=( + "Base directory to use for log files when using torchrun/torch.distributed.run as launcher. " + "Use with --tee to redirect std streams info log files." + ), + ) + distributed_args.add_argument( + "--role", + type=str, + default="default", + help="User-defined role for the workers.", + ) + # Rendezvous related arguments + distributed_args.add_argument( + "--rdzv_backend", + type=str, + default="static", + help="The rendezvous method to use, such as 'static' (the default) or 'c10d'", + ) + distributed_args.add_argument( + "--rdzv_conf", + type=str, + default="", + help="Additional rendezvous configuration (=,=,...).", + ) + distributed_args.add_argument( + "--max_restarts", + type=int, + default=0, + help="Maximum number of worker group restarts before failing.", + ) + distributed_args.add_argument( + "--monitor_interval", + type=float, + default=0.1, + help="Interval, in seconds, to monitor the state of workers.", + ) + parser.add_argument( + "-m", + "--module", + action="store_true", + help="Change each process to interpret the launch script as a Python module, executing with the same behavior as 'python -m'.", + ) + parser.add_argument( + "--no_python", + action="store_true", + help="Skip prepending the training script with 'python' - just execute it directly. Useful when the script is not a Python script.", + ) + + # TPU arguments + tpu_args = parser.add_argument_group("TPU", "Arguments related to TPU.") + tpu_args.add_argument( + "--tpu_cluster", + action="store_true", + dest="tpu_use_cluster", + help="Whether to use a GCP TPU pod for training.", + ) + tpu_args.add_argument( + "--no_tpu_cluster", + action="store_false", + dest="tpu_use_cluster", + help="Should not be passed explicitly, this is for internal use only.", + ) + tpu_args.add_argument( + "--tpu_use_sudo", + action="store_true", + help="Whether to use `sudo` when running the TPU training script in each pod.", + ) + tpu_args.add_argument( + "--vm", + type=str, + action="append", + help=( + "List of single Compute VM instance names. " + "If not provided we assume usage of instance groups. For TPU pods." + ), + ) + tpu_args.add_argument( + "--env", + type=str, + action="append", + help="List of environment variables to set on the Compute VM instances. For TPU pods.", + ) + tpu_args.add_argument( + "--main_training_function", + type=str, + default=None, + help="The name of the main function to be executed in your script (only for TPU training).", + ) + tpu_args.add_argument( + "--downcast_bf16", + action="store_true", + help="Whether when using bf16 precision on TPUs if both float and double tensors are cast to bfloat16 or if double tensors remain as float32.", + ) + + # DeepSpeed arguments + deepspeed_args = parser.add_argument_group("DeepSpeed Arguments", "Arguments related to DeepSpeed.") + deepspeed_args.add_argument( + "--deepspeed_config_file", + default=None, + type=str, + help="DeepSpeed config file.", + ) + deepspeed_args.add_argument( + "--zero_stage", + default=None, + type=int, + help="DeepSpeed's ZeRO optimization stage (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to `2`.", + ) + deepspeed_args.add_argument( + "--offload_optimizer_device", + default=None, + type=str, + help="Decides where (none|cpu|nvme) to offload optimizer states (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to 'none'.", + ) + deepspeed_args.add_argument( + "--offload_param_device", + default=None, + type=str, + help="Decides where (none|cpu|nvme) to offload parameters (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to 'none'.", + ) + deepspeed_args.add_argument( + "--offload_optimizer_nvme_path", + default=None, + type=str, + help="Decides Nvme Path to offload optimizer states (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to 'none'.", + ) + deepspeed_args.add_argument( + "--offload_param_nvme_path", + default=None, + type=str, + help="Decides Nvme Path to offload parameters (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to 'none'.", + ) + deepspeed_args.add_argument( + "--gradient_accumulation_steps", + default=None, + type=int, + help="No of gradient_accumulation_steps used in your training script (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to `1`.", + ) + deepspeed_args.add_argument( + "--gradient_clipping", + default=None, + type=float, + help="gradient clipping value used in your training script (useful only when `use_deepspeed` flag is passed). " + "If unspecified, will default to `1.0`.", + ) + deepspeed_args.add_argument( + "--zero3_init_flag", + default=None, + type=str, + help="Decides Whether (true|false) to enable `deepspeed.zero.Init` for constructing massive models. " + "Only applicable with DeepSpeed ZeRO Stage-3. If unspecified, will default to `true`.", + ) + deepspeed_args.add_argument( + "--zero3_save_16bit_model", + default=None, + type=str, + help="Decides Whether (true|false) to save 16-bit model weights when using ZeRO Stage-3. " + "Only applicable with DeepSpeed ZeRO Stage-3. If unspecified, will default to `false`.", + ) + deepspeed_args.add_argument( + "--deepspeed_hostfile", + default=None, + type=str, + help="DeepSpeed hostfile for configuring multi-node compute resources.", + ) + deepspeed_args.add_argument( + "--deepspeed_exclusion_filter", + default=None, + type=str, + help="DeepSpeed exclusion filter string when using multi-node setup.", + ) + deepspeed_args.add_argument( + "--deepspeed_inclusion_filter", + default=None, + type=str, + help="DeepSpeed inclusion filter string when using multi-node setup.", + ) + deepspeed_args.add_argument( + "--deepspeed_multinode_launcher", + default=None, + type=str, + help="DeepSpeed multi-node launcher to use, e.g. `pdsh`, `standard`, `openmpi`, `mvapich`, `mpich`, `slurm`, `nossh` (requires DeepSpeed >= 0.14.5). If unspecified, will default to `pdsh`.", + ) + deepspeed_args.add_argument( + "--deepspeed_moe_layer_cls_names", + default=None, + type=str, + help="comma-separated list of transformer MoE layer class names (case-sensitive) to wrap ,e.g, `MixtralSparseMoeBlock`, `Qwen2MoeSparseMoeBlock`, `JetMoEAttention,JetMoEBlock` ..." + " (useful only when `use_deepspeed` flag is passed).", + ) + + # fsdp arguments + fsdp_args = parser.add_argument_group("FSDP Arguments", "Arguments related to Fully Shared Data Parallelism.") + fsdp_args.add_argument( + "--fsdp_version", + type=str, + default="1", + choices=["1", "2"], + help="FSDP version to use. (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_offload_params", + default="false", + type=str, + help="Decides Whether (true|false) to offload parameters and gradients to CPU. (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_min_num_params", + type=int, + default=int(1e8), + help="FSDP's minimum number of parameters for Default Auto Wrapping. (useful only when `use_fsdp` flag is passed).", + ) + # We enable this for backwards compatibility, throw a warning if this is set in `FullyShardedDataParallelPlugin` + fsdp_args.add_argument( + "--fsdp_sharding_strategy", + type=str, + default="FULL_SHARD", + help="FSDP's sharding strategy. (useful only when `use_fsdp` flag is passed and `fsdp_version=1`).", + ) + fsdp_args.add_argument( + "--fsdp_reshard_after_forward", + type=str, + default="true", + help="FSDP's Reshard After Forward Strategy. (useful only when `use_fsdp` flag is passed). Supports either boolean (FSDP2) or `FULL_SHARD | SHARD_GRAD_OP | NO_RESHARD` (FSDP1).", + ) + fsdp_args.add_argument( + "--fsdp_auto_wrap_policy", + type=str, + default=None, + help="FSDP's auto wrap policy. (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_transformer_layer_cls_to_wrap", + default=None, + type=str, + help="Transformer layer class name (case-sensitive) to wrap ,e.g, `BertLayer`, `GPTJBlock`, `T5Block` .... " + "(useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_backward_prefetch", + default=None, + type=str, + help="FSDP's backward prefetch policy. (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_state_dict_type", + default=None, + type=str, + help="FSDP's state dict type. (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_forward_prefetch", + default="false", + type=str, + help="If True, then FSDP explicitly prefetches the next upcoming " + "all-gather while executing in the forward pass (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_use_orig_params", + default="true", + type=str, + help="If True, allows non-uniform `requires_grad` during init, which means support for interspersed frozen and trainable parameters." + " (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_cpu_ram_efficient_loading", + default="true", + type=str, + help="If True, only the first process loads the pretrained model checkoint while all other processes have empty weights. " + "Only applicable for 🤗 Transformers. When using this, `--fsdp_sync_module_states` needs to True. " + "(useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_sync_module_states", + default="true", + type=str, + help="If True, each individually wrapped FSDP unit will broadcast module parameters from rank 0." + " (useful only when `use_fsdp` flag is passed).", + ) + fsdp_args.add_argument( + "--fsdp_activation_checkpointing", + default="false", + type=str, + help="Decides Whether (true|false) intermediate activations are freed during the forward pass, and a checkpoint is left as a placeholder. (useful only when `use_fsdp` flag is passed).", + ) + + # megatron_lm args + megatron_lm_args = parser.add_argument_group("Megatron-LM Arguments", "Arguments related to Megatron-LM.") + megatron_lm_args.add_argument( + "--megatron_lm_tp_degree", + type=int, + default=1, + help="Megatron-LM's Tensor Parallelism (TP) degree. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_use_custom_fsdp", + type=bool, + default=False, + help="Whether to use custom FSDP. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_no_load_optim", + type=bool, + default=False, + help="Whether to not load optimizer. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_eod_mask_loss", + type=bool, + default=False, + help="Whether to use eod mask loss. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_overlap_cpu_optimizer_d2h_h2d", + type=bool, + default=False, + help="Whether to overlap CPU optimizer step, gradients D2H and updated parameters H2D. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_no_save_optim", + type=bool, + default=False, + help="Whether to not save optimizer. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_optimizer_cpu_offload", + type=bool, + default=False, + help="Whether to use CPU offload for optimizer. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_use_precision_aware_optimizer", + type=bool, + default=False, + help="Whether to use precision aware optimizer. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_decoder_last_pipeline_num_layers", + type=int, + default=None, + help="Megatron-LM's decoder last pipeline number of layers, default None is even split of transformer layers across all pipeline stages.", + ) + megatron_lm_args.add_argument( + "--megatron_lm_pp_degree", + type=int, + default=1, + help="Megatron-LM's Pipeline Parallelism (PP) degree. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_num_micro_batches", + type=int, + default=None, + help="Megatron-LM's number of micro batches when PP degree > 1. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_sequence_parallelism", + default=None, + type=str, + help="Decides Whether (true|false) to enable Sequence Parallelism when TP degree > 1. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_recompute_activations", + default=None, + type=str, + help="Decides Whether (true|false) to enable Selective Activation Recomputation. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_use_distributed_optimizer", + default=None, + type=str, + help="Decides Whether (true|false) to use distributed optimizer " + "which shards optimizer state and gradients across Data Pralellel (DP) ranks. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_gradient_clipping", + default=1.0, + type=float, + help="Megatron-LM's gradient clipping value based on global L2 Norm (0 to disable). " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_recompute_granularity", + default=None, + type=str, + help="Megatron-LM's recompute granularity (full, selective). " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_recompute_method", + default=None, + type=str, + help="Megatron-LM's recompute method (uniform, block). (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_recompute_num_layers", + default=None, + type=int, + help="Megatron-LM's number of layers to recompute. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_attention_backend", + default=None, + type=str, + help="Decides Whether (true|false) to enable attention backend. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_expert_model_parallel_size", + default=None, + type=int, + help="Megatron-LM's expert model parallel size. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_context_parallel_size", + default=None, + type=int, + help="Megatron-LM's context parallel size. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_attention_dropout", + default=None, + type=float, + help="Megatron-LM's attention dropout rate. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_hidden_dropout", + default=None, + type=float, + help="Megatron-LM's hidden dropout rate. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_attention_softmax_in_fp32", + default=None, + type=str, + help="Decides Whether (true|false) to use fp32 for attention softmax. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_expert_tensor_parallel_size", + default=None, + type=int, + help="Megatron-LM's expert tensor parallel size. (useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_calculate_per_token_loss", + default=None, + type=str, + help="Decides Whether (true|false) to calculate per token loss. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + megatron_lm_args.add_argument( + "--megatron_lm_use_rotary_position_embeddings", + default=None, + type=str, + help="Decides Whether (true|false) to use rotary position embeddings. " + "(useful only when `use_megatron_lm` flag is passed).", + ) + + # FP8 arguments + fp8_args = parser.add_argument_group( + "FP8 Arguments", "Arguments related to FP8 training (requires `--mixed_precision=fp8`)" + ) + fp8_args.add_argument( + "--fp8_backend", + type=str, + choices=["ao", "te", "msamp"], + help="Choose a backend to train with FP8 (ao: torchao, te: TransformerEngine, msamp: MS-AMP)", + ) + fp8_args.add_argument( + "--fp8_use_autocast_during_eval", + default=False, + action="store_true", + help="Whether to use FP8 autocast during eval mode (useful only when `--fp8_backend=te` is passed). Generally better metrics are found when this is not passed.", + ) + fp8_args.add_argument( + "--fp8_margin", + type=int, + default=0, + help="The margin to use for the gradient scaling (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_interval", + type=int, + default=1, + help="The interval to use for how often the scaling factor is recomputed (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_format", + type=str, + default="HYBRID", + choices=["HYBRID", "E4M3", "E5M2"], + help="The format to use for the FP8 recipe (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_amax_history_len", + type=int, + default=1024, + help="The length of the history to use for the scaling factor computation (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_amax_compute_algo", + type=str, + default="most_recent", + choices=["max", "most_recent"], + help="The algorithm to use for the scaling factor computation. (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_override_linear_precision", + type=lambda x: tuple(map(str_to_bool, x.split(","))), + default=(False, False, False), + help="Whether or not to execute `fprop`, `dgrad`, and `wgrad` GEMMS in higher precision. Should be passed in a comma-separated string of booleans (useful only when `--fp8_backend=te` is passed).", + ) + fp8_args.add_argument( + "--fp8_opt_level", + type=str, + default="O2", + choices=["O1", "O2"], + help="What level of 8-bit collective communication should be used with MS-AMP (useful only when `--fp8_backend=msamp` is passed).", + ) + fp8_args.add_argument( + "--fp8_enable_fsdp_float8_all_gather", + default="true", + type=str_to_bool, + help="Whether to enable FSDP2 float8 all gather (useful only when `--fp8_backend=ao` is passed).", + ) + fp8_args.add_argument( + "--fp8_pad_inner_dim", + default="true", + type=str_to_bool, + help="Whether to pad the inner dimension for FP8 GEMMs (useful only when `--fp8_backend=ao` is passed).", + ) + + # AWS arguments + aws_args = parser.add_argument_group("AWS Arguments", "Arguments related to AWS.") + aws_args.add_argument( + "--aws_access_key_id", + type=str, + default=None, + help="The AWS_ACCESS_KEY_ID used to launch the Amazon SageMaker training job", + ) + aws_args.add_argument( + "--aws_secret_access_key", + type=str, + default=None, + help="The AWS_SECRET_ACCESS_KEY used to launch the Amazon SageMaker training job.", + ) + parser.add_argument( + "--debug", + action="store_true", + help="Whether to print out the torch.distributed stack trace when something fails.", + ) + parser.add_argument( + "training_script", + type=str, + help=( + "The full path to the script to be launched in parallel, followed by all the arguments for the training " + "script." + ), + ) + + # MPI arguments + mpirun_args = parser.add_argument_group("MPI Arguments", "Arguments related to mpirun for Multi-CPU") + mpirun_args.add_argument( + "--mpirun_hostfile", + type=str, + default=None, + help="Location for a hostfile for using Accelerate to launch a multi-CPU training job with mpirun. This will " + "get passed to the MPI --hostfile or -f parameter, depending on which MPI program is installed.", + ) + + # ParallelismConfig arguments + parallelism_config_args = parser.add_argument_group( + "ParallelismConfig Arguments", + "Arguments related to the ParallelismConfig used for distributed training.", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_dp_replicate_size", + type=int, + default=1, + help="The number of processes for data parallel training. Defaults to 1 (no data parallelism).", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_dp_shard_size", + type=int, + default=1, + help="The number of processes for FSDP sharding. Defaults to 1 (No FSDP sharding).", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_tp_size", + type=int, + default=1, + help="The number of processes for tensor parallel training. Defaults to 1 (no tensor parallelism).", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_cp_size", + type=int, + default=1, + help="The number of processese for context parallel training. Defaults to 1 (no context parallelism).", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_cp_backend", + type=str, + choices=["torch"], + default="torch", + help="Context Parallelism backend: torch (FSDP2) or deepspeed (ALST/Ulysses)", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_cp_comm_strategy", + type=str, + default="allgather", + help="The communication strategy for context parallel training. Defaults to 'allgather'. Other option is alltoall", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_sp_size", + type=int, + default=1, + help="The number of processese for context parallel training. Defaults to 1 (no context parallelism).", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_sp_backend", + type=str, + choices=["deepspeed"], + default="deepspeed", + help="Sequence Parallelism backend: deepspeed (ALST/Ulysses)", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_sp_seq_length", + type=str, + default=None, + help="Sequence length for when batches are all of the same length. For variable sequence lengths across batches set `parallelism_config_sp_seq_length_is_variable=True`", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_sp_seq_length_is_variable", + type=bool, + default=True, + help="If `True` will work with a sequence length that may change between batches, in which case `parallelism_config_sp_seq_length` value can be set to anything divisible by sp size or remain unset. If `False` then `parallelism_config_sp_seq_length` needs to match the batch's sequence length dimension. The default is `True`.", + ) + + parallelism_config_args.add_argument( + "--parallelism_config_sp_attn_implementation", + type=str, + default="sdpa", + help="Attention implementation to use. Can be one of 'flash_attention_2', 'flash_attention_3' or 'sdpa'. Defaults to `sdpa`.", + ) + + # Other arguments of the training scripts + parser.add_argument("training_script_args", nargs=argparse.REMAINDER, help="Arguments of the training script.") + + if subparsers is not None: + parser.set_defaults(func=launch_command) + return parser + + +def simple_launcher(args): + cmd, current_env = prepare_simple_launcher_cmd_env(args) + + process = subprocess.Popen(cmd, env=current_env) + process.wait() + if process.returncode != 0: + if not args.quiet: + raise subprocess.CalledProcessError(returncode=process.returncode, cmd=cmd) + else: + sys.exit(1) + + +def multi_gpu_launcher(args): + import torch.distributed.run as distrib_run + + current_env = prepare_multi_gpu_env(args) + if not check_cuda_p2p_ib_support(): + message = "Using RTX 4000 series which doesn't support faster communication speedups. Ensuring P2P and IB communications are disabled." + warn = False + if "NCCL_P2P_DISABLE" not in current_env: + current_env["NCCL_P2P_DISABLE"] = "1" + warn = True + if "NCCL_IB_DISABLE" not in current_env: + current_env["NCCL_IB_DISABLE"] = "1" + warn = True + if warn: + logger.warning(message) + + debug = getattr(args, "debug", False) + args = _filter_args( + args, + distrib_run.get_args_parser(), + ["--training_script", args.training_script, "--training_script_args", args.training_script_args], + ) + + with patch_environment(**current_env): + try: + distrib_run.run(args) + except Exception: + if is_rich_available() and debug: + console = get_console() + console.print("\n[bold red]Using --debug, `torch.distributed` Stack Trace:[/bold red]") + console.print_exception(suppress=[__file__], show_locals=False) + else: + raise + + +def deepspeed_launcher(args): + import torch.distributed.run as distrib_run + + if not is_deepspeed_available(): + raise ImportError("DeepSpeed is not installed => run `pip3 install deepspeed` or build it from source.") + else: + from deepspeed.launcher.runner import DEEPSPEED_ENVIRONMENT_NAME + + cmd, current_env = prepare_deepspeed_cmd_env(args) + if not check_cuda_p2p_ib_support(): + message = "Using RTX 4000 series which doesn't support faster communication speedups. Ensuring P2P and IB communications are disabled." + warn = False + if "NCCL_P2P_DISABLE" not in current_env: + current_env["NCCL_P2P_DISABLE"] = "1" + warn = True + if "NCCL_IB_DISABLE" not in current_env: + current_env["NCCL_IB_DISABLE"] = "1" + warn = True + if warn: + logger.warning(message) + + if args.num_machines > 1 and args.deepspeed_multinode_launcher != DEEPSPEED_MULTINODE_LAUNCHERS[1]: + with open(DEEPSPEED_ENVIRONMENT_NAME, "a") as f: + valid_env_items = convert_dict_to_env_variables(current_env) + if len(valid_env_items) > 1: + f.writelines(valid_env_items) + + process = subprocess.Popen(cmd, env=current_env) + process.wait() + if process.returncode != 0: + if not args.quiet: + raise subprocess.CalledProcessError(returncode=process.returncode, cmd=cmd) + else: + sys.exit(1) + else: + debug = getattr(args, "debug", False) + args = _filter_args( + args, + distrib_run.get_args_parser(), + ["--training_script", args.training_script, "--training_script_args", args.training_script_args], + ) + with patch_environment(**current_env): + try: + distrib_run.run(args) + except Exception: + if is_rich_available() and debug: + console = get_console() + console.print("\n[bold red]Using --debug, `torch.distributed` Stack Trace:[/bold red]") + console.print_exception(suppress=[__file__], show_locals=False) + else: + raise + + +def tpu_launcher(args): + import torch_xla.distributed.xla_multiprocessing as xmp + + if args.no_python: + raise ValueError("--no_python cannot be used with TPU launcher") + + args, current_env = prepare_tpu(args, {}) + + if args.module: + mod_name = args.training_script + else: + # Import training_script as a module + script_path = Path(args.training_script) + sys.path.append(str(script_path.parent.resolve())) + mod_name = script_path.stem + + mod = importlib.import_module(mod_name) + if not hasattr(mod, args.main_training_function): + raise ValueError( + f"Your training script should have a function named {args.main_training_function}, or you should pass a " + "different value to `--main_training_function`." + ) + + # Patch sys.argv + sys.argv = [mod.__file__] + args.training_script_args + + main_function = getattr(mod, args.main_training_function) + with patch_environment(**current_env): + xmp.spawn(PrepareForLaunch(main_function), args=()) + + +def tpu_pod_launcher(args): + from torch_xla.distributed import xla_dist + + current_env = {} + args, current_env = prepare_tpu(args, current_env, True) + debug = getattr(args, "debug", False) + + training_script = args.training_script + training_script_args = args.training_script_args + new_args = _filter_args( + args, xla_dist.get_args_parser(), ["--tpu", args.tpu_name, "--positional", "", "--restart-tpuvm-pod-server"] + ) + + if args.tpu_use_sudo: + new_cmd = ["sudo"] + else: + new_cmd = [] + + new_cmd += [ + "accelerate-launch", + "--tpu", + "--no_tpu_cluster", + "--num_machines", + "1", + "--mixed_precision", + "no", + "--dynamo_backend", + "no", + "--num_processes", + str(args.num_processes), + "--main_training_function", + str(args.main_training_function), + training_script, + ] + training_script_args + + new_args.positional = new_cmd + bad_flags = "" + for arg in vars(new_args): + if arg.startswith("docker_"): + value = getattr(new_args, arg) + if value != "" and value is not None: + bad_flags += f'{arg}="{value}"\n' + if bad_flags != "": + raise ValueError( + f"Docker containers are not supported for TPU pod launcher currently, please remove the following flags:\n{bad_flags}" + ) + new_args.env = [f"{k}={v}" for k, v in current_env.items()] + new_args.env.append("ACCELERATE_IN_TPU_POD=1") + try: + xla_dist.resolve_and_execute(new_args) + except Exception: + if is_rich_available() and debug: + console = get_console() + console.print("\n[bold red]Using --debug, `torch_xla.xla_dist` Stack Trace:[/bold red]") + console.print_exception(suppress=[__file__], show_locals=False) + else: + raise + + +def sagemaker_launcher(sagemaker_config: SageMakerConfig, args): + if not is_sagemaker_available(): + raise ImportError( + "Please install sagemaker to be able to launch training on Amazon SageMaker with `pip install accelerate[sagemaker]`" + ) + if args.module or args.no_python: + raise ValueError( + "SageMaker requires a python training script file and cannot be used with --module or --no_python" + ) + + from sagemaker.huggingface import HuggingFace + + args, sagemaker_inputs = prepare_sagemager_args_inputs(sagemaker_config, args) + + huggingface_estimator = HuggingFace(**args) + + huggingface_estimator.fit(inputs=sagemaker_inputs) + print(f"You can find your model data at: {huggingface_estimator.model_data}") + + +def _validate_launch_command(args): + # Sanity checks + if sum([args.multi_gpu, args.cpu, args.tpu, args.use_deepspeed, args.use_fsdp]) > 1: + raise ValueError( + "You can only use one of `--cpu`, `--multi_gpu`, `--tpu`, `--use_deepspeed`, `--use_fsdp` at a time." + ) + if args.multi_gpu and (args.num_processes is not None) and (args.num_processes < 2): + raise ValueError("You need to use at least 2 processes to use `--multi_gpu`.") + + if (not args.use_fsdp or args.fsdp_version == 1) and args.use_parallelism_config: + raise ValueError("You cannot use `--use_parallelism_config` without `--use_fsdp` and `--fsdp_version=2`. ") + + defaults = None + warned = [] + mp_from_config_flag = False + # Get the default from the config file. + if args.config_file is not None or os.path.isfile(default_config_file) and not args.cpu: + defaults = load_config_from_file(args.config_file) + if ( + not args.multi_gpu + and not args.tpu + and not args.tpu_use_cluster + and not args.use_deepspeed + and not args.use_fsdp + and not args.use_megatron_lm + ): + args.use_deepspeed = defaults.distributed_type == DistributedType.DEEPSPEED + args.multi_gpu = ( + True + if defaults.distributed_type + in ( + DistributedType.MULTI_GPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ) + else False + ) + args.tpu = defaults.distributed_type == DistributedType.XLA + args.use_fsdp = defaults.distributed_type == DistributedType.FSDP + args.use_megatron_lm = defaults.distributed_type == DistributedType.MEGATRON_LM + args.tpu_use_cluster = defaults.tpu_use_cluster if args.tpu else False + args.use_parallelism_config = defaults.parallelism_config != {} + if args.gpu_ids is None: + if defaults.gpu_ids is not None: + args.gpu_ids = defaults.gpu_ids + else: + args.gpu_ids = "all" + + if args.multi_gpu and args.num_machines is None: + args.num_machines = defaults.num_machines + + if len(args.gpu_ids.split(",")) < 2 and (args.gpu_ids != "all") and args.multi_gpu and args.num_machines <= 1: + raise ValueError( + "Less than two GPU ids were configured and tried to run on on multiple GPUs. " + "Please ensure at least two are specified for `--gpu_ids`, or use `--gpu_ids='all'`." + ) + if defaults.compute_environment == ComputeEnvironment.LOCAL_MACHINE: + # Update args with the defaults + for name, attr in defaults.__dict__.items(): + if isinstance(attr, dict): + # Copy defaults.somedict.somearg to args.somearg and + # defaults.fsdp_config.x to args.fsdp_x + for key, value in attr.items(): + if name == "fsdp_config" and not key.startswith("fsdp"): + key = "fsdp_" + key + elif name == "fp8_config" and not key.startswith("fp8"): + key = "fp8_" + key + if hasattr(args, "nondefault") and key not in args.nondefault: + setattr(args, key, value) + elif ( + name not in ["compute_environment", "mixed_precision", "distributed_type"] + and getattr(args, name, None) is None + ): + # Those args are handled separately + setattr(args, name, attr) + if not args.debug: + args.debug = defaults.debug + + if not args.mixed_precision: + if defaults.mixed_precision is None: + args.mixed_precision = "no" + else: + args.mixed_precision = defaults.mixed_precision + mp_from_config_flag = True + else: + native_amp = is_bf16_available(True) + if ( + args.mixed_precision == "bf16" + and not native_amp + and not (args.tpu and is_torch_xla_available(check_is_tpu=True)) + ): + raise ValueError("bf16 mixed precision requires PyTorch >= 1.10 and a supported device.") + + # Silently set the default here + if args.dynamo_backend is None: + args.dynamo_backend = "no" + if args.num_processes == -1: + raise ValueError("You need to manually pass in `--num_processes` using this config yaml.") + else: + if args.num_processes is None: + if is_xpu_available(): + args.num_processes = torch.xpu.device_count() + elif is_mlu_available(): + args.num_processes = torch.mlu.device_count() + elif is_sdaa_available(): + args.num_processes = torch.sdaa.device_count() + elif is_musa_available(): + args.num_processes = torch.musa.device_count() + elif is_npu_available(): + args.num_processes = torch.npu.device_count() + elif is_hpu_available(): + args.num_processes = torch.hpu.device_count() + elif is_neuron_available(): + args.num_processes = torch.neuron.device_count() + else: + args.num_processes = torch.cuda.device_count() + warned.append(f"\t`--num_processes` was set to a value of `{args.num_processes}`") + if args.debug is None: + args.debug = False + if ( + not args.multi_gpu + and args.num_processes > 1 + and ( + (is_xpu_available() and torch.xpu.device_count() > 1) + or (is_npu_available() and torch.npu.device_count() > 1) + or (is_hpu_available() and torch.hpu.device_count() > 1) + or (is_mlu_available() and torch.mlu.device_count() > 1) + or (is_sdaa_available() and torch.sdaa.device_count() > 1) + or (is_musa_available() and torch.musa.device_count() > 1) + or (is_neuron_available() and torch.neuron.device_count() > 1) + or (torch.cuda.is_available() and torch.cuda.device_count() > 1) + ) + ): + warned.append( + "\t\tMore than one GPU was found, enabling multi-GPU training.\n" + "\t\tIf this was unintended please pass in `--num_processes=1`." + ) + args.multi_gpu = True + if args.num_machines is None: + warned.append("\t`--num_machines` was set to a value of `1`") + args.num_machines = 1 + if args.mixed_precision is None: + warned.append("\t`--mixed_precision` was set to a value of `'no'`") + args.mixed_precision = "no" + if not hasattr(args, "use_cpu"): + args.use_cpu = args.cpu + if args.dynamo_backend is None: + warned.append("\t`--dynamo_backend` was set to a value of `'no'`") + args.dynamo_backend = "no" + if args.debug: + logger.debug("Running script in debug mode, expect distributed operations to be slightly slower.") + + is_aws_env_disabled = defaults is None or ( + defaults is not None and defaults.compute_environment != ComputeEnvironment.AMAZON_SAGEMAKER + ) + if is_aws_env_disabled and args.num_cpu_threads_per_process is None: + args.num_cpu_threads_per_process = get_int_from_env(["OMP_NUM_THREADS"], 1) + if args.use_cpu and args.num_processes >= 1 and get_int_from_env(["OMP_NUM_THREADS"], 0) == 0: + local_size = get_int_from_env( + ["MPI_LOCALNRANKS", "OMPI_COMM_WORLD_LOCAL_SIZE", "MV2_COMM_WORLD_LOCAL_SIZE"], + max(int(args.num_processes / args.num_machines), 1), + ) + import psutil + + threads_per_process = int(psutil.cpu_count(logical=False) / local_size) + if threads_per_process > 1: + args.num_cpu_threads_per_process = threads_per_process + warned.append( + f"\t`--num_cpu_threads_per_process` was set to `{args.num_cpu_threads_per_process}` to improve out-of-box performance when training on CPUs" + ) + + if any(warned): + message = "The following values were not passed to `accelerate launch` and had defaults used instead:\n" + message += "\n".join(warned) + message += ( + "\nTo avoid this warning pass in values for each of the problematic parameters or run `accelerate config`." + ) + logger.warning(message) + return args, defaults, mp_from_config_flag + + +def launch_command(args): + args, defaults, mp_from_config_flag = _validate_launch_command(args) + # Use the proper launcher + if args.use_deepspeed and not args.cpu: + args.deepspeed_fields_from_accelerate_config = list(defaults.deepspeed_config.keys()) if defaults else [] + if mp_from_config_flag: + args.deepspeed_fields_from_accelerate_config.append("mixed_precision") + args.deepspeed_fields_from_accelerate_config = ",".join(args.deepspeed_fields_from_accelerate_config) + deepspeed_launcher(args) + elif args.use_fsdp and not args.cpu: + multi_gpu_launcher(args) + elif args.use_megatron_lm and not args.cpu: + multi_gpu_launcher(args) + elif args.multi_gpu and not args.cpu: + multi_gpu_launcher(args) + elif args.tpu and not args.cpu: + if args.tpu_use_cluster: + tpu_pod_launcher(args) + else: + tpu_launcher(args) + elif defaults is not None and defaults.compute_environment == ComputeEnvironment.AMAZON_SAGEMAKER: + sagemaker_launcher(defaults, args) + else: + simple_launcher(args) + + +def main(): + parser = launch_command_parser() + args = parser.parse_args() + launch_command(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c2c851cc0b192ab8207d3fa68d7409868c84354c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .selection_menu import BulletMenu diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86b2190aa8a6720ad71f3b53f53468e79d41a166 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/cursor.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/cursor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..900411dff4f2f66f90ea05a7b409abadfd225799 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/cursor.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/helpers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/helpers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4526cb3caa19ee99e2f4dfa0448bda4e87b34ad6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/helpers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/input.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/input.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..137b23cc1a30ab4197555a4d8ce947fa8e584d3d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/input.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/keymap.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/keymap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a98f6806ff42fe22873e389f608aec07ecadd5b1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/keymap.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/selection_menu.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/selection_menu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..041809088152fdffe16dfb4daf223823aa5c9037 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/__pycache__/selection_menu.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/cursor.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/cursor.py new file mode 100644 index 0000000000000000000000000000000000000000..c1f0bb7b68025ae4fe0c2c76c095eb36b4e64f2c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/cursor.py @@ -0,0 +1,65 @@ +# Copyright 2022 The HuggingFace Team and Brian Chao. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A utility for showing and hiding the terminal cursor on Windows and Linux, based on https://github.com/bchao1/bullet +""" + +import os +import sys +from contextlib import contextmanager + + +# Windows only +if os.name == "nt": + import ctypes + import msvcrt # noqa + + class CursorInfo(ctypes.Structure): + # _fields is a specific attr expected by ctypes + _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] + + +def hide_cursor(): + if os.name == "nt": + ci = CursorInfo() + handle = ctypes.windll.kernel32.GetStdHandle(-11) + ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) + ci.visible = False + ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) + elif os.name == "posix": + sys.stdout.write("\033[?25l") + sys.stdout.flush() + + +def show_cursor(): + if os.name == "nt": + ci = CursorInfo() + handle = ctypes.windll.kernel32.GetStdHandle(-11) + ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) + ci.visible = True + ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) + elif os.name == "posix": + sys.stdout.write("\033[?25h") + sys.stdout.flush() + + +@contextmanager +def hide(): + "Context manager to hide the terminal cursor" + try: + hide_cursor() + yield + finally: + show_cursor() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/helpers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..de46f37ddcf4591167e3e01791391e4b1729034f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/helpers.py @@ -0,0 +1,59 @@ +# Copyright 2022 The HuggingFace Team and Brian Chao. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A variety of helper functions and constants when dealing with terminal menu choices, based on +https://github.com/bchao1/bullet +""" + +import enum +import shutil +import sys + + +TERMINAL_WIDTH, _ = shutil.get_terminal_size() + +CURSOR_TO_CHAR = {"UP": "A", "DOWN": "B", "RIGHT": "C", "LEFT": "D"} + + +class Direction(enum.Enum): + UP = 0 + DOWN = 1 + + +def forceWrite(content, end=""): + sys.stdout.write(str(content) + end) + sys.stdout.flush() + + +def writeColor(content, color, end=""): + forceWrite(f"\u001b[{color}m{content}\u001b[0m", end) + + +def reset_cursor(): + forceWrite("\r") + + +def move_cursor(num_lines: int, direction: str): + forceWrite(f"\033[{num_lines}{CURSOR_TO_CHAR[direction.upper()]}") + + +def clear_line(): + forceWrite(" " * TERMINAL_WIDTH) + reset_cursor() + + +def linebreak(): + reset_cursor() + forceWrite("-" * TERMINAL_WIDTH) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/input.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/input.py new file mode 100644 index 0000000000000000000000000000000000000000..f1270eaece9d4243e7282dcb31166feeeb9bdfc1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/input.py @@ -0,0 +1,84 @@ +# Copyright 2022 The HuggingFace Team and Brian Chao. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This file contains utilities for handling input from the user and registering specific keys to specific functions, +based on https://github.com/bchao1/bullet +""" + +from .keymap import KEYMAP, get_character + + +def mark(key: str): + """ + Mark the function with the key code so it can be handled in the register + """ + + def decorator(func): + handle = getattr(func, "handle_key", []) + handle += [key] + func.handle_key = handle + return func + + return decorator + + +def mark_multiple(*keys: list[str]): + """ + Mark the function with the key codes so it can be handled in the register + """ + + def decorator(func): + handle = getattr(func, "handle_key", []) + handle += keys + func.handle_key = handle + return func + + return decorator + + +class KeyHandler(type): + """ + Metaclass that adds the key handlers to the class + """ + + def __new__(cls, name, bases, attrs): + new_cls = super().__new__(cls, name, bases, attrs) + if not hasattr(new_cls, "key_handler"): + new_cls.key_handler = {} + new_cls.handle_input = KeyHandler.handle_input + + for value in attrs.values(): + handled_keys = getattr(value, "handle_key", []) + for key in handled_keys: + new_cls.key_handler[key] = value + return new_cls + + @staticmethod + def handle_input(cls): + "Finds and returns the selected character if it exists in the handler" + char = get_character() + if char != KEYMAP["undefined"]: + char = ord(char) + handler = cls.key_handler.get(char) + if handler: + cls.current_selection = char + return handler(cls) + else: + return None + + +def register(cls): + """Adds KeyHandler metaclass to the class""" + return KeyHandler(cls.__name__, cls.__bases__, cls.__dict__.copy()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/keymap.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/keymap.py new file mode 100644 index 0000000000000000000000000000000000000000..787db12860fe21c6786dda69c34fcccab114f2f8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/keymap.py @@ -0,0 +1,133 @@ +# Copyright 2022 The HuggingFace Team and Brian Chao. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Utilities relating to parsing raw characters from the keyboard, based on https://github.com/bchao1/bullet +""" + +import os +import string +import sys + + +ARROW_KEY_FLAG = 1 << 8 + +KEYMAP = { + "tab": ord("\t"), + "newline": ord("\r"), + "esc": 27, + "up": 65 + ARROW_KEY_FLAG, + "down": 66 + ARROW_KEY_FLAG, + "right": 67 + ARROW_KEY_FLAG, + "left": 68 + ARROW_KEY_FLAG, + "mod_int": 91, + "undefined": sys.maxsize, + "interrupt": 3, + "insert": 50, + "delete": 51, + "pg_up": 53, + "pg_down": 54, +} + +KEYMAP["arrow_begin"] = KEYMAP["up"] +KEYMAP["arrow_end"] = KEYMAP["left"] + +if sys.platform == "win32": + WIN_CH_BUFFER = [] + WIN_KEYMAP = { + b"\xe0H": KEYMAP["up"] - ARROW_KEY_FLAG, + b"\x00H": KEYMAP["up"] - ARROW_KEY_FLAG, + b"\xe0P": KEYMAP["down"] - ARROW_KEY_FLAG, + b"\x00P": KEYMAP["down"] - ARROW_KEY_FLAG, + b"\xe0M": KEYMAP["right"] - ARROW_KEY_FLAG, + b"\x00M": KEYMAP["right"] - ARROW_KEY_FLAG, + b"\xe0K": KEYMAP["left"] - ARROW_KEY_FLAG, + b"\x00K": KEYMAP["left"] - ARROW_KEY_FLAG, + } + +for i in range(10): + KEYMAP[str(i)] = ord(str(i)) + + +def get_raw_chars(): + "Gets raw characters from inputs" + if os.name == "nt": + import msvcrt + + encoding = "mbcs" + # Flush the keyboard buffer + while msvcrt.kbhit(): + msvcrt.getch() + if len(WIN_CH_BUFFER) == 0: + # Read the keystroke + ch = msvcrt.getch() + + # If it is a prefix char, get second part + if ch in (b"\x00", b"\xe0"): + ch2 = ch + msvcrt.getch() + # Translate actual Win chars to bullet char types + try: + chx = chr(WIN_KEYMAP[ch2]) + WIN_CH_BUFFER.append(chr(KEYMAP["mod_int"])) + WIN_CH_BUFFER.append(chx) + if ord(chx) in ( + KEYMAP["insert"] - 1 << 9, + KEYMAP["delete"] - 1 << 9, + KEYMAP["pg_up"] - 1 << 9, + KEYMAP["pg_down"] - 1 << 9, + ): + WIN_CH_BUFFER.append(chr(126)) + ch = chr(KEYMAP["esc"]) + except KeyError: + ch = ch2[1] + else: + ch = ch.decode(encoding) + else: + ch = WIN_CH_BUFFER.pop(0) + elif os.name == "posix": + import termios + import tty + + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +def get_character(): + "Gets a character from the keyboard and returns the key code" + char = get_raw_chars() + if ord(char) in [KEYMAP["interrupt"], KEYMAP["newline"]]: + return char + + elif ord(char) == KEYMAP["esc"]: + combo = get_raw_chars() + if ord(combo) == KEYMAP["mod_int"]: + key = get_raw_chars() + if ord(key) >= KEYMAP["arrow_begin"] - ARROW_KEY_FLAG and ord(key) <= KEYMAP["arrow_end"] - ARROW_KEY_FLAG: + return chr(ord(key) + ARROW_KEY_FLAG) + else: + return KEYMAP["undefined"] + else: + return get_raw_chars() + + else: + if char in string.printable: + return char + else: + return KEYMAP["undefined"] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/selection_menu.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/selection_menu.py new file mode 100644 index 0000000000000000000000000000000000000000..ca66b2ed6782f819ffc69bd0bb3be5b7139a3863 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/menu/selection_menu.py @@ -0,0 +1,145 @@ +# Copyright 2022 The HuggingFace Team and Brian Chao. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Main driver for the selection menu, based on https://github.com/bchao1/bullet +""" + +import builtins +import sys +from typing import Optional + +from ...utils.imports import _is_package_available +from . import cursor, input +from .helpers import Direction, clear_line, forceWrite, linebreak, move_cursor, reset_cursor, writeColor +from .keymap import KEYMAP + + +in_colab = False +try: + in_colab = _is_package_available("google.colab") +except ModuleNotFoundError: + pass + + +@input.register +class BulletMenu: + """ + A CLI menu to select a choice from a list of choices using the keyboard. + """ + + def __init__(self, prompt: Optional[str] = None, choices: list = []): + self.position = 0 + self.choices = choices + self.prompt = prompt + if sys.platform == "win32": + self.arrow_char = "*" + else: + self.arrow_char = "➔ " + + def write_choice(self, index, end: str = ""): + if sys.platform != "win32": + writeColor(self.choices[index], 32, end) + else: + forceWrite(self.choices[index], end) + + def print_choice(self, index: int): + "Prints the choice at the given index" + if index == self.position: + forceWrite(f" {self.arrow_char} ") + self.write_choice(index) + else: + forceWrite(f" {self.choices[index]}") + reset_cursor() + + def move_direction(self, direction: Direction, num_spaces: int = 1): + "Should not be directly called, used to move a direction of either up or down" + old_position = self.position + if direction == Direction.DOWN: + if self.position + 1 >= len(self.choices): + return + self.position += num_spaces + else: + if self.position - 1 < 0: + return + self.position -= num_spaces + clear_line() + self.print_choice(old_position) + move_cursor(num_spaces, direction.name) + self.print_choice(self.position) + + @input.mark(KEYMAP["up"]) + def move_up(self): + self.move_direction(Direction.UP) + + @input.mark(KEYMAP["down"]) + def move_down(self): + self.move_direction(Direction.DOWN) + + @input.mark(KEYMAP["newline"]) + def select(self): + move_cursor(len(self.choices) - self.position, "DOWN") + return self.position + + @input.mark(KEYMAP["interrupt"]) + def interrupt(self): + move_cursor(len(self.choices) - self.position, "DOWN") + raise KeyboardInterrupt + + @input.mark_multiple(*[KEYMAP[str(number)] for number in range(10)]) + def select_row(self): + index = int(chr(self.current_selection)) + movement = index - self.position + if index == self.position: + return + if index < len(self.choices): + if self.position > index: + self.move_direction(Direction.UP, -movement) + elif self.position < index: + self.move_direction(Direction.DOWN, movement) + else: + return + else: + return + + def run(self, default_choice: int = 0): + "Start the menu and return the selected choice" + if self.prompt: + linebreak() + forceWrite(self.prompt, "\n") + if in_colab: + forceWrite("Please input a choice index (starting from 0), and press enter", "\n") + else: + forceWrite("Please select a choice using the arrow or number keys, and selecting with enter", "\n") + self.position = default_choice + for i in range(len(self.choices)): + self.print_choice(i) + forceWrite("\n") + move_cursor(len(self.choices) - self.position, "UP") + with cursor.hide(): + while True: + if in_colab: + try: + choice = int(builtins.input()) + except ValueError: + choice = default_choice + else: + choice = self.handle_input() + if choice is not None: + reset_cursor() + for _ in range(len(self.choices) + 1): + move_cursor(1, "UP") + clear_line() + self.write_choice(choice, "\n") + return choice diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/merge.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/merge.py new file mode 100644 index 0000000000000000000000000000000000000000..475b53b5bbb71b959057126f8667d7f61eb9d0e1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/merge.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from accelerate.commands.utils import CustomArgumentParser +from accelerate.utils import merge_fsdp_weights + + +description = """Utility to merge the weights from multiple FSDP checkpoints into a single combined checkpoint. Should be used if +`SHARDED_STATE_DICT` was used for the model. Weights will be saved to `{output_path}`. + +This is a CPU-bound process and requires enough RAM to load the entire model state dict.""" + + +def merge_command(args): + merge_fsdp_weights( + args.checkpoint_directory, args.output_path, not args.unsafe_serialization, args.remove_checkpoint_dir + ) + + +def merge_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("merge-weights", description=description) + else: + parser = CustomArgumentParser(description=description) + + parser.add_argument("checkpoint_directory", type=str, help="A directory containing sharded weights saved by FSDP.") + parser.add_argument( + "output_path", + type=str, + help="The path to save the merged weights. Defaults to the current directory. ", + ) + parser.add_argument( + "--unsafe_serialization", + action="store_true", + default=False, + help="Whether to save the merged weights as `.bin` rather than `.safetensors` (not recommended).", + ) + parser.add_argument( + "--remove_checkpoint_dir", + action="store_true", + help="Whether to remove the checkpoint directory after merging.", + default=False, + ) + + if subparsers is not None: + parser.set_defaults(func=merge_command) + return parser + + +def main(): + parser = merge_command_parser() + args = parser.parse_args() + merge_command(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/test.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/test.py new file mode 100644 index 0000000000000000000000000000000000000000..a0d2f7bcf14727aa13e3438f4cd6e6f140f5bb2f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/test.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + +from accelerate.test_utils import execute_subprocess_async, path_in_accelerate_package + + +def test_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("test") + else: + parser = argparse.ArgumentParser("Accelerate test command") + + parser.add_argument( + "--config_file", + default=None, + help=( + "The path to use to store the config file. Will default to a file named default_config.yaml in the cache " + "location, which is the content of the environment `HF_HOME` suffixed with 'accelerate', or if you don't have " + "such an environment variable, your cache directory ('~/.cache' or the content of `XDG_CACHE_HOME`) suffixed " + "with 'huggingface'." + ), + ) + + if subparsers is not None: + parser.set_defaults(func=test_command) + return parser + + +def test_command(args): + script_name = path_in_accelerate_package("test_utils", "scripts", "test_script.py") + + if args.config_file is None: + test_args = [script_name] + else: + test_args = f"--config_file={args.config_file} {script_name}".split() + + cmd = ["accelerate-launch"] + test_args + result = execute_subprocess_async(cmd) + if result.returncode == 0: + print("Test is a success! You are ready for your distributed training!") + + +def main(): + parser = test_command_parser() + args = parser.parse_args() + test_command(args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/to_fsdp2.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/to_fsdp2.py new file mode 100644 index 0000000000000000000000000000000000000000..d2ce22f999caf60ad7bfd142289e18a56fe76280 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/to_fsdp2.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python + +# Copyright 2025 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum +import logging +from pathlib import Path + +import yaml + +from accelerate.commands.utils import CustomArgumentParser + + +class ConversionStatus(enum.Enum): + NOT_YET_IMPLEMENTED = 0 + REMOVED = -1 + + +ARGUMENT_KEY_MAPPING = { + # New keys in FSDP2 + "fsdp_version": "fsdp_version", + "fsdp_reshard_after_forward": "fsdp_reshard_after_forward", + # https://github.com/pytorch/torchtitan/blob/main/docs/fsdp.md + # https://huggingface.co/docs/accelerate/en/usage_guides/fsdp + "fsdp_auto_wrap_policy": "fsdp_auto_wrap_policy", + "fsdp_backward_prefetch": ConversionStatus.REMOVED, + "fsdp_forward_prefetch": ConversionStatus.NOT_YET_IMPLEMENTED, + "fsdp_cpu_ram_efficient_loading": "fsdp_cpu_ram_efficient_loading", + "fsdp_offload_params": "fsdp_offload_params", + "fsdp_sharding_strategy": "fsdp_reshard_after_forward", + "fsdp_state_dict_type": "fsdp_state_dict_type", + "fsdp_sync_module_states": ConversionStatus.REMOVED, + "fsdp_transformer_layer_cls_to_wrap": "fsdp_transformer_layer_cls_to_wrap", + "fsdp_min_num_params": "fsdp_min_num_params", + "fsdp_use_orig_params": ConversionStatus.REMOVED, + "fsdp_activation_checkpointing": "fsdp_activation_checkpointing", +} + +ARGUMENT_VALUE_MAPPING = { + "fsdp_sharding_strategy": { + "FULL_SHARD": True, + "SHARD_GRAD_OP": False, + "HYBRID_SHARD": True, + "HYBRID_SHARD_ZERO2": False, + "NO_SHARD": False, + }, + "fsdp_reshard_after_forward": { # Needed to convert newly created configs using FSDP1 to FSDP2 + "FULL_SHARD": True, + "SHARD_GRAD_OP": False, + "HYBRID_SHARD": True, + "HYBRID_SHARD_ZERO2": False, + "NO_SHARD": False, + }, +} + +logger = logging.getLogger(__name__) + + +def _validate_to_fsdp2_args(args): + if not Path(args.config_file).exists(): + raise FileNotFoundError(f"Config file {args.config_file} not found") + + if not args.overwrite and args.output_file is None: + raise ValueError("If --overwrite is not set, --output_file must be provided") + + if not args.overwrite and Path(args.output_file).exists(): + raise FileExistsError(f"Output file {args.output_file} already exists and --overwrite is not set") + + +def convert_config_to_fsdp2(config: dict) -> dict: + fsdp_config = config.get("fsdp_config", {}) + + if not fsdp_config: + logger.info("No FSDP config found in the config file, skipping conversion...") + return config + + new_fsdp_config = {} + + if fsdp_config.get("fsdp_version", 1) == 2: + logger.warning("Config already specifies FSDP2, skipping conversion...") + logger.warning( + "If the config doesn't use new argument names, change `fsdp_version` to `1` and rerun the command." + ) + return config + + for key, value in fsdp_config.items(): + conversion_status = ARGUMENT_KEY_MAPPING.get(key, None) + if isinstance(conversion_status, ConversionStatus) or conversion_status is None: + conversion_status = key + new_fsdp_config[conversion_status] = value + continue + + if conversion_status == ConversionStatus.REMOVED: + logger.warning(f"Argument {key} has been removed in FSDP2, skipping this key...") + continue + + if conversion_status == ConversionStatus.NOT_YET_IMPLEMENTED: + logger.warning(f"Argument {key} is not yet implemented in FSDP2, skipping this key...") + continue + + if conversion_status is None: + logger.warning(f"Argument {key} is not being converted, skipping this key...") + new_fsdp_config[key] = value + else: + if key in ARGUMENT_VALUE_MAPPING: + value = ARGUMENT_VALUE_MAPPING[key].get(value, value) + new_fsdp_config[ARGUMENT_KEY_MAPPING[key]] = value + + new_fsdp_config["fsdp_version"] = 2 + config["fsdp_config"] = new_fsdp_config + return config + + +def to_fsdp2_command_parser(subparsers=None): + description = "Convert an Accelerate config from FSDP1 to FSDP2" + + if subparsers is not None: + parser = subparsers.add_parser("to-fsdp2", description=description) + else: + parser = CustomArgumentParser(description=description) + + parser.add_argument("--config_file", type=str, help="The config file to convert to FSDP2", required=True) + parser.add_argument( + "--overwrite", + action="store_true", + help="Overwrite the config file if it exists", + default=False, + ) + parser.add_argument( + "--output_file", + type=str, + help="The path to the output file to write the converted config to. If not provided, the input file will be overwritten (if --overwrite is set)", + default=None, + ) + if subparsers is not None: + parser.set_defaults(func=to_fsdp2_command) + + return parser + + +def load_config(config_file: str) -> dict: + with open(config_file) as f: + config = yaml.safe_load(f) + if not config: + raise ValueError("Config file is empty") + + return config + + +def to_fsdp2_command(args): + _validate_to_fsdp2_args(args) + config = load_config(args.config_file) + + if args.overwrite and args.output_file is None: + args.output_file = args.config_file + + new_config = convert_config_to_fsdp2(config) + + with open(args.output_file, "w") as f: + yaml.dump(new_config, f) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/tpu.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/tpu.py new file mode 100644 index 0000000000000000000000000000000000000000..fc0f07bf8697bfdb6484d3bf817f2e18b1313b00 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/tpu.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import subprocess + +from packaging.version import Version, parse + +from accelerate.commands.config.config_args import default_config_file, load_config_from_file + + +_description = "Run commands across TPU VMs for initial setup before running `accelerate launch`." + + +def tpu_command_parser(subparsers=None): + if subparsers is not None: + parser = subparsers.add_parser("tpu-config", description=_description) + else: + parser = argparse.ArgumentParser("Accelerate tpu-config command", description=_description) + # Core arguments + config_args = parser.add_argument_group( + "Config Arguments", "Arguments that can be configured through `accelerate config`." + ) + config_args.add_argument( + "--config_file", + type=str, + default=None, + help="Path to the config file to use for accelerate.", + ) + config_args.add_argument( + "--tpu_name", + default=None, + help="The name of the TPU to use. If not specified, will use the TPU specified in the config file.", + ) + config_args.add_argument( + "--tpu_zone", + default=None, + help="The zone of the TPU to use. If not specified, will use the zone specified in the config file.", + ) + pod_args = parser.add_argument_group("TPU Arguments", "Arguments for options ran inside the TPU.") + pod_args.add_argument( + "--use_alpha", + action="store_true", + help="Whether to use `gcloud alpha` when running the TPU training script instead of `gcloud`.", + ) + pod_args.add_argument( + "--command_file", + default=None, + help="The path to the file containing the commands to run on the pod on startup.", + ) + pod_args.add_argument( + "--command", + action="append", + nargs="+", + help="A command to run on the pod. Can be passed multiple times.", + ) + pod_args.add_argument( + "--install_accelerate", + action="store_true", + help="Whether to install accelerate on the pod. Defaults to False.", + ) + pod_args.add_argument( + "--accelerate_version", + default="latest", + help="The version of accelerate to install on the pod. If not specified, will use the latest pypi version. Specify 'dev' to install from GitHub.", + ) + pod_args.add_argument( + "--debug", action="store_true", help="If set, will print the command that would be run instead of running it." + ) + + if subparsers is not None: + parser.set_defaults(func=tpu_command_launcher) + return parser + + +def tpu_command_launcher(args): + defaults = None + + # Get the default from the config file if it exists. + if args.config_file is not None or os.path.isfile(default_config_file): + defaults = load_config_from_file(args.config_file) + if not args.command_file and defaults.command_file is not None and not args.command: + args.command_file = defaults.command_file + if not args.command and defaults.commands is not None: + args.command = defaults.commands + if not args.tpu_name: + args.tpu_name = defaults.tpu_name + if not args.tpu_zone: + args.tpu_zone = defaults.tpu_zone + if args.accelerate_version == "dev": + args.accelerate_version = "git+https://github.com/huggingface/accelerate.git" + elif args.accelerate_version == "latest": + args.accelerate_version = "accelerate -U" + elif isinstance(parse(args.accelerate_version), Version): + args.accelerate_version = f"accelerate=={args.accelerate_version}" + + if not args.command_file and not args.command: + raise ValueError("You must specify either a command file or a command to run on the pod.") + + if args.command_file: + with open(args.command_file) as f: + args.command = [f.read().splitlines()] + + # To turn list of lists into list of strings + if isinstance(args.command[0], list): + args.command = [line for cmd in args.command for line in cmd] + # Default to the shared folder and install accelerate + new_cmd = ["cd /usr/share"] + if args.install_accelerate: + new_cmd += [f"pip install {args.accelerate_version}"] + new_cmd += args.command + args.command = "; ".join(new_cmd) + + # Then send it to gcloud + # Eventually try to use google-api-core to do this instead of subprocess + cmd = ["gcloud"] + if args.use_alpha: + cmd += ["alpha"] + cmd += [ + "compute", + "tpus", + "tpu-vm", + "ssh", + args.tpu_name, + "--zone", + args.tpu_zone, + "--command", + args.command, + "--worker", + "all", + ] + if args.debug: + print(f"Running {' '.join(cmd)}") + return + subprocess.run(cmd) + print("Successfully setup pod.") + + +def main(): + parser = tpu_command_parser() + args = parser.parse_args() + + tpu_command_launcher(args) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..326f37d7f93de2417e4171e5ffe91193fb97225c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/commands/utils.py @@ -0,0 +1,123 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse + + +class _StoreAction(argparse.Action): + """ + Custom action that allows for `-` or `_` to be passed in for an argument. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + new_option_strings = [] + for option_string in self.option_strings: + new_option_strings.append(option_string) + if "_" in option_string[2:]: + # Add `-` version to the option string + new_option_strings.append(option_string.replace("_", "-")) + self.option_strings = new_option_strings + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values) + if not hasattr(namespace, "nondefault"): + namespace.nondefault = set() + namespace.nondefault.add(self.dest) + + +class _StoreConstAction(_StoreAction): + """ + Same as `argparse._StoreConstAction` but uses the custom `_StoreAction`. + """ + + def __init__(self, option_strings, dest, const, default=None, required=False, help=None): + super().__init__( + option_strings=option_strings, + dest=dest, + nargs=0, + const=const, + default=default, + required=required, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=None): + super().__call__(parser, namespace, self.const, option_string) + + +class _StoreTrueAction(_StoreConstAction): + """ + Same as `argparse._StoreTrueAction` but uses the custom `_StoreConstAction`. + """ + + def __init__( + self, + option_strings, + dest, + default=None, + required=False, + help=None, + ): + super().__init__( + option_strings=option_strings, dest=dest, const=True, default=default, required=required, help=help + ) + + +class CustomArgumentGroup(argparse._ArgumentGroup): + """ + Custom argument group that allows for the use of `-` or `_` in arguments passed and overrides the help for each + when applicable. + """ + + def _add_action(self, action): + args = vars(action) + if isinstance(action, argparse._StoreTrueAction): + action = _StoreTrueAction( + args["option_strings"], args["dest"], args["default"], args["required"], args["help"] + ) + elif isinstance(action, argparse._StoreConstAction): + action = _StoreConstAction( + args["option_strings"], + args["dest"], + args["const"], + args["default"], + args["required"], + args["help"], + ) + elif isinstance(action, argparse._StoreAction): + action = _StoreAction(**args) + action = super()._add_action(action) + return action + + +class CustomArgumentParser(argparse.ArgumentParser): + """ + Custom argument parser that allows for the use of `-` or `_` in arguments passed and overrides the help for each + when applicable. + """ + + def add_argument(self, *args, **kwargs): + if "action" in kwargs: + # Translate action -> class + if kwargs["action"] == "store_true": + kwargs["action"] = _StoreTrueAction + else: + kwargs["action"] = _StoreAction + super().add_argument(*args, **kwargs) + + def add_argument_group(self, *args, **kwargs): + group = CustomArgumentGroup(self, *args, **kwargs) + self._action_groups.append(group) + return group diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..67031886219f56344b1173c792ac4d8d421746d3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__init__.py @@ -0,0 +1,66 @@ +# Copyright 2020 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .testing import ( + DEFAULT_LAUNCH_COMMAND, + are_the_same_tensors, + assert_exception, + capture_call_output, + device_count, + execute_subprocess_async, + get_launch_command, + get_torch_dist_unique_port, + memory_allocated_func, + path_in_accelerate_package, + pytest_xdist_worker_id, + require_bnb, + require_cpu, + require_cuda, + require_cuda_or_hpu, + require_cuda_or_xpu, + require_fp8, + require_fp16, + require_huggingface_suite, + require_mlu, + require_mps, + require_multi_device, + require_multi_gpu, + require_multi_gpu_or_xpu, + require_multi_xpu, + require_musa, + require_non_cpu, + require_non_hpu, + require_non_torch_xla, + require_non_xpu, + require_npu, + require_pippy, + require_sdaa, + require_single_device, + require_single_gpu, + require_single_xpu, + require_torch_min_version, + require_torchao, + require_torchvision, + require_tpu, + require_transformer_engine, + require_transformer_engine_mxfp8, + require_xpu, + run_first, + skip, + slow, + torch_device, +) +from .training import RegressionDataset, RegressionModel + + +from .scripts import test_script, test_sync, test_ops # isort: skip diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b34614914759b52c0ffa44d320d0e99d7396568e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/examples.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/examples.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cad805fc448310c6b2beb374740114be32f5b4e0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/examples.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/testing.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/testing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e850074a7a0df69b63dbadc701265cbb9951cc8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/testing.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/training.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/training.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c525c5aa6bae779819643bb998dd1e62be431677 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/__pycache__/training.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/examples.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/examples.py new file mode 100644 index 0000000000000000000000000000000000000000..18549cdb31492650e707b6008e25eeac8e33b04e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/examples.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +A collection of utilities for comparing `examples/complete_*_example.py` scripts with the capabilities inside of each +`examples/by_feature` example. `compare_against_test` is the main function that should be used when testing, while the +others are used to either get the code that matters, or to preprocess them (such as stripping comments) +""" + +import os +from typing import Optional + + +def get_function_contents_by_name(lines: list[str], name: str): + """ + Extracts a function from `lines` of segmented source code with the name `name`. + + Args: + lines (`List[str]`): + Source code of a script separated by line. + name (`str`): + The name of the function to extract. Should be either `training_function` or `main` + """ + if name != "training_function" and name != "main": + raise ValueError(f"Incorrect function name passed: {name}, choose either 'main' or 'training_function'") + good_lines, found_start = [], False + for line in lines: + if not found_start and f"def {name}" in line: + found_start = True + good_lines.append(line) + continue + if found_start: + if name == "training_function" and "def main" in line: + return good_lines + if name == "main" and "if __name__" in line: + return good_lines + good_lines.append(line) + + +def clean_lines(lines: list[str]): + """ + Filters `lines` and removes any entries that start with a comment ('#') or is just a newline ('\n') + + Args: + lines (`List[str]`): + Source code of a script separated by line. + """ + return [line for line in lines if not line.lstrip().startswith("#") and line != "\n"] + + +def compare_against_test( + base_filename: str, feature_filename: str, parser_only: bool, secondary_filename: Optional[str] = None +): + """ + Tests whether the additional code inside of `feature_filename` was implemented in `base_filename`. This should be + used when testing to see if `complete_*_.py` examples have all of the implementations from each of the + `examples/by_feature/*` scripts. + + It utilizes `nlp_example.py` to extract out all of the repeated training code, so that only the new additional code + is examined and checked. If something *other* than `nlp_example.py` should be used, such as `cv_example.py` for the + `complete_cv_example.py` script, it should be passed in for the `secondary_filename` parameter. + + Args: + base_filename (`str` or `os.PathLike`): + The filepath of a single "complete" example script to test, such as `examples/complete_cv_example.py` + feature_filename (`str` or `os.PathLike`): + The filepath of a single feature example script. The contents of this script are checked to see if they + exist in `base_filename` + parser_only (`bool`): + Whether to compare only the `main()` sections in both files, or to compare the contents of + `training_loop()` + secondary_filename (`str`, *optional*): + A potential secondary filepath that should be included in the check. This function extracts the base + functionalities off of "examples/nlp_example.py", so if `base_filename` is a script other than + `complete_nlp_example.py`, the template script should be included here. Such as `examples/cv_example.py` + """ + with open(base_filename) as f: + base_file_contents = f.readlines() + with open(os.path.abspath(os.path.join("examples", "nlp_example.py"))) as f: + full_file_contents = f.readlines() + with open(feature_filename) as f: + feature_file_contents = f.readlines() + if secondary_filename is not None: + with open(secondary_filename) as f: + secondary_file_contents = f.readlines() + + # This is our base, we remove all the code from here in our `full_filename` and `feature_filename` to find the new content + if parser_only: + base_file_func = clean_lines(get_function_contents_by_name(base_file_contents, "main")) + full_file_func = clean_lines(get_function_contents_by_name(full_file_contents, "main")) + feature_file_func = clean_lines(get_function_contents_by_name(feature_file_contents, "main")) + if secondary_filename is not None: + secondary_file_func = clean_lines(get_function_contents_by_name(secondary_file_contents, "main")) + else: + base_file_func = clean_lines(get_function_contents_by_name(base_file_contents, "training_function")) + full_file_func = clean_lines(get_function_contents_by_name(full_file_contents, "training_function")) + feature_file_func = clean_lines(get_function_contents_by_name(feature_file_contents, "training_function")) + if secondary_filename is not None: + secondary_file_func = clean_lines( + get_function_contents_by_name(secondary_file_contents, "training_function") + ) + + _dl_line = "train_dataloader, eval_dataloader = get_dataloaders(accelerator, batch_size)\n" + + # Specific code in our script that differs from the full version, aka what is new + new_feature_code = [] + passed_idxs = [] # We keep track of the idxs just in case it's a repeated statement + it = iter(feature_file_func) + for i in range(len(feature_file_func) - 1): + if i not in passed_idxs: + line = next(it) + if (line not in full_file_func) and (line.lstrip() != _dl_line): + if "TESTING_MOCKED_DATALOADERS" not in line: + new_feature_code.append(line) + passed_idxs.append(i) + else: + # Skip over the `config['num_epochs'] = 2` statement + _ = next(it) + + # Extract out just the new parts from the full_file_training_func + new_full_example_parts = [] + passed_idxs = [] # We keep track of the idxs just in case it's a repeated statement + for i, line in enumerate(base_file_func): + if i not in passed_idxs: + if (line not in full_file_func) and (line.lstrip() != _dl_line): + if "TESTING_MOCKED_DATALOADERS" not in line: + new_full_example_parts.append(line) + passed_idxs.append(i) + + # Finally, get the overall diff + diff_from_example = [line for line in new_feature_code if line not in new_full_example_parts] + if secondary_filename is not None: + diff_from_two = [line for line in full_file_contents if line not in secondary_file_func] + diff_from_example = [line for line in diff_from_example if line not in diff_from_two] + + return diff_from_example diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c9cbe26c257b515f657c05e1996d517e69613972 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df98aa023a0a98726099b437fb24ce952b4fccda Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_cli.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c49c417db4d824f87042b47743c82b23ade13509 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_cli.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ddp_comm_hook.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ddp_comm_hook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a3cebe5ab124a9d5c7525550536f661bd2b1425 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ddp_comm_hook.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_distributed_data_loop.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_distributed_data_loop.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a5305937af74efb10908e7afe58cb213b6b82ec Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_distributed_data_loop.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_merge_weights.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_merge_weights.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f96d1d209410272f8e476af523763ba07a5c419c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_merge_weights.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_notebook.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_notebook.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff339ee9a377112e7b32074e632aa635615d303a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_notebook.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ops.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ops.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6282813094148668f06cc7d800fe22e8acb4bbc7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_ops.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_script.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_script.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..316c66ada559a2e8d1faf04b689131420d65e2ea Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_script.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_sync.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_sync.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9932a96abfa41b774de0c9c468fc45c7b8056f8a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/__pycache__/test_sync.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c9cbe26c257b515f657c05e1996d517e69613972 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06b835a0d7cf30f0fcac9309c64170de2d234094 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_checkpointing.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_checkpointing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a09347c00ca0ef617903711b8447e0806d3028ce Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_checkpointing.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_alst_ulysses_sp.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_alst_ulysses_sp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27d41293f8d63185182d0b8a82161004ed052a03 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_alst_ulysses_sp.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_multiple_model.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_multiple_model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e67cde361bfc4b5dc8e08db17468575c2e49efd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_ds_multiple_model.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_metrics.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_metrics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..151a9c65612bc88908c2039aaf0c0e5efa6d3d46 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_metrics.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_peak_memory_usage.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_peak_memory_usage.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15a9a0b45e8e279f387102468cb11e82e75a9dd8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_peak_memory_usage.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_performance.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_performance.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6494a306f3736818a7891e5d6965d74ec7678978 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_performance.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_pippy.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_pippy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bc457d9e872a0cbcd662c9332bd7cfd99e7e996 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_pippy.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_zero3_integration.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_zero3_integration.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52baa2af7803aa0928e92511bf7ea17d182a9d3b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/__pycache__/test_zero3_integration.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_checkpointing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_checkpointing.py new file mode 100644 index 0000000000000000000000000000000000000000..6a1553898ec3d55e64822c204ddf7e705069ce8a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_checkpointing.py @@ -0,0 +1,269 @@ +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import json +import os + +import evaluate +import torch +from datasets import load_dataset +from torch.optim import AdamW +from torch.utils.data import DataLoader +from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup, set_seed + +from accelerate import Accelerator, DistributedType +from accelerate.utils.deepspeed import DummyOptim, DummyScheduler + + +MAX_GPU_BATCH_SIZE = 16 +EVAL_BATCH_SIZE = 32 + + +def get_dataloaders(accelerator: Accelerator, batch_size: int = 16, model_name: str = "bert-base-cased"): + """ + Creates a set of `DataLoader`s for the `glue` dataset. + + Args: + accelerator (`Accelerator`): + An `Accelerator` object + batch_size (`int`, *optional*): + The batch size for the train and validation DataLoaders. + model_name (`str`, *optional*): + """ + tokenizer = AutoTokenizer.from_pretrained(model_name) + datasets = load_dataset("glue", "mrpc") + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None) + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + tokenized_datasets = datasets.map( + tokenize_function, batched=True, remove_columns=["idx", "sentence1", "sentence2"], load_from_cache_file=False + ) + + # We also rename the 'label' column to 'labels' which is the expected name for labels by the models of the + # transformers library + tokenized_datasets = tokenized_datasets.rename_column("label", "labels") + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + if accelerator.distributed_type == DistributedType.XLA: + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + + # Instantiate dataloaders. + train_dataloader = DataLoader( + tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=batch_size + ) + eval_dataloader = DataLoader( + tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=EVAL_BATCH_SIZE + ) + + return train_dataloader, eval_dataloader + + +def evaluation_loop(accelerator, model, eval_dataloader, metric): + model.eval() + samples_seen = 0 + for step, batch in enumerate(eval_dataloader): + # We could avoid this line since we set the accelerator with `device_placement=True`. + batch.to(accelerator.device) + with torch.no_grad(): + outputs = model(**batch) + predictions = outputs.logits.argmax(dim=-1) + # It is slightly faster to call this once, than multiple times + predictions, references = accelerator.gather( + (predictions, batch["labels"]) + ) # If we are in a multiprocess environment, the last batch has duplicates + if accelerator.use_distributed: + if step == len(eval_dataloader) - 1: + predictions = predictions[: len(eval_dataloader.dataset) - samples_seen] + references = references[: len(eval_dataloader.dataset) - samples_seen] + else: + samples_seen += references.shape[0] + metric.add_batch( + predictions=predictions, + references=references, + ) + + eval_metric = metric.compute() + return eval_metric["accuracy"] + + +def training_function(config, args): + # Initialize accelerator + accelerator = Accelerator() + + # Sample hyper-parameters for learning rate, batch size, seed and a few other HPs + lr = config["lr"] + num_epochs = int(config["num_epochs"]) + seed = int(config["seed"]) + batch_size = int(config["batch_size"]) + model_name = args.model_name_or_path + + set_seed(seed) + train_dataloader, eval_dataloader = get_dataloaders(accelerator, batch_size, model_name) + + # Instantiate the model (we build the model here so that the seed also control new weights initialization) + model = AutoModelForSequenceClassification.from_pretrained(model_name, return_dict=True) + + # Instantiate optimizer + optimizer_cls = ( + AdamW + if accelerator.state.deepspeed_plugin is None + or "optimizer" not in accelerator.state.deepspeed_plugin.deepspeed_config + else DummyOptim + ) + optimizer = optimizer_cls(params=model.parameters(), lr=lr) + + if accelerator.state.deepspeed_plugin is not None: + gradient_accumulation_steps = accelerator.state.deepspeed_plugin.deepspeed_config[ + "gradient_accumulation_steps" + ] + else: + gradient_accumulation_steps = 1 + max_training_steps = (len(train_dataloader) * num_epochs) // gradient_accumulation_steps + + # Instantiate scheduler + if ( + accelerator.state.deepspeed_plugin is None + or "scheduler" not in accelerator.state.deepspeed_plugin.deepspeed_config + ): + lr_scheduler = get_linear_schedule_with_warmup( + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=max_training_steps, + ) + else: + lr_scheduler = DummyScheduler(optimizer, total_num_steps=max_training_steps, warmup_num_steps=0) + + # Prepare everything + # There is no specific order to remember, we just need to unpack the objects in the same order we gave them to the + # prepare method. + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler + ) + + # We need to keep track of how many total steps we have iterated over + overall_step = 0 + # We also need to keep track of the stating epoch so files are named properly + starting_epoch = 0 + metric = evaluate.load("glue", "mrpc") + ending_epoch = num_epochs + + if args.partial_train_epoch is not None: + ending_epoch = args.partial_train_epoch + + if args.resume_from_checkpoint: + accelerator.load_state(args.resume_from_checkpoint) + epoch_string = args.resume_from_checkpoint.split("epoch_")[1] + state_epoch_num = "" + for char in epoch_string: + if char.isdigit(): + state_epoch_num += char + else: + break + starting_epoch = int(state_epoch_num) + 1 + accuracy = evaluation_loop(accelerator, model, eval_dataloader, metric) + accelerator.print("resumed checkpoint performance:", accuracy) + accelerator.print("resumed checkpoint's scheduler's lr:", lr_scheduler.get_lr()[0]) + accelerator.print("resumed optimizers's lr:", optimizer.param_groups[0]["lr"]) + with open(os.path.join(args.output_dir, f"state_{starting_epoch - 1}.json")) as f: + resumed_state = json.load(f) + assert resumed_state["accuracy"] == accuracy, "Accuracy mismatch, loading from checkpoint failed" + assert resumed_state["lr"] == lr_scheduler.get_lr()[0], ( + "Scheduler learning rate mismatch, loading from checkpoint failed" + ) + assert resumed_state["optimizer_lr"] == optimizer.param_groups[0]["lr"], ( + "Optimizer learning rate mismatch, loading from checkpoint failed" + ) + assert resumed_state["epoch"] == starting_epoch - 1, "Epoch mismatch, loading from checkpoint failed" + return + + # Now we train the model + state = {} + for epoch in range(starting_epoch, ending_epoch): + model.train() + for step, batch in enumerate(train_dataloader): + outputs = model(**batch) + loss = outputs.loss + loss = loss / gradient_accumulation_steps + accelerator.backward(loss) + if step % gradient_accumulation_steps == 0: + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + overall_step += 1 + output_dir = f"epoch_{epoch}" + output_dir = os.path.join(args.output_dir, output_dir) + accelerator.save_state(output_dir) + accuracy = evaluation_loop(accelerator, model, eval_dataloader, metric) + state["accuracy"] = accuracy + state["lr"] = lr_scheduler.get_lr()[0] + state["optimizer_lr"] = optimizer.param_groups[0]["lr"] + state["epoch"] = epoch + state["step"] = overall_step + accelerator.print(f"epoch {epoch}:", state) + + accelerator.wait_for_everyone() + if accelerator.is_main_process: + with open(os.path.join(args.output_dir, f"state_{epoch}.json"), "w") as f: + json.dump(state, f) + accelerator.end_training() + + +def main(): + parser = argparse.ArgumentParser(description="Simple example of training script tracking peak GPU memory usage.") + parser.add_argument( + "--model_name_or_path", + type=str, + default="bert-base-cased", + help="Path to pretrained model or model identifier from huggingface.co/models.", + required=False, + ) + parser.add_argument( + "--output_dir", + type=str, + default=".", + help="Optional save directory where all checkpoint folders will be stored. Default is the current working directory.", + ) + parser.add_argument( + "--resume_from_checkpoint", + type=str, + default=None, + help="If the training should continue from a checkpoint folder.", + ) + parser.add_argument( + "--partial_train_epoch", + type=int, + default=None, + help="If passed, the training will stop after this number of epochs.", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=2, + help="Number of train epochs.", + ) + args = parser.parse_args() + config = {"lr": 2e-5, "num_epochs": args.num_epochs, "seed": 42, "batch_size": 16} + + training_function(config, args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_alst_ulysses_sp.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_alst_ulysses_sp.py new file mode 100644 index 0000000000000000000000000000000000000000..c756d5dc77f9ce51e305001697fad5c342575887 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_alst_ulysses_sp.py @@ -0,0 +1,131 @@ +# Copyright 2024 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test script for verifying ALST/Ulysses SP works +""" + +import torch +from deepspeed.runtime.utils import move_to_device +from transformers import AutoModelForCausalLM, AutoTokenizer + +from accelerate import Accelerator +from accelerate.utils import ParallelismConfig, set_seed +from accelerate.utils.dataclasses import DeepSpeedSequenceParallelConfig + + +set_seed(42) + +world_size = 2 +model_name = "hf-internal-testing/tiny-random-LlamaForCausalLM" + +micro_batch_size = 1 + +parallelism_config = ParallelismConfig( + sp_backend="deepspeed", + sp_size=world_size, + # dp_shard_size=1, # set if dp is wanted as well + sp_handler=DeepSpeedSequenceParallelConfig( + sp_seq_length=256, + sp_seq_length_is_variable=True, + sp_attn_implementation="sdpa", + ), +) + +accelerator = Accelerator( + parallelism_config=parallelism_config, +) + +tokenizer = AutoTokenizer.from_pretrained(model_name) +model = AutoModelForCausalLM.from_pretrained(model_name) + +samples = 4 +seqlen = 32 +input_ids = torch.arange(1, seqlen * samples + 1).view(-1, seqlen) + 100 +position_ids = torch.arange(seqlen * samples).view(-1, seqlen) + +ds = torch.utils.data.TensorDataset(input_ids, position_ids) + + +def collate_fn(batch): + input_ids, position_ids = batch[0] + return dict( + input_ids=input_ids.unsqueeze(0), + position_ids=position_ids.unsqueeze(0), + labels=input_ids.unsqueeze(0), + ) + + +dl = torch.utils.data.DataLoader(ds, batch_size=micro_batch_size, collate_fn=collate_fn) + +optimizer = torch.optim.Adam(model.parameters(), lr=1e-5) + +rank = torch.distributed.get_rank() + +if rank == 0: + print(f"DL orig: {len(dl)} samples") + +model, optimizer, dl = accelerator.prepare(model, optimizer, dl) + +if rank == 0: + print(f"DL w/ adapter: {len(dl)} samples") + +sp_size = parallelism_config.sp_size if parallelism_config else 1 +if sp_size > 1: + from deepspeed.utils import groups + + sp_group = groups._get_sequence_parallel_group() + sp_world_size = parallelism_config.sp_size + +unwrapped_model = accelerator.unwrap_model(model) + +# Normal training loop +for iter, batch in enumerate(dl): + optimizer.zero_grad() + + if rank == 0: + print(f"batch {iter}: seqlen: {len(batch['input_ids'][0])}") + batch = move_to_device(batch, model.device) + outputs = model(**batch) + + shift_labels = batch["shift_labels"] + loss = unwrapped_model.loss_function( + logits=outputs.logits, + labels=None, + shift_labels=shift_labels, + vocab_size=unwrapped_model.config.vocab_size, + ) + + if sp_size > 1: + # differentiable weighted per-shard-loss aggregation across ranks + losses_per_rank = torch.distributed.nn.functional.all_gather(loss, group=sp_group) + # special dealing with SFT that has prompt tokens that aren't used in loss computation + good_tokens = (shift_labels != -100).view(-1).sum() + good_tokens_per_rank = torch.distributed.nn.functional.all_gather(good_tokens, group=sp_group) + total_loss = sum( + losses_per_rank[rank] * good_tokens_per_rank[rank] + for rank in range(sp_world_size) + if good_tokens_per_rank[rank] > 0 + ) + total_good_tokens = sum(good_tokens_per_rank) + loss = total_loss / max(total_good_tokens, 1) + + if rank == 0: + accelerator.print(f"{iter}: {loss=}") + accelerator.log(dict(train_loss=loss, step=iter)) + + accelerator.backward(loss) + optimizer.step() + +accelerator.end_training() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_multiple_model.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_multiple_model.py new file mode 100644 index 0000000000000000000000000000000000000000..a9ff3f3d2d0214f2b36f2b51e1d029b6ddf7cb7c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_ds_multiple_model.py @@ -0,0 +1,331 @@ +# Copyright 2024 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test script for verifying multiple models can be utilized with Accelerate + DeepSpeed: + +Scenario 1: One model is training, another model is being used for inference/logits to impact training in some form. +Scenario 2: Two models are training simultaneously, which means two optimizers, etc. +""" + +import argparse +from pathlib import Path + +import evaluate +import torch +from datasets import load_dataset +from torch.optim import AdamW +from torch.utils.data import DataLoader +from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup + +from accelerate import Accelerator, DeepSpeedPlugin, DistributedType +from accelerate.state import AcceleratorState +from accelerate.utils.deepspeed import get_active_deepspeed_plugin + + +EVAL_BATCH_SIZE = 16 + + +class NoiseModel(torch.nn.Module): + def __init__(self, noise_factor=0.1): + super().__init__() + self.noise_factor = torch.nn.Parameter(torch.tensor(noise_factor, dtype=torch.float32)) + + def forward(self, loss): + return loss * self.noise_factor + + +def get_dataloaders(accelerator: Accelerator, batch_size: int = 16, model_name: str = "bert-base-cased"): + """ + Creates a set of `DataLoader`s for the `glue` dataset. + + Args: + accelerator (`Accelerator`): + An `Accelerator` object + batch_size (`int`, *optional*): + The batch size for the train and validation DataLoaders. + model_name (`str`, *optional*): + """ + tokenizer = AutoTokenizer.from_pretrained(model_name) + datasets = load_dataset("glue", "mrpc") + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None) + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + tokenized_datasets = datasets.map( + tokenize_function, batched=True, remove_columns=["idx", "sentence1", "sentence2"], load_from_cache_file=False + ) + + # We also rename the 'label' column to 'labels' which is the expected name for labels by the models of the + # transformers library + tokenized_datasets = tokenized_datasets.rename_column("label", "labels") + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + if accelerator.distributed_type == DistributedType.XLA: + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + + # Instantiate dataloaders. + train_dataloader = DataLoader( + tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=batch_size + ) + eval_dataloader = DataLoader( + tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=EVAL_BATCH_SIZE + ) + + return train_dataloader, eval_dataloader + + +test_file_path = __file__ +path = Path(test_file_path).resolve() +test_file_dir_str = str(path.parent.parent.parent.parent.parent.parent) + +# Create our DS plugins +# We use custom schedulers and optimizers, hence `model_only` +ds_config_file = dict( + zero2=f"{test_file_dir_str}/tests/deepspeed/ds_config_zero2_model_only.json", + zero3=f"{test_file_dir_str}/tests/deepspeed/ds_config_zero3_model_only.json", +) + + +def single_model_training(config, args): + # Training a single model, we have a `noise` model that is untrainable used to inject some noise into the training process + num_epochs = config["num_epochs"] + zero2_plugin = DeepSpeedPlugin(hf_ds_config=ds_config_file["zero2"]) + zero3_plugin = DeepSpeedPlugin(hf_ds_config=ds_config_file["zero3"]) + + deepspeed_plugins = {"training": zero2_plugin, "inference": zero3_plugin} + + # Initialize accelerator + accelerator = Accelerator( + deepspeed_plugins=deepspeed_plugins, + mixed_precision="bf16", + ) + + # Initialize model under zero2 plugin + assert get_active_deepspeed_plugin(accelerator.state) is zero2_plugin + train_model = AutoModelForSequenceClassification.from_pretrained(args.model_name_or_path) + train_dataloader, eval_dataloader = get_dataloaders( + accelerator, batch_size=config["batch_size"], model_name=args.model_name_or_path + ) + max_training_steps = len(train_dataloader) * config["num_epochs"] + optimizer = AdamW(train_model.parameters(), lr=config["lr"]) + lr_scheduler = get_linear_schedule_with_warmup( + optimizer, num_warmup_steps=0, num_training_steps=max_training_steps + ) + + train_dataloader, eval_dataloader, train_model, optimizer, lr_scheduler = accelerator.prepare( + train_dataloader, eval_dataloader, train_model, optimizer, lr_scheduler + ) + + # Now prepare the model under zero3 plugin + accelerator.state.select_deepspeed_plugin("inference") + assert get_active_deepspeed_plugin(accelerator.state) is zero3_plugin + inference_model = NoiseModel() + inference_model = accelerator.prepare(inference_model) + inference_model.eval() + + # Run training loop + accelerator.state.select_deepspeed_plugin("training") + # We also need to keep track of the stating epoch so files are named properly + starting_epoch = 0 + + # Now we train the model + best_performance = 0 + metric = evaluate.load("glue", "mrpc") + performance_metric = {} + for epoch in range(starting_epoch, num_epochs): + train_model.train() + inference_model.train() + for step, batch in enumerate(train_dataloader): + with accelerator.accumulate(train_model): + outputs_1 = train_model(**batch) + with torch.no_grad(): + outputs_2 = inference_model(outputs_1.loss) + # Combine the losses + loss = outputs_1.loss + outputs_2 + accelerator.backward(loss) + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + train_model.eval() + for step, batch in enumerate(eval_dataloader): + with torch.no_grad(): + outputs = train_model(**batch) + predictions = outputs.logits.argmax(dim=-1) + # It is slightly faster to call this once, than multiple times + predictions, references = accelerator.gather_for_metrics((predictions, batch["labels"])) + metric.add_batch( + predictions=predictions, + references=references, + ) + + eval_metric = metric.compute() + # Use accelerator.print to print only on the main process. + accelerator.print(f"epoch {epoch}:", eval_metric) + performance_metric[f"epoch-{epoch}"] = eval_metric["accuracy"] + + if best_performance < eval_metric["accuracy"]: + best_performance = eval_metric["accuracy"] + assert best_performance > performance_metric["epoch-0"] + + +def multiple_model_training(config, args): + # This will essentially be like a k-fold model, but one model is Zero-2 and another model is Zero-3 + num_epochs = config["num_epochs"] + zero2_plugin = DeepSpeedPlugin(hf_ds_config=ds_config_file["zero2"]) + zero3_plugin = DeepSpeedPlugin(hf_ds_config=ds_config_file["zero3"]) + + deepspeed_plugins = {"zero2": zero2_plugin, "zero3": zero3_plugin} + + # Initialize accelerator + zero2_accelerator = Accelerator( + deepspeed_plugins=deepspeed_plugins, + mixed_precision="bf16", + ) + + # Since an `AcceleratorState` has already been made, we can just reuse it here + zero3_accelerator = Accelerator() + + # Initialize model under zero2 plugin + assert get_active_deepspeed_plugin(zero2_accelerator.state) is zero2_plugin + zero2_model = AutoModelForSequenceClassification.from_pretrained(args.model_name_or_path) + train_dataloader, eval_dataloader = get_dataloaders( + zero2_accelerator, batch_size=config["batch_size"], model_name=args.model_name_or_path + ) + max_training_steps = len(train_dataloader) * config["num_epochs"] + zero2_optimizer = AdamW(zero2_model.parameters(), lr=config["lr"]) + zero2_lr_scheduler = get_linear_schedule_with_warmup( + zero2_optimizer, num_warmup_steps=0, num_training_steps=max_training_steps + ) + + train_dataloader, eval_dataloader, zero2_model, zero2_optimizer, zero2_lr_scheduler = zero2_accelerator.prepare( + train_dataloader, eval_dataloader, zero2_model, zero2_optimizer, zero2_lr_scheduler + ) + assert zero2_accelerator.deepspeed_engine_wrapped.engine is zero2_model + + # now do Zero3 + zero3_accelerator.state.select_deepspeed_plugin("zero3") + zero3_plugin.deepspeed_config["train_micro_batch_size_per_gpu"] = zero2_plugin.deepspeed_config[ + "train_micro_batch_size_per_gpu" + ] + assert get_active_deepspeed_plugin(zero3_accelerator.state) is zero3_plugin + zero3_model = AutoModelForSequenceClassification.from_pretrained(args.model_name_or_path) + zero3_optimizer = AdamW(zero3_model.parameters(), lr=config["lr"]) + zero3_lr_scheduler = get_linear_schedule_with_warmup( + zero3_optimizer, num_warmup_steps=0, num_training_steps=max_training_steps + ) + zero3_model, zero3_optimizer, zero3_lr_scheduler = zero3_accelerator.prepare( + zero3_model, zero3_optimizer, zero3_lr_scheduler + ) + assert zero3_accelerator.deepspeed_engine_wrapped.engine is zero3_model + + # Run training loop + starting_epoch = 0 + + # Now we train the model + best_performance_a = 0 + best_performance_b = 0 + metric_a = evaluate.load("glue", "mrpc") + metric_b = evaluate.load("glue", "mrpc") + performance_metric_a = {} + performance_metric_b = {} + for epoch in range(starting_epoch, num_epochs): + zero2_model.train() + zero3_model.train() + for step, batch in enumerate(train_dataloader): + with zero2_accelerator.accumulate(zero2_model, zero3_model): + outputs_1 = zero2_model(**batch) + zero2_accelerator.backward(outputs_1.loss) + zero2_optimizer.step() + zero2_lr_scheduler.step() + zero2_optimizer.zero_grad() + outputs_2 = zero3_model(**batch) + zero3_accelerator.backward(outputs_2.loss) + zero3_optimizer.step() + zero3_lr_scheduler.step() + zero3_optimizer.zero_grad() + + zero2_model.eval() + zero3_model.eval() + for step, batch in enumerate(eval_dataloader): + with torch.no_grad(): + logits_a = zero2_model(**batch).logits + logits_b = zero3_model(**batch).logits + # Combine the logits from both models + predictions_a = logits_a.argmax(dim=-1) + predictions_b = logits_b.argmax(dim=-1) + # It is slightly faster to call this once, than multiple times + predictions_a, predictions_b, references = zero2_accelerator.gather_for_metrics( + (predictions_a, predictions_b, batch["labels"]) + ) + metric_a.add_batch( + predictions=predictions_a, + references=references, + ) + metric_b.add_batch( + predictions=predictions_b, + references=references, + ) + + eval_metric_a = metric_a.compute() + eval_metric_b = metric_b.compute() + # Use accelerator.print to print only on the main process. + zero2_accelerator.print(f"epoch {epoch}:", eval_metric_a, eval_metric_b) + performance_metric_a[f"epoch-{epoch}"] = eval_metric_a["accuracy"] + performance_metric_b[f"epoch-{epoch}"] = eval_metric_b["accuracy"] + + if best_performance_a < eval_metric_a["accuracy"]: + best_performance_a = eval_metric_a["accuracy"] + if best_performance_b < eval_metric_b["accuracy"]: + best_performance_b = eval_metric_b["accuracy"] + assert best_performance_a > performance_metric_a["epoch-0"] + assert best_performance_b > performance_metric_b["epoch-0"] + + +def main(): + parser = argparse.ArgumentParser(description="Simple example of training script tracking peak GPU memory usage.") + parser.add_argument( + "--model_name_or_path", + type=str, + default="bert-base-cased", + help="Path to pretrained model or model identifier from huggingface.co/models.", + required=False, + ) + parser.add_argument( + "--performance_lower_bound", + type=float, + default=None, + help="Optional lower bound for the performance metric. If set, the training will throw error when the performance metric drops below this value.", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=3, + help="Number of train epochs.", + ) + args = parser.parse_args() + config = {"lr": 2e-5, "num_epochs": args.num_epochs, "seed": 42, "batch_size": 8} + single_model_training(config, args) + AcceleratorState._reset_state(True) + multiple_model_training(config, args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_metrics.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..d1bfe351509148ebc48067584e9d61b93e7210a6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_metrics.py @@ -0,0 +1,307 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import math +import os +from copy import deepcopy + +import datasets +import evaluate +import torch +import transformers +from datasets import load_dataset +from torch.utils.data import DataLoader, IterableDataset +from transformers import AutoModelForSequenceClassification, AutoTokenizer + +from accelerate import Accelerator, DataLoaderConfiguration, DistributedType +from accelerate.data_loader import DataLoaderDispatcher +from accelerate.test_utils import RegressionDataset, RegressionModel, torch_device +from accelerate.utils import is_torch_xla_available, set_seed + + +os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "true" + + +class ListHandler(logging.Handler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logs = [] + + def emit(self, record): + self.logs.append(record) + + +def get_basic_setup(accelerator, num_samples=82, batch_size=16): + "Returns everything needed to perform basic training" + set_seed(42) + model = RegressionModel() + ddp_model = deepcopy(model) + dset = RegressionDataset(length=num_samples) + dataloader = DataLoader(dset, batch_size=batch_size) + model.to(accelerator.device) + ddp_model, dataloader = accelerator.prepare(ddp_model, dataloader) + return model, ddp_model, dataloader + + +def get_dataloader(accelerator: Accelerator, use_longest=False): + tokenizer = AutoTokenizer.from_pretrained("hf-internal-testing/mrpc-bert-base-cased") + dataset = load_dataset("glue", "mrpc", split="validation") + + def tokenize_function(examples): + outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None) + return outputs + + with accelerator.main_process_first(): + tokenized_datasets = dataset.map( + tokenize_function, + batched=True, + remove_columns=["idx", "sentence1", "sentence2"], + ) + + tokenized_datasets = tokenized_datasets.rename_column("label", "labels") + + def collate_fn(examples): + if use_longest: + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + + return DataLoader(tokenized_datasets, shuffle=False, collate_fn=collate_fn, batch_size=16) + + +def get_mrpc_setup(dispatch_batches, split_batches): + dataloader_config = DataLoaderConfiguration(dispatch_batches=dispatch_batches, split_batches=split_batches) + accelerator = Accelerator(dataloader_config=dataloader_config) + dataloader = get_dataloader(accelerator, not dispatch_batches) + model = AutoModelForSequenceClassification.from_pretrained( + "hf-internal-testing/mrpc-bert-base-cased", return_dict=True + ) + ddp_model, ddp_dataloader = accelerator.prepare(model, dataloader) + return { + "ddp": [ddp_model, ddp_dataloader, torch_device], + "no": [model, dataloader, accelerator.device], + }, accelerator + + +def generate_predictions(model, dataloader, accelerator): + logits_and_targets = [] + for batch in dataloader: + input, target = batch.values() + with torch.no_grad(): + logit = model(input) + logit, target = accelerator.gather_for_metrics((logit, target)) + logits_and_targets.append((logit, target)) + logits, targs = [], [] + for logit, targ in logits_and_targets: + logits.append(logit) + targs.append(targ) + logits, targs = torch.cat(logits), torch.cat(targs) + return logits, targs + + +def test_torch_metrics( + accelerator: Accelerator, num_samples=82, dispatch_batches=False, split_batches=False, batch_size=16 +): + _, ddp_model, dataloader = get_basic_setup(accelerator, num_samples, batch_size) + logits, _ = generate_predictions(ddp_model, dataloader, accelerator) + assert len(logits) == num_samples, ( + f"Unexpected number of inputs:\n Expected: {num_samples}\n Actual: {len(logits)}" + ) + + +def test_mrpc(dispatch_batches: bool = False, split_batches: bool = False): + metric = evaluate.load("glue", "mrpc") + setup, accelerator = get_mrpc_setup(dispatch_batches, split_batches) + # First do baseline + model, dataloader, device = setup["no"] + model.to(device) + model.eval() + for batch in dataloader: + batch.to(device) + with torch.inference_mode(): + outputs = model(**batch) + preds = outputs.logits.argmax(dim=-1) + metric.add_batch(predictions=preds, references=batch["labels"]) + baseline = metric.compute() + + # Then do distributed + model, dataloader, device = setup["ddp"] + model.eval() + for batch in dataloader: + with torch.inference_mode(): + outputs = model(**batch) + preds = outputs.logits.argmax(dim=-1) + references = batch["labels"] + preds, references = accelerator.gather_for_metrics((preds, references)) + metric.add_batch(predictions=preds, references=references) + distributed = metric.compute() + + for key in "accuracy f1".split(): + assert math.isclose(baseline[key], distributed[key]), ( + f"Baseline and Distributed are not the same for key {key}:\n\tBaseline: {baseline[key]}\n\tDistributed: {distributed[key]}\n" + ) + + +def test_gather_for_metrics_with_non_tensor_objects_iterable_dataset(): + class DummyIterableDataset(IterableDataset): + def __init__(self, data): + self.data = data + + def __len__(self): + return len(self.data) + + def __iter__(self): + yield from self.data + + iterable_dataset = DummyIterableDataset([n for n in range(30)]) + dataloader = DataLoader(iterable_dataset, batch_size=4) + accelerator = Accelerator() + prepared_dataloader = accelerator.prepare(dataloader) + + if accelerator.is_main_process: + logger = logging.root.manager.loggerDict["accelerate.accelerator"] + list_handler = ListHandler() + logger.addHandler(list_handler) + + batches_for_metrics = [] + for batch in prepared_dataloader: + batches_for_metrics.append(accelerator.gather_for_metrics(batch)) + + assert torch.cat(batches_for_metrics).size(0) == 30 + + if accelerator.is_main_process: + assert len(list_handler.logs) == 0 + logger.removeHandler(list_handler) + + +def test_gather_for_metrics_with_iterable_dataset(): + class DummyIterableDataset(IterableDataset): + def __init__(self, data): + self.data = data + + def __len__(self): + return len(self.data) + + def __iter__(self): + yield from self.data + + iterable_dataset = DummyIterableDataset(torch.as_tensor(range(30))) + dataloader = DataLoader(iterable_dataset, batch_size=4) + + accelerator = Accelerator() + prepared_dataloader = accelerator.prepare(dataloader) + + assert isinstance(prepared_dataloader, DataLoaderDispatcher) + + if accelerator.is_main_process: + logger = logging.root.manager.loggerDict["accelerate.accelerator"] + list_handler = ListHandler() + logger.addHandler(list_handler) + + batches_for_metrics = [] + for batch in prepared_dataloader: + batches_for_metrics.append(accelerator.gather_for_metrics(batch)) + + assert torch.cat(batches_for_metrics).size(0) == 30 + + if accelerator.is_main_process: + assert len(list_handler.logs) == 0 + + logger.removeHandler(list_handler) + + +def test_gather_for_metrics_drop_last(): + accelerator = Accelerator() + per_device_batch_size = 5 + num_items = (10 * accelerator.num_processes) + 1 + dataloader = DataLoader(range(num_items), batch_size=per_device_batch_size, drop_last=True) + dataloader = accelerator.prepare(dataloader) + + iterator = iter(dataloader) + next(iterator) # Skip first batch tensor([0, 1, 2, 3, 4], device='cuda:0') + batch = next(iterator) + gathered_items = accelerator.gather_for_metrics(batch) + + # Should return a full set of complete batches from each GPU + num_expected_items = per_device_batch_size * accelerator.num_processes + assert gathered_items.size(0) == (num_expected_items), ( + f"Expected number of items: {num_expected_items}, Actual: {gathered_items.size(0)}" + ) + + +def main(): + dataloader_config = DataLoaderConfiguration(split_batches=False, dispatch_batches=False) + accelerator = Accelerator(dataloader_config=dataloader_config) + if accelerator.is_local_main_process: + datasets.utils.logging.set_verbosity_warning() + transformers.utils.logging.set_verbosity_warning() + else: + datasets.utils.logging.set_verbosity_error() + transformers.utils.logging.set_verbosity_error() + # TorchXLA does not support batch dispatching. 'put_on_device' is always False for + # TorchXLA, which can cause a value error in 'prepare_data_loader' function. + dispatch_batches_options = [False] if accelerator.state.distributed_type == DistributedType.XLA else [True, False] + + # Temporarily close this test for TorchXLA due to the 'Cannot set version_counter for + # inference tensor' error in inference mode. Reopen it after TorchXLA fixes this bug. + # These are a bit slower so they should only be ran on the GPU or TPU + if accelerator.device.type != "cpu" and not is_torch_xla_available(): + if accelerator.is_local_main_process: + print("**Testing gather_for_metrics**") + for split_batches in [True, False]: + for dispatch_batches in dispatch_batches_options: + if accelerator.is_local_main_process: + print(f"With: `split_batches={split_batches}`, `dispatch_batches={dispatch_batches}`") + test_mrpc(dispatch_batches, split_batches) + accelerator.state._reset_state() + print("test_gather_for_metrics_with_iterable_dataset") + test_gather_for_metrics_with_iterable_dataset() + print("test gather_for_metrics_with_non_tensor_objects_iterable_dataset") + test_gather_for_metrics_with_non_tensor_objects_iterable_dataset() + + # MpDeviceLoader in TorchXLA is an asynchronous loader that preloads several batches into cache. + # This can cause the 'end_of_dataloader' of DataLoaderStateMixin to be set earlier than intended. + # Skip this test when TorchXLA is enabled. + if accelerator.state.distributed_type != DistributedType.XLA: + if accelerator.is_local_main_process: + print("**Test torch metrics**") + for split_batches in [True, False]: + for dispatch_batches in dispatch_batches_options: + dataloader_config = DataLoaderConfiguration( + split_batches=split_batches, dispatch_batches=dispatch_batches + ) + accelerator = Accelerator(dataloader_config=dataloader_config) + if accelerator.is_local_main_process: + print(f"With: `split_batches={split_batches}`, `dispatch_batches={dispatch_batches}`, length=99") + test_torch_metrics(accelerator, 99) + accelerator.state._reset_state() + if accelerator.is_local_main_process: + print("**Test last batch is not dropped when perfectly divisible**") + accelerator = Accelerator() + test_torch_metrics(accelerator, 512) + accelerator.state._reset_state() + if accelerator.is_local_main_process: + print("**Test that `drop_last` is taken into account**") + test_gather_for_metrics_drop_last() + accelerator.end_training() + accelerator.state._reset_state() + + +def _mp_fn(index): + # For xla_spawn (TPUs) + main() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_peak_memory_usage.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_peak_memory_usage.py new file mode 100644 index 0000000000000000000000000000000000000000..5d75259e9959c1b9f75850d81546a5e98dd206e8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_peak_memory_usage.py @@ -0,0 +1,323 @@ +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import gc +import json +import os + +import torch +from datasets import load_dataset +from torch.optim import AdamW +from torch.utils.data import DataLoader +from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup, set_seed + +from accelerate import Accelerator, DistributedType +from accelerate.utils import ( + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_xpu_available, +) +from accelerate.utils.deepspeed import DummyOptim, DummyScheduler + + +MAX_GPU_BATCH_SIZE = 16 +EVAL_BATCH_SIZE = 32 + + +# Converting Bytes to Megabytes +def b2mb(x): + return int(x / 2**20) + + +# This context manager is used to track the peak memory usage of the process +class TorchTracemalloc: + def __enter__(self): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.reset_max_memory_allocated() # reset the peak gauge to zero + self.begin = torch.cuda.memory_allocated() + elif is_mlu_available(): + torch.mlu.empty_cache() + torch.mlu.reset_max_memory_allocated() # reset the peak gauge to zero + self.begin = torch.mlu.memory_allocated() + elif is_sdaa_available(): + torch.sdaa.empty_cache() + torch.sdaa.reset_max_memory_allocated() # reset the peak gauge to zero + self.begin = torch.sdaa.memory_allocated() + elif is_musa_available(): + torch.musa.empty_cache() + torch.musa.reset_max_memory_allocated() # reset the peak gauge to zero + self.begin = torch.musa.memory_allocated() + elif is_npu_available(): + torch.npu.empty_cache() + torch.npu.reset_max_memory_allocated() # reset the peak gauge to zero + self.begin = torch.npu.memory_allocated() + elif is_xpu_available(): + torch.xpu.empty_cache() + torch.xpu.reset_peak_memory_stats() # reset the peak gauge to zero + self.begin = torch.xpu.memory_allocated() + elif is_hpu_available(): + # torch.hpu.empty_cache() # not available on hpu as it reserves all device memory for the current process + torch.hpu.reset_peak_memory_stats() # reset the peak gauge to zero + self.begin = torch.hpu.memory_allocated() + elif is_neuron_available(): + torch.neuron.empty_cache() + torch.neuron.reset_peak_memory_stats() # reset the peak gauge to zero + self.begin = torch.neuron.memory_allocated() + return self + + def __exit__(self, *exc): + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + self.end = torch.cuda.memory_allocated() + self.peak = torch.cuda.max_memory_allocated() + elif is_mlu_available(): + torch.mlu.empty_cache() + self.end = torch.mlu.memory_allocated() + self.begin = torch.mlu.max_memory_allocated() + elif is_sdaa_available(): + torch.sdaa.empty_cache() + self.end = torch.sdaa.memory_allocated() + self.begin = torch.sdaa.max_memory_allocated() + elif is_musa_available(): + torch.musa.empty_cache() + self.end = torch.musa.memory_allocated() + self.begin = torch.musa.max_memory_allocated() + elif is_npu_available(): + torch.npu.empty_cache() + self.end = torch.npu.memory_allocated() + self.peak = torch.npu.max_memory_allocated() + elif is_xpu_available(): + torch.xpu.empty_cache() + self.end = torch.xpu.memory_allocated() + self.peak = torch.xpu.max_memory_allocated() + elif is_hpu_available(): + # torch.hpu.empty_cache() # not available on hpu as it reserves all device memory for the current process + self.end = torch.hpu.memory_allocated() + self.peak = torch.hpu.max_memory_allocated() + elif is_neuron_available(): + torch.neuron.empty_cache() + self.end = torch.neuron.memory_allocated() + self.peak = torch.neuron.max_memory_allocated() + self.used = b2mb(self.end - self.begin) + self.peaked = b2mb(self.peak - self.begin) + # print(f"delta used/peak {self.used:4d}/{self.peaked:4d}") + + +def get_dataloaders( + accelerator: Accelerator, + batch_size: int = 16, + model_name: str = "bert-base-cased", + n_train: int = 320, + n_val: int = 160, +): + """ + Creates a set of `DataLoader`s for the `glue` dataset. + + Args: + accelerator (`Accelerator`): + An `Accelerator` object + batch_size (`int`, *optional*): + The batch size for the train and validation DataLoaders. + model_name (`str`, *optional*): + The name of the model to use. + n_train (`int`, *optional*): + The number of training examples to use. + n_val (`int`, *optional*): + The number of validation examples to use. + """ + tokenizer = AutoTokenizer.from_pretrained(model_name) + datasets = load_dataset( + "glue", "mrpc", split={"train": f"train[:{n_train}]", "validation": f"validation[:{n_val}]"} + ) + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None) + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + tokenized_datasets = datasets.map( + tokenize_function, batched=True, remove_columns=["idx", "sentence1", "sentence2"], load_from_cache_file=False + ) + + # We also rename the 'label' column to 'labels' which is the expected name for labels by the models of the + # transformers library + tokenized_datasets = tokenized_datasets.rename_column("label", "labels") + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + if accelerator.distributed_type == DistributedType.XLA: + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + + # Instantiate dataloaders. + train_dataloader = DataLoader( + tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=batch_size + ) + eval_dataloader = DataLoader( + tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=EVAL_BATCH_SIZE + ) + + return train_dataloader, eval_dataloader + + +def training_function(config, args): + # Initialize accelerator + accelerator = Accelerator() + + # Sample hyper-parameters for learning rate, batch size, seed and a few other HPs + lr = config["lr"] + num_epochs = int(config["num_epochs"]) + seed = int(config["seed"]) + batch_size = int(config["batch_size"]) + model_name = args.model_name_or_path + + set_seed(seed) + train_dataloader, eval_dataloader = get_dataloaders(accelerator, batch_size, model_name, args.n_train, args.n_val) + + # Instantiate the model (we build the model here so that the seed also control new weights initialization) + model = AutoModelForSequenceClassification.from_pretrained(model_name, return_dict=True) + + # Instantiate optimizer + optimizer_cls = ( + AdamW + if accelerator.state.deepspeed_plugin is None + or "optimizer" not in accelerator.state.deepspeed_plugin.deepspeed_config + else DummyOptim + ) + optimizer = optimizer_cls(params=model.parameters(), lr=lr) + + if accelerator.state.deepspeed_plugin is not None: + gradient_accumulation_steps = accelerator.state.deepspeed_plugin.deepspeed_config[ + "gradient_accumulation_steps" + ] + else: + gradient_accumulation_steps = 1 + max_training_steps = (len(train_dataloader) * num_epochs) // gradient_accumulation_steps + + # Instantiate scheduler + if ( + accelerator.state.deepspeed_plugin is None + or "scheduler" not in accelerator.state.deepspeed_plugin.deepspeed_config + ): + lr_scheduler = get_linear_schedule_with_warmup( + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=max_training_steps, + ) + else: + lr_scheduler = DummyScheduler(optimizer, total_num_steps=max_training_steps, warmup_num_steps=0) + + # Prepare everything + # There is no specific order to remember, we just need to unpack the objects in the same order we gave them to the + # prepare method. + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler + ) + + # We need to keep track of how many total steps we have iterated over + overall_step = 0 + # We also need to keep track of the stating epoch so files are named properly + starting_epoch = 0 + + # Now we train the model + train_total_peak_memory = {} + for epoch in range(starting_epoch, num_epochs): + with TorchTracemalloc() as tracemalloc: + model.train() + for step, batch in enumerate(train_dataloader): + outputs = model(**batch) + loss = outputs.loss + loss = loss / gradient_accumulation_steps + accelerator.backward(loss) + if step % gradient_accumulation_steps == 0: + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + overall_step += 1 + + # Printing the GPU memory usage details such as allocated memory, peak memory, and total memory usage + accelerator.print(f"Memory before entering the train : {b2mb(tracemalloc.begin)}") + accelerator.print(f"Memory consumed at the end of the train (end-begin): {tracemalloc.used}") + accelerator.print(f"Peak Memory consumed during the train (max-begin): {tracemalloc.peaked}") + accelerator.print( + f"Total Peak Memory consumed during the train (max): {tracemalloc.peaked + b2mb(tracemalloc.begin)}" + ) + train_total_peak_memory[f"epoch-{epoch}"] = tracemalloc.peaked + b2mb(tracemalloc.begin) + if args.peak_memory_upper_bound is not None: + assert train_total_peak_memory[f"epoch-{epoch}"] <= args.peak_memory_upper_bound, ( + "Peak memory usage exceeded the upper bound" + ) + + accelerator.wait_for_everyone() + if accelerator.is_main_process: + with open(os.path.join(args.output_dir, "peak_memory_utilization.json"), "w") as f: + json.dump(train_total_peak_memory, f) + accelerator.end_training() + + +def main(): + parser = argparse.ArgumentParser(description="Simple example of training script tracking peak GPU memory usage.") + parser.add_argument( + "--model_name_or_path", + type=str, + default="bert-base-cased", + help="Path to pretrained model or model identifier from huggingface.co/models.", + required=False, + ) + parser.add_argument( + "--output_dir", + type=str, + default=".", + help="Optional save directory where all checkpoint folders will be stored. Default is the current working directory.", + ) + parser.add_argument( + "--peak_memory_upper_bound", + type=float, + default=None, + help="The upper bound of peak memory usage in MB. If set, the training will throw an error if the peak memory usage exceeds this value.", + ) + parser.add_argument( + "--n_train", + type=int, + default=320, + help="Number of training examples to use.", + ) + parser.add_argument( + "--n_val", + type=int, + default=160, + help="Number of validation examples to use.", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=1, + help="Number of train epochs.", + ) + args = parser.parse_args() + config = {"lr": 2e-5, "num_epochs": args.num_epochs, "seed": 42, "batch_size": 16} + training_function(config, args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_performance.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_performance.py new file mode 100644 index 0000000000000000000000000000000000000000..8e500bdd4c01013904375f010e99d22aea4e4ff9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_performance.py @@ -0,0 +1,299 @@ +# Copyright 2022 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import json +import os +from contextlib import nullcontext +from pathlib import Path + +import evaluate +import torch +from datasets import load_dataset +from torch.optim import AdamW +from torch.utils.data import DataLoader +from transformers import AutoModelForSequenceClassification, AutoTokenizer, get_linear_schedule_with_warmup + +from accelerate import Accelerator, DistributedType +from accelerate.parallelism_config import ParallelismConfig +from accelerate.utils import SAFE_WEIGHTS_NAME, set_seed +from accelerate.utils.deepspeed import DummyOptim, DummyScheduler + + +MAX_GPU_BATCH_SIZE = 16 +EVAL_BATCH_SIZE = 32 + + +def get_dataloaders(accelerator: Accelerator, batch_size: int = 16, model_name: str = "bert-base-cased"): + """ + Creates a set of `DataLoader`s for the `glue` dataset. + + Args: + accelerator (`Accelerator`): + An `Accelerator` object + batch_size (`int`, *optional*): + The batch size for the train and validation DataLoaders. + model_name (`str`, *optional*): + """ + tokenizer = AutoTokenizer.from_pretrained(model_name) + + datasets = load_dataset("glue", "mrpc") + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer(examples["sentence1"], examples["sentence2"], truncation=True, max_length=None) + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + tokenized_datasets = datasets.map( + tokenize_function, batched=True, remove_columns=["idx", "sentence1", "sentence2"], load_from_cache_file=False + ) + + # We also rename the 'label' column to 'labels' which is the expected name for labels by the models of the + # transformers library + tokenized_datasets = tokenized_datasets.rename_column("label", "labels") + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + if accelerator.distributed_type == DistributedType.XLA: + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + + # Instantiate dataloaders. + train_dataloader = DataLoader( + tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=batch_size + ) + eval_dataloader = DataLoader( + tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=EVAL_BATCH_SIZE + ) + + return train_dataloader, eval_dataloader + + +def training_function(config, args): + accelerator_kwargs = {} + # need this for DeepSpeed tests as `args.tp_size` would be None and `torch.distributed.init_device_mesh` would fail + if args.tp_size is not None: + accelerator_kwargs["parallelism_config"] = ParallelismConfig(tp_size=args.tp_size) + + # Initialize accelerator + accelerator = Accelerator(**accelerator_kwargs) + + # Sample hyper-parameters for learning rate, batch size, seed and a few other HPs + lr = config["lr"] + num_epochs = int(config["num_epochs"]) + seed = int(config["seed"]) + batch_size = int(config["batch_size"]) + model_name = args.model_name_or_path + + set_seed(seed) + train_dataloader, eval_dataloader = get_dataloaders(accelerator, batch_size, model_name) + + # Add TP related kwargs if provided + model_kwargs = {} + if args.tp_plan is not None: + model_kwargs["tp_plan"] = args.tp_plan + if args.tp_size is not None: + model_kwargs["tp_size"] = args.tp_size + + # Instantiate the model (we build the model here so that the seed also control new weights initialization) + model = AutoModelForSequenceClassification.from_pretrained(model_name, return_dict=True, **model_kwargs) + + if args.add_pad_token: + if model.config.pad_token_id is None: + model.config.pad_token_id = 0 + + # Instantiate optimizer + optimizer_cls = ( + AdamW + if accelerator.state.deepspeed_plugin is None + or "optimizer" not in accelerator.state.deepspeed_plugin.deepspeed_config + else DummyOptim + ) + optimizer = optimizer_cls(params=model.parameters(), lr=lr) + + max_training_steps = len(train_dataloader) * num_epochs + + # Instantiate scheduler + linear_decay_scheduler = False + if ( + accelerator.state.deepspeed_plugin is None + or "scheduler" not in accelerator.state.deepspeed_plugin.deepspeed_config + ): + lr_scheduler = get_linear_schedule_with_warmup( + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=max_training_steps, + ) + linear_decay_scheduler = True + else: + lr_scheduler = DummyScheduler(optimizer, total_num_steps=max_training_steps, warmup_num_steps=0) + + # Prepare everything + # There is no specific order to remember, we just need to unpack the objects in the same order we gave them to the + # prepare method. + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader, lr_scheduler + ) + + # We also need to keep track of the stating epoch so files are named properly + starting_epoch = 0 + + # Now we train the model + metric = evaluate.load("glue", "mrpc") + best_performance = 0 + performance_metric = {} + expected_lr_after_first_optim_step = lr * ( + 1 - 1 / (max_training_steps / accelerator.num_processes / accelerator.gradient_accumulation_steps) + ) + lr_scheduler_check_completed = False + for epoch in range(starting_epoch, num_epochs): + model.train() + for step, batch in enumerate(train_dataloader): + with accelerator.accumulate(model): + outputs = model(**batch) + loss = outputs.loss + accelerator.backward(loss) + context = nullcontext + if args.tp_plan is not None: + from torch.distributed._tensor.experimental import implicit_replication + + context = implicit_replication + with context(): + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + + # assert the learning rate after first optimizer step + if ( + accelerator.sync_gradients + and not lr_scheduler_check_completed + and linear_decay_scheduler + and accelerator.state.mixed_precision == "no" + ): + assert lr_scheduler.get_last_lr()[0] == expected_lr_after_first_optim_step, ( + f"Wrong lr found at second step, expected {expected_lr_after_first_optim_step}, got {lr_scheduler.get_last_lr()[0]}" + ) + lr_scheduler_check_completed = True + + model.eval() + samples_seen = 0 + for step, batch in enumerate(eval_dataloader): + # We could avoid this line since we set the accelerator with `device_placement=True`. + batch.to(accelerator.device) + with torch.no_grad(): + outputs = model(**batch) + predictions = outputs.logits.argmax(dim=-1) + # It is slightly faster to call this once, than multiple times + predictions, references = accelerator.gather( + (predictions, batch["labels"]) + ) # If we are in a multiprocess environment, the last batch has duplicates + if accelerator.use_distributed: + if step == len(eval_dataloader) - 1: + predictions = predictions[: len(eval_dataloader.dataset) - samples_seen] + references = references[: len(eval_dataloader.dataset) - samples_seen] + else: + samples_seen += references.shape[0] + metric.add_batch( + predictions=predictions, + references=references, + ) + + eval_metric = metric.compute() + # Use accelerator.print to print only on the main process. + accelerator.print(f"epoch {epoch}:", eval_metric) + performance_metric[f"epoch-{epoch}"] = eval_metric["accuracy"] + + if best_performance < eval_metric["accuracy"]: + best_performance = eval_metric["accuracy"] + + # check that the LR is 0 + if linear_decay_scheduler and accelerator.state.mixed_precision == "no": + assert lr_scheduler.get_last_lr()[0] == 0, ( + f"Wrong lr found at last step, expected 0, got {lr_scheduler.get_last_lr()[0]}" + ) + + if args.performance_lower_bound is not None: + assert args.performance_lower_bound <= best_performance, ( + f"Best performance metric {best_performance} is lower than the lower bound {args.performance_lower_bound}" + ) + + accelerator.wait_for_everyone() + if accelerator.is_main_process: + with open(os.path.join(args.output_dir, "all_results.json"), "w") as f: + json.dump(performance_metric, f) + + # TODO: skip saving of the model test for TP until the feature lands + if args.tp_plan is None: + # Finally try saving the model + accelerator.save_model(model, args.output_dir) + accelerator.wait_for_everyone() + if args.tp_plan is None: + assert Path(args.output_dir, SAFE_WEIGHTS_NAME).exists(), ( + "Model was not saved when calling `Accelerator.save_model`" + ) + accelerator.end_training() + + +def main(): + parser = argparse.ArgumentParser(description="Simple example of training script tracking peak GPU memory usage.") + parser.add_argument( + "--model_name_or_path", + type=str, + default="bert-base-cased", + help="Path to pretrained model or model identifier from huggingface.co/models.", + required=False, + ) + parser.add_argument( + "--output_dir", + type=str, + default=".", + help="Optional save directory where all checkpoint folders will be stored. Default is the current working directory.", + ) + parser.add_argument( + "--performance_lower_bound", + type=float, + default=None, + help="Optional lower bound for the performance metric. If set, the training will throw error when the performance metric drops below this value.", + ) + parser.add_argument( + "--num_epochs", + type=int, + default=3, + help="Number of train epochs.", + ) + parser.add_argument( + "--add_pad_token", + type=bool, + default=False, + help="To add pad token if not exists.", + ) + parser.add_argument( + "--tp_plan", + type=str, + default=None, + help="pass 'auto' to use TP", + ) + parser.add_argument( + "--tp_size", + type=int, + default=None, + help="TP size to be used to shard the model", + ) + args = parser.parse_args() + config = {"lr": 2e-5, "num_epochs": args.num_epochs, "seed": 42, "batch_size": 16} + training_function(config, args) + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_pippy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_pippy.py new file mode 100644 index 0000000000000000000000000000000000000000..1dbd86c46b4a0c12df8ea4d736c7cd1e03f81813 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_pippy.py @@ -0,0 +1,117 @@ +# Copyright 2024 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import torch +from transformers import ( + BertConfig, + BertForMaskedLM, + GPT2Config, + GPT2ForSequenceClassification, +) + +from accelerate import PartialState +from accelerate.inference import prepare_pippy +from accelerate.test_utils import torch_device +from accelerate.utils import DistributedType, set_seed + + +model_to_config = { + "bert": (BertForMaskedLM, BertConfig, 512), + "gpt2": (GPT2ForSequenceClassification, GPT2Config, 1024), +} + + +def get_model_and_data_for_text(model_name, device, num_processes: int = 2): + initializer, config, seq_len = model_to_config[model_name] + config_args = {} + # Eventually needed for batch inference tests on gpt-2 when bs != 1 + # if model_name == "gpt2": + # config_args["pad_token_id"] = 0 + model_config = config(**config_args) + model = initializer(model_config) + kwargs = dict(low=0, high=model_config.vocab_size, device=device, dtype=torch.int64, requires_grad=False) + trace_input = torch.randint(size=(1, seq_len), **kwargs) + inference_inputs = torch.randint(size=(num_processes, seq_len), **kwargs) + return model, trace_input, inference_inputs + + +def test_bert(batch_size: int = 2): + set_seed(42) + state = PartialState() + model, trace_input, inference_inputs = get_model_and_data_for_text("bert", "cpu", batch_size) + model = prepare_pippy(model, example_args=(trace_input,), no_split_module_classes=model._no_split_modules) + # For inference args need to be a tuple + inputs = inference_inputs.to(torch_device) + with torch.no_grad(): + output = model(inputs) + # Zach: Check that we just grab the real outputs we need at the end + if not state.is_last_process: + assert output is None, "Output was not generated on just the last process!" + else: + assert output is not None, "Output was not generated in the last process!" + + +def test_gpt2(batch_size: int = 2): + set_seed(42) + state = PartialState() + model, trace_input, inference_inputs = get_model_and_data_for_text("gpt2", "cpu", batch_size) + model = prepare_pippy(model, example_args=(trace_input,), no_split_module_classes=model._no_split_modules) + # For inference args need to be a tuple + inputs = inference_inputs.to(torch_device) + with torch.no_grad(): + output = model(inputs) + # Zach: Check that we just grab the real outputs we need at the end + if not state.is_last_process: + assert output is None, "Output was not generated on just the last process!" + else: + assert output is not None, "Output was not generated in the last process!" + + +# Currently disabled, enable again once PyTorch pippy interface can trace a resnet34 +# def test_resnet(batch_size: int = 2): +# set_seed(42) +# state = PartialState() +# model = resnet34() +# input_tensor = torch.rand(1, 3, 224, 224) +# model = prepare_pippy( +# model, +# example_args=(input_tensor,), +# ) +# inference_inputs = torch.rand(batch_size, 3, 224, 224) +# inputs = send_to_device(inference_inputs, torch_device) +# with torch.no_grad(): +# output = model(inputs) +# # Zach: Check that we just grab the real outputs we need at the end +# if not state.is_last_process: +# assert output is None, "Output was not generated on just the last process!" +# else: +# assert output is not None, "Output was not generated in the last process!" + + +if __name__ == "__main__": + state = PartialState() + state.print("Testing pippy integration...") + try: + if state.distributed_type in [DistributedType.MULTI_GPU, DistributedType.MULTI_XPU, DistributedType.MULTI_HPU]: + state.print("Testing GPT2...") + test_gpt2() + # Issue: When modifying the tokenizer for batch GPT2 inference, there's an issue + # due to references + # NameError: cannot access free variable 'chunk_args_list' where it is not associated with a value in enclosing scope + # test_gpt2(3) + state.print("Testing BERT...") + test_bert() + else: + print("Less than two GPUs found, not running tests!") + finally: + state.destroy_process_group() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_zero3_integration.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_zero3_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..f6e46d342a0c754cf1ed5b2629290d62e7c943aa --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/external_deps/test_zero3_integration.py @@ -0,0 +1,59 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch.distributed + +from accelerate.test_utils import require_huggingface_suite, torch_device +from accelerate.utils import is_transformers_available + + +if is_transformers_available(): + from transformers import AutoModel, TrainingArguments + + +GPT2_TINY = "sshleifer/tiny-gpt2" + + +@require_huggingface_suite +def init_torch_dist_then_launch_deepspeed(): + if torch_device == "xpu": + backend = "xccl" + elif torch_device == "hpu": + backend = "hccl" + else: + backend = "nccl" + + torch.distributed.init_process_group(backend=backend) + deepspeed_config = { + "zero_optimization": { + "stage": 3, + }, + "train_batch_size": "auto", + "train_micro_batch_size_per_gpu": "auto", + } + train_args = TrainingArguments( + output_dir="./", + deepspeed=deepspeed_config, + ) + model = AutoModel.from_pretrained(GPT2_TINY) + assert train_args is not None + assert model is not None + + +def main(): + init_torch_dist_then_launch_deepspeed() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_cli.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_cli.py new file mode 100644 index 0000000000000000000000000000000000000000..fc9dd1d36e8f2949d6e3cebb8ce65efbf0b9e5e4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_cli.py @@ -0,0 +1,32 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import torch + +from accelerate.utils import is_xpu_available + + +def main(): + accelerator_type = "GPU" + num_accelerators = 0 + if torch.cuda.is_available(): + num_accelerators = torch.cuda.device_count() + accelerator_type = "GPU" + elif is_xpu_available(): + num_accelerators = torch.xpu.device_count() + accelerator_type = "XPU" + print(f"Successfully ran on {num_accelerators} {accelerator_type}s") + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ddp_comm_hook.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ddp_comm_hook.py new file mode 100644 index 0000000000000000000000000000000000000000..0db5844e026d1c035670e518a8f81d33136ea665 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ddp_comm_hook.py @@ -0,0 +1,85 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import torch + +from accelerate import Accelerator, DDPCommunicationHookType, DistributedDataParallelKwargs, PartialState +from accelerate.utils import is_hpu_available + + +class MockModel(torch.nn.Module): + def __init__(self): + super().__init__() + torch.manual_seed(0) + self.p = torch.nn.Parameter(torch.randn(40, 20)) + + def forward(self, x, rank): + return self.p * (x ** (1 + rank)) + + +def _run_and_get_grads(model, rank): + torch.manual_seed(2024) + input = torch.randn(40, 20) + output = model(input, rank) + output.mean().backward() + param = next(model.parameters()) + return param.grad + + +def test_ddp_comm_hook(comm_hook, comm_wrapper, comm_state_option): + ddp_kwargs = DistributedDataParallelKwargs( + comm_hook=comm_hook, + comm_wrapper=comm_wrapper, + comm_state_option=comm_state_option, + ) + accelerator = Accelerator(kwargs_handlers=[ddp_kwargs]) + + model = accelerator.prepare(MockModel()) + hook_grads = _run_and_get_grads(model, accelerator.local_process_index) + + reference_model = torch.nn.parallel.DistributedDataParallel( + MockModel().to(accelerator.device), + device_ids=[accelerator.local_process_index], + output_device=accelerator.local_process_index, + ) + reference_grads = _run_and_get_grads(reference_model, accelerator.local_process_index) + + torch.testing.assert_close(hook_grads, reference_grads, rtol=1e-2, atol=1e-2) + + +def main(): + for comm_hook, comm_wrapper, comm_state_option in [ + (DDPCommunicationHookType.NO, DDPCommunicationHookType.NO, {}), + (DDPCommunicationHookType.FP16, DDPCommunicationHookType.NO, {}), + (DDPCommunicationHookType.BF16, DDPCommunicationHookType.NO, {}), + (DDPCommunicationHookType.POWER_SGD, DDPCommunicationHookType.NO, {}), + (DDPCommunicationHookType.POWER_SGD, DDPCommunicationHookType.FP16, {}), + (DDPCommunicationHookType.POWER_SGD, DDPCommunicationHookType.BF16, {}), + (DDPCommunicationHookType.POWER_SGD, DDPCommunicationHookType.NO, {"matrix_approximation_rank": 2}), + (DDPCommunicationHookType.BATCHED_POWER_SGD, DDPCommunicationHookType.NO, {}), + (DDPCommunicationHookType.BATCHED_POWER_SGD, DDPCommunicationHookType.FP16, {}), + (DDPCommunicationHookType.BATCHED_POWER_SGD, DDPCommunicationHookType.BF16, {}), + ]: + if is_hpu_available(): + HPU_UNSUPPORTED_COMM_HOOKS = {DDPCommunicationHookType.FP16, DDPCommunicationHookType.BF16} + if comm_hook in HPU_UNSUPPORTED_COMM_HOOKS or comm_wrapper in HPU_UNSUPPORTED_COMM_HOOKS: + print(f"Skipping test DDP comm hook: {comm_hook}, comm wrapper: {comm_wrapper} on HPU") + continue + + print(f"Test DDP comm hook: {comm_hook}, comm wrapper: {comm_wrapper}") + test_ddp_comm_hook(comm_hook, comm_wrapper, comm_state_option) + PartialState().destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_distributed_data_loop.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_distributed_data_loop.py new file mode 100644 index 0000000000000000000000000000000000000000..84a9247a6bce04a55a67ad33e1761c627772aced --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_distributed_data_loop.py @@ -0,0 +1,429 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pickle +import tempfile +import warnings +from unittest.mock import Mock + +import torch +from torch.utils.data import ( + BatchSampler, + DataLoader, + Dataset, + IterableDataset, + RandomSampler, + TensorDataset, + default_collate, +) + +from accelerate.accelerator import Accelerator, DataLoaderConfiguration +from accelerate.utils.dataclasses import DistributedType + + +NUM_ELEMENTS = 22 +NUM_WORKERS = 4 +BATCH_SIZE = 4 + + +class DummyDataset(Dataset): + def __len__(self): + return NUM_ELEMENTS + + def __getitem__(self, index): + squeeze = False + + if isinstance(index, int): + index = [index] + squeeze = True + elif isinstance(index, slice): + index = list(range(*index.indices(self.size))) + else: + index = list(index) + + batch = [{"index": i, "label": i % 2, "random_augmentation": torch.rand(1).item()} for i in index] + + if squeeze: + batch = batch[0] + + return batch + + +class DummyIterableDataset(IterableDataset): + def __init__(self, data): + self.data = data + + def __iter__(self): + yield from self.data + + +def create_accelerator(even_batches=True): + dataloader_config = DataLoaderConfiguration(even_batches=even_batches) + accelerator = Accelerator(dataloader_config=dataloader_config) + assert accelerator.num_processes == 2, "this script expects that two GPUs are available" + return accelerator + + +def create_dataloader( + accelerator: Accelerator, dataset_size: int, batch_size: int, iterable: bool = False, shuffle: bool = False +): + """ + Create a simple DataLoader to use during the test cases + """ + values = torch.as_tensor(range(dataset_size)) + if shuffle: + values = values[torch.randperm(values.size(0))] + if iterable: + dataset = DummyIterableDataset(values) + else: + dataset = TensorDataset(torch.as_tensor(range(dataset_size))) + + dl = DataLoader(dataset, batch_size=batch_size) + dl = accelerator.prepare(dl) + + return dl + + +def verify_dataloader_batch_sizes( + accelerator: Accelerator, + dataset_size: int, + batch_size: int, + process_0_expected_batch_sizes: list[int], + process_1_expected_batch_sizes: list[int], +): + """ + A helper function for verifying the batch sizes coming from a prepared dataloader in each process + """ + dl = create_dataloader(accelerator=accelerator, dataset_size=dataset_size, batch_size=batch_size) + + batch_sizes = [len(batch[0]) for batch in dl] + + if accelerator.process_index == 0: + assert batch_sizes == process_0_expected_batch_sizes + elif accelerator.process_index == 1: + assert batch_sizes == process_1_expected_batch_sizes + + +def test_default_ensures_even_batch_sizes(): + accelerator = create_accelerator() + + # without padding, we would expect a different number of batches + verify_dataloader_batch_sizes( + accelerator, + dataset_size=3, + batch_size=1, + process_0_expected_batch_sizes=[1, 1], + process_1_expected_batch_sizes=[1, 1], + ) + + # without padding, we would expect the same number of batches, but different sizes + verify_dataloader_batch_sizes( + accelerator, + dataset_size=7, + batch_size=2, + process_0_expected_batch_sizes=[2, 2], + process_1_expected_batch_sizes=[2, 2], + ) + + +def test_can_disable_even_batches(): + accelerator = create_accelerator(even_batches=False) + + verify_dataloader_batch_sizes( + accelerator, + dataset_size=3, + batch_size=1, + process_0_expected_batch_sizes=[1, 1], + process_1_expected_batch_sizes=[1], + ) + + verify_dataloader_batch_sizes( + accelerator, + dataset_size=7, + batch_size=2, + process_0_expected_batch_sizes=[2, 2], + process_1_expected_batch_sizes=[2, 1], + ) + + +def test_can_join_uneven_inputs(): + accelerator = create_accelerator(even_batches=False) + + model = torch.nn.Linear(1, 1) + ddp_model = accelerator.prepare(model) + + dl = create_dataloader(accelerator, dataset_size=3, batch_size=1) + + batch_idxs = [] + with accelerator.join_uneven_inputs([ddp_model]): + for batch_idx, batch in enumerate(dl): + output = ddp_model(batch[0].float()) + loss = output.sum() + loss.backward() + batch_idxs.append(batch_idx) + + accelerator.wait_for_everyone() + + if accelerator.process_index == 0: + assert batch_idxs == [0, 1] + elif accelerator.process_index == 1: + assert batch_idxs == [0] + + +def test_join_raises_warning_for_non_ddp_distributed(accelerator): + with warnings.catch_warnings(record=True) as w: + with accelerator.join_uneven_inputs([Mock()]): + pass + + assert issubclass(w[-1].category, UserWarning) + assert "only supported for multi-GPU" in str(w[-1].message) + + +def test_join_can_override_even_batches(): + default_even_batches = True + overridden_even_batches = False + accelerator = create_accelerator(even_batches=default_even_batches) + model = torch.nn.Linear(1, 1) + ddp_model = accelerator.prepare(model) + train_dl = create_dataloader(accelerator, dataset_size=3, batch_size=1) + valid_dl = create_dataloader(accelerator, dataset_size=3, batch_size=1) + + with accelerator.join_uneven_inputs([ddp_model], even_batches=overridden_even_batches): + train_dl_overridden_value = train_dl.batch_sampler.even_batches + valid_dl_overridden_value = valid_dl.batch_sampler.even_batches + + assert train_dl_overridden_value == overridden_even_batches + assert valid_dl_overridden_value == overridden_even_batches + assert train_dl.batch_sampler.even_batches == default_even_batches + assert valid_dl.batch_sampler.even_batches == default_even_batches + + +def test_join_can_override_for_mixed_type_dataloaders(): + default_even_batches = True + overridden_even_batches = False + accelerator = create_accelerator(even_batches=default_even_batches) + model = torch.nn.Linear(1, 1) + ddp_model = accelerator.prepare(model) + create_dataloader(accelerator, dataset_size=3, batch_size=1, iterable=True) + batch_dl = create_dataloader(accelerator, dataset_size=3, batch_size=1) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + try: + with accelerator.join_uneven_inputs([ddp_model], even_batches=overridden_even_batches): + batch_dl_overridden_value = batch_dl.batch_sampler.even_batches + except AttributeError: + # ensure attribute error is not raised when processing iterable dl + raise AssertionError + + assert batch_dl_overridden_value == overridden_even_batches + assert batch_dl.batch_sampler.even_batches == default_even_batches + + +def test_join_raises_warning_for_iterable_when_overriding_even_batches(): + accelerator = create_accelerator() + model = torch.nn.Linear(1, 1) + ddp_model = accelerator.prepare(model) + create_dataloader(accelerator, dataset_size=3, batch_size=1, iterable=True) + + with warnings.catch_warnings(record=True) as w: + with accelerator.join_uneven_inputs([ddp_model], even_batches=False): + pass + + assert issubclass(w[-1].category, UserWarning) + assert "only supported for map-style datasets" in str(w[-1].message) + + +def test_pickle_accelerator(): + accelerator = create_accelerator() + data_loader = create_dataloader(accelerator, dataset_size=32, batch_size=4) + _ = accelerator.prepare(data_loader) + pickled_accelerator = pickle.dumps(accelerator) + unpickled_accelerator = pickle.loads(pickled_accelerator) + # TODO: Maybe this should be implemented as __eq__ for AcceleratorState? + assert accelerator.state.__dict__ == unpickled_accelerator.state.__dict__ + + +def test_data_loader(data_loader, accelerator): + # Prepare the DataLoader + data_loader = accelerator.prepare(data_loader) + + all_examples = [] + for i, batch in enumerate(data_loader): + index, _ = accelerator.gather_for_metrics((batch["index"], batch["label"])) + all_examples.extend(index.detach().cpu().numpy().tolist()) + + # Sort the examples + sorted_all_examples = sorted(all_examples) + + # Check if all elements are present in the sorted list of iterated samples + assert len(set(sorted_all_examples)) == NUM_ELEMENTS, ( + "Not all the dataset elements have been iterated in an epoch due to duplication of samples across processes." + ) + + +def _test_stateful_dataloader_resume(accelerator, iterable): + """ + Helper: iterate a stateful dataloader, save state after a few batches using `load_state_dict`, + resume from the saved state, and verify the resumed batches match what was originally unseen. + + Saves early (after 3 batches) so many batches remain, exposing any off-by-one in state restoration. + Tested with both iterable and map-style datasets to cover different state_dict code paths. + """ + old_dataloader_config = accelerator.dataloader_config + try: + accelerator.dataloader_config = DataLoaderConfiguration(use_stateful_dataloader=True) + prepared_dl = create_dataloader( + accelerator, dataset_size=32 * accelerator.num_processes, batch_size=4, iterable=iterable, shuffle=True + ) + untrained_batches = [] + save_step = 2 + for step, batch in enumerate(prepared_dl): + if step == save_step: + state_dict = prepared_dl.state_dict() + if step > save_step: + untrained_batches.append(batch) + not_skipped_batches = accelerator.gather(untrained_batches) + prepared_dl.load_state_dict(state_dict) + resumed_batches = [] + for batch in prepared_dl: + resumed_batches.append(batch) + resumed_batches = accelerator.gather(resumed_batches) + assert len(not_skipped_batches) == len(resumed_batches), ( + f"Expected {len(not_skipped_batches)} batches after resume, got {len(resumed_batches)}" + ) + for b1, b2 in zip(not_skipped_batches, resumed_batches): + for v1, v2 in zip(b1, b2): + assert torch.equal(v1, v2), f"Batch {b1} and {b2} are not equal" + finally: + accelerator.dataloader_config = old_dataloader_config + + +def test_stateful_dataloader(accelerator): + """ + Tests that a stateful dataloader can be iterated over, saved after a few batches using `load_state_dict`, and then + resumed from the saved state. + + The result should be the same as the rest of the data that iterated over after saving. + """ + _test_stateful_dataloader_resume(accelerator, iterable=True) + _test_stateful_dataloader_resume(accelerator, iterable=False) + + +def _test_stateful_dataloader_save_state_resume(accelerator, iterable): + """ + Helper: iterate a stateful dataloader, save state after a few batches using `Accelerator.save_state`, + resume, and verify the resumed batches match what was originally unseen. + """ + old_dataloader_config = accelerator.dataloader_config + try: + with tempfile.TemporaryDirectory() as tmpdir: + accelerator.dataloader_config = DataLoaderConfiguration(use_stateful_dataloader=True) + prepared_dl = create_dataloader( + accelerator, dataset_size=32 * accelerator.num_processes, batch_size=4, iterable=iterable, shuffle=True + ) + untrained_batches = [] + save_step = 2 + for step, batch in enumerate(prepared_dl): + if step == save_step: + accelerator.save_state(tmpdir) + if step > save_step: + untrained_batches.append(batch) + not_skipped_batches = accelerator.gather(untrained_batches) + accelerator.load_state(tmpdir) + resumed_batches = [] + for batch in prepared_dl: + resumed_batches.append(batch) + resumed_batches = accelerator.gather(resumed_batches) + assert len(not_skipped_batches) == len(resumed_batches), ( + f"Expected {len(not_skipped_batches)} batches after resume, got {len(resumed_batches)}" + ) + for b1, b2 in zip(not_skipped_batches, resumed_batches): + for v1, v2 in zip(b1, b2): + assert torch.equal(v1, v2), f"Batch {b1} and {b2} are not equal" + finally: + accelerator.dataloader_config = old_dataloader_config + + +def test_stateful_dataloader_save_state(accelerator): + """ + Tests that a stateful dataloader can be iterated over, saved after a few batches using `Accelerator.save_state`, + and then resumed from the saved state. + + The result should be the same as the rest of the data that iterated over after saving. + """ + _test_stateful_dataloader_save_state_resume(accelerator, iterable=True) + _test_stateful_dataloader_save_state_resume(accelerator, iterable=False) + + +def main(): + accelerator = create_accelerator() + torch.manual_seed(accelerator.process_index) + + accelerator.print("Test that even_batches variable ensures uniform batches across processes") + test_default_ensures_even_batch_sizes() + + accelerator.print("Run tests with even_batches disabled") + test_can_disable_even_batches() + + accelerator.print("Test joining uneven inputs") + test_can_join_uneven_inputs() + + accelerator.print("Test overriding even_batches when joining uneven inputs") + test_join_can_override_even_batches() + + accelerator.print("Test overriding even_batches for mixed dataloader types") + test_join_can_override_for_mixed_type_dataloaders() + + accelerator.print("Test overriding even_batches raises a warning for iterable dataloaders") + test_join_raises_warning_for_iterable_when_overriding_even_batches() + + accelerator.print("Test join with non DDP distributed raises warning") + original_state = accelerator.state.distributed_type + accelerator.state.distributed_type = DistributedType.FSDP + test_join_raises_warning_for_non_ddp_distributed(accelerator) + accelerator.state.distributed_type = original_state + + accelerator.print("Test pickling an accelerator") + test_pickle_accelerator() + + dataset = DummyDataset() + + accelerator.print("Test DataLoader with shuffle=False") + loader = DataLoader(dataset, shuffle=False, batch_size=BATCH_SIZE, num_workers=NUM_WORKERS) + test_data_loader(loader, accelerator) + + accelerator.print("Test DataLoader with shuffle=True") + loader = DataLoader(dataset, shuffle=True, batch_size=BATCH_SIZE, num_workers=NUM_WORKERS) + test_data_loader(loader, accelerator) + + accelerator.print("Test DataLoader with batch_sampler") + sampler = BatchSampler(RandomSampler(dataset), batch_size=BATCH_SIZE, drop_last=False) + loader = DataLoader(dataset, batch_sampler=sampler, num_workers=NUM_WORKERS) + test_data_loader(loader, accelerator) + + accelerator.print("Test DataLoader with sampler as an instance of `BatchSampler`") + sampler = BatchSampler(RandomSampler(dataset), batch_size=BATCH_SIZE, drop_last=False) + loader = DataLoader(dataset, sampler=sampler, batch_size=None, collate_fn=default_collate, num_workers=NUM_WORKERS) + test_data_loader(loader, accelerator) + test_stateful_dataloader(accelerator) + test_stateful_dataloader_save_state(accelerator) + + accelerator.end_training() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_merge_weights.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_merge_weights.py new file mode 100644 index 0000000000000000000000000000000000000000..f280c8fa17919367001389a8efcd78faaa041f0c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_merge_weights.py @@ -0,0 +1,158 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import gc +import logging +import shutil +from pathlib import Path + +import torch +from safetensors.torch import load_file +from torch.distributed.fsdp.fully_sharded_data_parallel import ShardingStrategy, StateDictType +from torch.utils.data import DataLoader + +from accelerate import Accelerator, FullyShardedDataParallelPlugin +from accelerate.commands.merge import merge_command, merge_command_parser +from accelerate.state import AcceleratorState +from accelerate.test_utils import torch_device +from accelerate.test_utils.training import RegressionDataset +from accelerate.utils import merge_fsdp_weights, patch_environment, save_fsdp_model + + +logging.basicConfig(level=logging.INFO) + +parser = merge_command_parser() + + +class TinyModel(torch.nn.Module): + def __init__(self): + super().__init__() + self.linear1 = torch.nn.Linear(16, 16) + self.activation = torch.nn.ReLU() + self.linear2 = torch.nn.Linear(16, 16) + self.softmax = torch.nn.Softmax() + + def forward(self, x): + return self.linear2(self.activation(self.linear1(x))) + + +def setup(): + if AcceleratorState._shared_state != {}: + AcceleratorState()._reset_state() + plugin = FullyShardedDataParallelPlugin( + sharding_strategy=ShardingStrategy.FULL_SHARD, state_dict_type=StateDictType.SHARDED_STATE_DICT + ) + model = TinyModel() + with patch_environment(fsdp_auto_wrap_policy="SIZE_BASED_WRAP"): + plugin.set_auto_wrap_policy(model) + accelerator = Accelerator(fsdp_plugin=plugin) + model = accelerator.prepare(model) + return model, plugin, accelerator + + +def mock_training(accelerator, model): + train_set = RegressionDataset(length=128, seed=42) + train_dl = DataLoader(train_set, batch_size=16, shuffle=False) + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + train_dl, model, optimizer = accelerator.prepare(train_dl, model, optimizer) + for _ in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + accelerator.backward(loss) + optimizer.step() + return model + + +def check_weights(operation, state_1, state_2): + for weight_1, weight_2 in zip(state_1.values(), state_2.values()): + if operation == "same": + assert torch.allclose(weight_1, weight_2) + else: + assert not torch.allclose(weight_1, weight_2) + + +def check_safetensors_weights(path, model): + safe_state_dict = load_file(path / "model.safetensors") + safe_loaded_model = TinyModel().to(torch_device) + check_weights("diff", model.state_dict(), safe_loaded_model.state_dict()) + safe_loaded_model.load_state_dict(safe_state_dict) + check_weights("same", model.state_dict(), safe_loaded_model.state_dict()) + + +def check_pytorch_weights(path, model): + nonsafe_state_dict = torch.load(path / "pytorch_model.bin", weights_only=True) + nonsafe_loaded_model = TinyModel().to(torch_device) + check_weights("diff", model.state_dict(), nonsafe_loaded_model.state_dict()) + nonsafe_loaded_model.load_state_dict(nonsafe_state_dict) + check_weights("same", model.state_dict(), nonsafe_loaded_model.state_dict()) + + +def test_merge_weights_safetensors(model, path): + # Should now be saved at `path/merged.safetensors` + merge_fsdp_weights(path / "pytorch_model_fsdp_0", path, safe_serialization=True) + check_safetensors_weights(path, model) + + +def test_merge_weights_command_safetensors(model, path): + args = parser.parse_args([str(path / "pytorch_model_fsdp_0"), str(path)]) + merge_command(args) + check_safetensors_weights(path, model) + + +def test_merge_weights_pytorch(model, path): + # Should now be saved at `path/merged.bin` + merge_fsdp_weights(path / "pytorch_model_fsdp_0", path, safe_serialization=False) + check_pytorch_weights(path, model) + + +def test_merge_weights_command_pytorch(model, path): + args = parser.parse_args([str(path / "pytorch_model_fsdp_0"), str(path), "--unsafe_serialization"]) + merge_command(args) + check_pytorch_weights(path, model) + + +if __name__ == "__main__": + # Note this test requires at least two accelerators! + model, plugin, accelerator = setup() + if accelerator.num_processes > 1: + try: + # Initial setup for things + out_path = Path("test_merge_weights_fsdp_weights") + if not out_path.exists(): + out_path.mkdir(parents=True, exist_ok=True) + + # Train briefly once weights aren't the baseline + model = mock_training(accelerator, model) + accelerator.wait_for_everyone() + + gc.collect() # Needed for some lingering refs after training + save_fsdp_model(plugin, accelerator, model, out_path) + accelerator.wait_for_everyone() + + # Finally we can test + test_merge_weights_safetensors(model, out_path) + test_merge_weights_command_safetensors(model, out_path) + test_merge_weights_pytorch(model, out_path) + test_merge_weights_command_pytorch(model, out_path) + except Exception: + raise + finally: + # Cleanup in case of any failures + if accelerator.is_main_process: + shutil.rmtree(out_path) + accelerator.wait_for_everyone() + accelerator.end_training() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_notebook.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_notebook.py new file mode 100644 index 0000000000000000000000000000000000000000..bc75a861758e20fcec3196ea75c556679a2632b3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_notebook.py @@ -0,0 +1,125 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test file to ensure that in general certain situational setups for notebooks work. +""" + +import os +import time + +from pytest import mark, raises +from torch.distributed.elastic.multiprocessing.errors import ChildFailedError + +from accelerate import PartialState, notebook_launcher +from accelerate.test_utils import require_bnb +from accelerate.utils import is_bnb_available, is_xpu_available + + +def basic_function(): + # Just prints the PartialState + print(f"PartialState:\n{PartialState()}") + + +def tough_nut_function(queue): + if queue.empty(): + return + trial = queue.get() + if trial > 0: + queue.put(trial - 1) + raise RuntimeError("The nut hasn't cracked yet! Try again.") + + print(f"PartialState:\n{PartialState()}") + + +def bipolar_sleep_function(sleep_sec: int): + state = PartialState() + if state.process_index % 2 == 0: + raise RuntimeError("I'm an even process. I don't like to sleep.") + else: + time.sleep(sleep_sec) + + +NUM_PROCESSES = int(os.environ.get("ACCELERATE_NUM_PROCESSES", 1)) + + +def test_can_initialize(): + notebook_launcher(basic_function, (), num_processes=NUM_PROCESSES) + + +@mark.skipif(NUM_PROCESSES < 2, reason="Need at least 2 processes to test static rendezvous backends") +def test_static_rdzv_backend(): + notebook_launcher(basic_function, (), num_processes=NUM_PROCESSES, rdzv_backend="static") + + +@mark.skipif(NUM_PROCESSES < 2, reason="Need at least 2 processes to test c10d rendezvous backends") +def test_c10d_rdzv_backend(): + notebook_launcher(basic_function, (), num_processes=NUM_PROCESSES, rdzv_backend="c10d") + + +@mark.skipif(NUM_PROCESSES < 2, reason="Need at least 2 processes to test fault tolerance") +def test_fault_tolerant(max_restarts: int = 3): + # Use torch.multiprocessing to get the right context for the current device + import torch.multiprocessing as mp + + # Get appropriate context - 'spawn' for XPU, 'fork' for others + if is_xpu_available(): + ctx = mp.get_context("spawn") + else: + ctx = mp.get_context("fork") + queue = ctx.Queue() + queue.put(max_restarts) + notebook_launcher(tough_nut_function, (queue,), num_processes=NUM_PROCESSES, max_restarts=max_restarts) + + +@mark.skipif(NUM_PROCESSES < 2, reason="Need at least 2 processes to test monitoring") +def test_monitoring(monitor_interval: float = 0.01, sleep_sec: int = 100): + start_time = time.time() + with raises(ChildFailedError, match="I'm an even process. I don't like to sleep."): + notebook_launcher( + bipolar_sleep_function, + (sleep_sec,), + num_processes=NUM_PROCESSES, + monitor_interval=monitor_interval, + ) + assert time.time() - start_time < sleep_sec, "Monitoring did not stop the process in time." + + +@require_bnb +def test_problematic_imports(): + with raises(RuntimeError, match="Please keep these imports"): + import bitsandbytes as bnb # noqa: F401 + + notebook_launcher(basic_function, (), num_processes=NUM_PROCESSES) + + +def main(): + print("Test basic notebook can be ran") + test_can_initialize() + print("Test static rendezvous backend") + test_static_rdzv_backend() + print("Test c10d rendezvous backend") + test_c10d_rdzv_backend() + print("Test fault tolerant") + test_fault_tolerant() + print("Test monitoring") + test_monitoring() + if is_bnb_available(): + print("Test problematic imports (bnb)") + test_problematic_imports() + if NUM_PROCESSES > 1: + PartialState().destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ops.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ops.py new file mode 100644 index 0000000000000000000000000000000000000000..1aa9b095e257bf63623b5127624b54c4a376c5ff --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_ops.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python + +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import torch + +from accelerate import PartialState +from accelerate.test_utils.testing import assert_exception +from accelerate.utils.dataclasses import DistributedType +from accelerate.utils.operations import ( + DistributedOperationException, + broadcast, + copy_tensor_to_devices, + gather, + gather_object, + pad_across_processes, + reduce, +) + + +def create_tensor(state): + return (torch.arange(state.num_processes) + 1.0 + (state.num_processes * state.process_index)).to(state.device) + + +def test_gather(state): + tensor = create_tensor(state) + gathered_tensor = gather(tensor) + assert gathered_tensor.tolist() == list(range(1, state.num_processes**2 + 1)) + + +def test_gather_object(state): + # Gather objects in TorchXLA is not supported. + if state.distributed_type == DistributedType.XLA: + return + obj = [state.process_index] + gathered_obj = gather_object(obj) + assert len(gathered_obj) == state.num_processes, f"{gathered_obj}, {len(gathered_obj)} != {state.num_processes}" + assert gathered_obj == list(range(state.num_processes)), f"{gathered_obj} != {list(range(state.num_processes))}" + + +def test_gather_non_contiguous(state): + # Skip this test because the 'is_contiguous' function of XLA tensor always returns True. + if state.distributed_type == DistributedType.XLA: + return + + # Create a non-contiguous tensor (enforce non-contiguity after device memory allocation) + tensor = torch.arange(12, device=state.device).view(4, 3).t() + assert not tensor.is_contiguous() + # Shouldn't error out + _ = gather(tensor) + + +def test_broadcast(state): + tensor = create_tensor(state) + broadcasted_tensor = broadcast(tensor) + assert broadcasted_tensor.shape == torch.Size([state.num_processes]) + assert broadcasted_tensor.tolist() == list(range(1, state.num_processes + 1)) + + +def test_pad_across_processes(state): + # We need to pad the tensor with one more element if we are the main process + # to ensure that we can pad + if state.is_main_process: + tensor = torch.arange(state.num_processes + 1).to(state.device) + else: + tensor = torch.arange(state.num_processes).to(state.device) + padded_tensor = pad_across_processes(tensor) + assert padded_tensor.shape == torch.Size([state.num_processes + 1]) + if not state.is_main_process: + assert padded_tensor.tolist() == list(range(0, state.num_processes)) + [0] + + +def test_reduce_sum(state): + # For now runs on only two processes + if state.num_processes != 2: + return + tensor = create_tensor(state) + reduced_tensor = reduce(tensor, "sum") + truth_tensor = torch.tensor([4.0, 6]).to(state.device) + assert torch.allclose(reduced_tensor, truth_tensor), f"{reduced_tensor} != {truth_tensor}" + + +def test_reduce_mean(state): + # For now runs on only two processes + if state.num_processes != 2: + return + tensor = create_tensor(state) + reduced_tensor = reduce(tensor, "mean") + truth_tensor = torch.tensor([2.0, 3]).to(state.device) + assert torch.allclose(reduced_tensor, truth_tensor), f"{reduced_tensor} != {truth_tensor}" + + +def test_op_checker(state): + # Must be in a distributed state, and gathering is currently not supported in TorchXLA. + if state.distributed_type in [DistributedType.NO, DistributedType.XLA]: + return + state.debug = True + # `pad_across_processes` + if state.process_index == 0: + data = {"tensor": torch.tensor([[0.0, 1, 2, 3, 4]]).to(state.device)} + else: + data = {"tensor": torch.tensor([[[0.0, 1, 2, 3, 4, 5]]]).to(state.device)} + + with assert_exception(DistributedOperationException): + pad_across_processes(data, dim=0) + + # `reduce` + if state.process_index == 0: + data = {"tensor": torch.tensor([[0.0, 1, 2, 3, 4]]).to(state.device)} + else: + data = {"tensor": torch.tensor([[[0.0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]]).to(state.device)} + + with assert_exception(DistributedOperationException): + reduce(data) + + # `broadcast` + if state.process_index == 0: + data = {"tensor": torch.tensor([[0.0, 1, 2, 3, 4]]).to(state.device)} + else: + data = {"tensor": torch.tensor([[[0.0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]]).to(state.device)} + + with assert_exception(DistributedOperationException): + broadcast(data) + + state.debug = False + + +def test_copy_tensor_to_devices(state): + if state.distributed_type not in [DistributedType.MULTI_GPU, DistributedType.XLA]: + return + if state.is_main_process: + tensor = torch.tensor([1, 2, 3], dtype=torch.int).to(state.device) + else: + tensor = None + tensor = copy_tensor_to_devices(tensor) + assert torch.allclose(tensor, torch.tensor([1, 2, 3], dtype=torch.int, device=state.device)) + + +def _mp_fn(index): + # For xla_spawn (TPUs) + main() + + +def main(): + state = PartialState() + state.print(f"State: {state}") + state.print("testing gather") + test_gather(state) + state.print("testing gather_object") + test_gather_object(state) + state.print("testing gather non-contiguous") + test_gather_non_contiguous(state) + state.print("testing broadcast") + test_broadcast(state) + state.print("testing pad_across_processes") + test_pad_across_processes(state) + state.print("testing reduce_sum") + test_reduce_sum(state) + state.print("testing reduce_mean") + test_reduce_mean(state) + state.print("testing op_checker") + test_op_checker(state) + state.print("testing sending tensors across devices") + test_copy_tensor_to_devices(state) + state.destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_script.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_script.py new file mode 100644 index 0000000000000000000000000000000000000000..e53e12a18fa79c221640e2e73bdf83c552f66bfa --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_script.py @@ -0,0 +1,909 @@ +#!/usr/bin/env python + +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import io +import math +import time +from copy import deepcopy +from pathlib import Path + +import numpy as np +import torch +from torch.utils.data import DataLoader, Dataset + +from accelerate import Accelerator +from accelerate.data_loader import SeedableRandomSampler, prepare_data_loader +from accelerate.state import AcceleratorState +from accelerate.test_utils import RegressionDataset, RegressionModel, are_the_same_tensors +from accelerate.utils import ( + DataLoaderConfiguration, + DistributedType, + gather, + gather_object, + is_bf16_available, + is_cuda_available, + is_datasets_available, + is_fp16_available, + is_hpu_available, + is_mps_available, + is_pytest_available, + set_seed, + synchronize_rng_states, +) + + +if is_hpu_available(): + ATOL = 1e-3 + RTOL = 1e-3 +else: + ATOL = 1e-6 + RTOL = 1e-6 + + +def generate_baseline_dataloader(train_set, generator, batch_size, use_seedable_sampler=False): + "Creates a dataloader that can also use the `SeedableRandomSampler`" + if use_seedable_sampler: + # The SeedableRandomSampler is needed during distributed setups + # for full reproducibility across processes with the `DataLoader` + sampler = SeedableRandomSampler( + generator=generator, + data_source=train_set, + num_samples=len(train_set), + ) + return DataLoader(train_set, batch_size=batch_size, sampler=sampler) + else: + return DataLoader(train_set, batch_size=batch_size, shuffle=True, generator=generator) + + +def print_main(state): + print(f"Printing from the main process {state.process_index}") + + +def print_local_main(state): + print(f"Printing from the local main process {state.local_process_index}") + + +def print_last(state): + print(f"Printing from the last process {state.process_index}") + + +def print_on(state, process_idx): + print(f"Printing from process {process_idx}: {state.process_index}") + + +def process_execution_check(): + accelerator = Accelerator() + num_processes = accelerator.num_processes + # Test main_process_first context manager + path = Path("check_main_process_first.txt") + with accelerator.main_process_first(): + if accelerator.is_main_process: + time.sleep(0.1) # ensure main process takes longest + with open(path, "a+") as f: + f.write("Currently in the main process\n") + else: + with open(path, "a+") as f: + f.write("Now on another process\n") + accelerator.wait_for_everyone() + + if accelerator.is_main_process: + with open(path) as f: + text = "".join(f.readlines()) + try: + assert text.startswith("Currently in the main process\n"), "Main process was not first" + if num_processes > 1: + assert text.endswith("Now on another process\n"), "Main process was not first" + assert text.count("Now on another process\n") == accelerator.num_processes - 1, ( + f"Only wrote to file {text.count('Now on another process') + 1} times, not {accelerator.num_processes}" + ) + except AssertionError: + path.unlink() + raise + + if accelerator.is_main_process and path.exists(): + path.unlink() + accelerator.wait_for_everyone() + # Test the decorators + f = io.StringIO() + with contextlib.redirect_stdout(f): + accelerator.on_main_process(print_main)(accelerator.state) + result = f.getvalue().rstrip() + if accelerator.is_main_process: + assert result == "Printing from the main process 0", f"{result} != Printing from the main process 0" + else: + assert f.getvalue().rstrip() == "", f'{result} != ""' + f.truncate(0) + f.seek(0) + + with contextlib.redirect_stdout(f): + accelerator.on_local_main_process(print_local_main)(accelerator.state) + if accelerator.is_local_main_process: + assert f.getvalue().rstrip() == "Printing from the local main process 0" + else: + assert f.getvalue().rstrip() == "" + f.truncate(0) + f.seek(0) + + with contextlib.redirect_stdout(f): + accelerator.on_last_process(print_last)(accelerator.state) + if accelerator.is_last_process: + assert f.getvalue().rstrip() == f"Printing from the last process {accelerator.state.num_processes - 1}" + else: + assert f.getvalue().rstrip() == "" + f.truncate(0) + f.seek(0) + + for process_idx in range(num_processes): + with contextlib.redirect_stdout(f): + accelerator.on_process(print_on, process_index=process_idx)(accelerator.state, process_idx) + if accelerator.process_index == process_idx: + assert f.getvalue().rstrip() == f"Printing from process {process_idx}: {accelerator.process_index}" + else: + assert f.getvalue().rstrip() == "" + f.truncate(0) + f.seek(0) + + +def init_state_check(): + # Test we can instantiate this twice in a row. + state = AcceleratorState() + if state.local_process_index == 0: + print("Testing, testing. 1, 2, 3.") + print(state) + + +def rng_sync_check(): + state = AcceleratorState() + synchronize_rng_states(["torch"]) + assert are_the_same_tensors(torch.get_rng_state()), "RNG states improperly synchronized on CPU." + if state.distributed_type == DistributedType.MULTI_GPU: + synchronize_rng_states(["cuda"]) + assert are_the_same_tensors(torch.cuda.get_rng_state()), "RNG states improperly synchronized on GPU." + elif state.distributed_type == DistributedType.MULTI_XPU: + synchronize_rng_states(["xpu"]) + assert are_the_same_tensors(torch.xpu.get_rng_state()), "RNG states improperly synchronized on XPU." + generator = torch.Generator() + synchronize_rng_states(["generator"], generator=generator) + assert are_the_same_tensors(generator.get_state()), "RNG states improperly synchronized in generator." + + if state.local_process_index == 0: + print("All rng are properly synched.") + + +def dl_preparation_check(): + state = AcceleratorState() + length = 32 * state.num_processes + + dl = DataLoader(range(length), batch_size=8) + dl = prepare_data_loader(dl, state.device, state.num_processes, state.process_index, put_on_device=True) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result) + + assert torch.equal(result.cpu(), torch.arange(0, length).long()), "Wrong non-shuffled dataloader result." + + dl = DataLoader(range(length), batch_size=8) + dl = prepare_data_loader( + dl, + state.device, + state.num_processes, + state.process_index, + put_on_device=True, + split_batches=True, + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result) + assert torch.equal(result.cpu(), torch.arange(0, length).long()), "Wrong non-shuffled dataloader result." + + if state.process_index == 0: + print("Non-shuffled dataloader passing.") + + dl = DataLoader(range(length), batch_size=8, shuffle=True) + dl = prepare_data_loader(dl, state.device, state.num_processes, state.process_index, put_on_device=True) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result).tolist() + result.sort() + assert result == list(range(length)), "Wrong shuffled dataloader result." + + dl = DataLoader(range(length), batch_size=8, shuffle=True) + dl = prepare_data_loader( + dl, + state.device, + state.num_processes, + state.process_index, + put_on_device=True, + split_batches=True, + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result).tolist() + result.sort() + assert result == list(range(length)), "Wrong shuffled dataloader result." + + if state.local_process_index == 0: + print("Shuffled dataloader passing.") + + +def central_dl_preparation_check(): + state = AcceleratorState() + length = 32 * state.num_processes + + dl = DataLoader(range(length), batch_size=8) + dl = prepare_data_loader( + dl, state.device, state.num_processes, state.process_index, put_on_device=True, dispatch_batches=True + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result) + assert torch.equal(result.cpu(), torch.arange(0, length).long()), "Wrong non-shuffled dataloader result." + + dl = DataLoader(range(length), batch_size=8) + dl = prepare_data_loader( + dl, + state.device, + state.num_processes, + state.process_index, + put_on_device=True, + split_batches=True, + dispatch_batches=True, + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result) + assert torch.equal(result.cpu(), torch.arange(0, length).long()), "Wrong non-shuffled dataloader result." + + if state.process_index == 0: + print("Non-shuffled central dataloader passing.") + + dl = DataLoader(range(length), batch_size=8, shuffle=True) + dl = prepare_data_loader( + dl, state.device, state.num_processes, state.process_index, put_on_device=True, dispatch_batches=True + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result).tolist() + result.sort() + assert result == list(range(length)), "Wrong shuffled dataloader result." + + dl = DataLoader(range(length), batch_size=8, shuffle=True) + dl = prepare_data_loader( + dl, + state.device, + state.num_processes, + state.process_index, + put_on_device=True, + split_batches=True, + dispatch_batches=True, + ) + result = [] + for batch in dl: + result.append(gather(batch)) + result = torch.cat(result).tolist() + result.sort() + assert result == list(range(length)), "Wrong shuffled dataloader result." + + if state.local_process_index == 0: + print("Shuffled central dataloader passing.") + + +def custom_sampler_check(): + state = AcceleratorState() + + class CustomDataset(Dataset): + def __init__(self, data): + self.data = data + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + return self.data[index] + + class CustomBatchSampler: + def __init__(self, dataset_length: int, batch_size: int, shuffle: bool = True): + self.batch_size = batch_size + self.data_index = np.arange(dataset_length) + self.shuffle = shuffle + + def __iter__(self): + num_batches = len(self) + if self.shuffle: + index = np.random.permutation(self.data_index) + else: + index = self.data_index + output = np.array_split(index, num_batches) + yield from output + + def __len__(self): + return math.ceil(len(self.data_index) / self.batch_size) + + dataset = CustomDataset(range(32 * state.num_processes)) + sampler = CustomBatchSampler(len(dataset), batch_size=8) + dl = DataLoader(dataset, batch_sampler=sampler) + dl = prepare_data_loader(dl, state.device, state.num_processes, state.process_index) + # We need just ensure that `dl.batch_sampler` (or `dl.batch_sampler.batch_sampler` is indeed the old batch sampler + if hasattr(dl.batch_sampler, "batch_sampler"): + assert isinstance(dl.batch_sampler.batch_sampler, CustomBatchSampler), ( + "Custom sampler was changed after calling `prepare_data_loader`" + ) + else: + assert isinstance(dl.batch_sampler, CustomBatchSampler), ( + "Custom sampler was changed after calling `prepare_data_loader`" + ) + + +def check_seedable_sampler(): + # Set seed + set_seed(42) + train_set = RegressionDataset(length=10, seed=42) + train_dl = DataLoader(train_set, batch_size=2, shuffle=True) + + config = DataLoaderConfiguration(use_seedable_sampler=True) + accelerator = Accelerator(dataloader_config=config) + train_dl = accelerator.prepare(train_dl) + original_items = [] + for _ in range(3): + for batch in train_dl: + original_items.append(batch["x"]) + original_items = torch.cat(original_items) + + # Set seed again and the epoch + set_seed(42) + train_dl.set_epoch(0) + new_items = [] + for _ in range(3): + for batch in train_dl: + new_items.append(batch["x"]) + new_items = torch.cat(new_items) + assert torch.allclose(original_items, new_items), "Did not obtain the same items with the same seed and epoch." + + +def check_seedable_sampler_in_batch_sampler_shard(): + set_seed(42) + + config = DataLoaderConfiguration(use_seedable_sampler=True) + accelerator = Accelerator(dataloader_config=config) + assert accelerator.num_processes > 1, "This test requires more than one process." + + dataloader = DataLoader(list(range(10)), batch_size=1, shuffle=True) + prepared_data_loader = prepare_data_loader( + dataloader=dataloader, + use_seedable_sampler=True, + ) + + target_sampler = prepared_data_loader.batch_sampler.batch_sampler.sampler + assert isinstance(target_sampler, SeedableRandomSampler), ( + "Sampler in BatchSamplerShard is not SeedableRandomSampler." + ) + + +def check_seedable_sampler_with_data_seed(): + # Set seed + set_seed(42) + data_seed = 42 + train_set = RegressionDataset(length=10, seed=42) + train_dl = DataLoader(train_set, batch_size=2, shuffle=True) + + config = DataLoaderConfiguration(use_seedable_sampler=True, data_seed=data_seed) + accelerator = Accelerator(dataloader_config=config) + prepared_dl = accelerator.prepare(train_dl) + original_items = [] + for _ in range(3): + for batch in prepared_dl: + original_items.append(batch["x"]) + original_items = torch.cat(original_items) + + # Set new data seed + config.data_seed = 43 + accelerator = Accelerator(dataloader_config=config) + prepared_dl = accelerator.prepare(train_dl) + new_items = [] + for _ in range(3): + for batch in prepared_dl: + new_items.append(batch["x"]) + new_items = torch.cat(new_items) + assert not torch.allclose(original_items, new_items), "Obtained the same items with different data seed." + + +def mock_training(length, batch_size, generator, use_seedable_sampler=False): + set_seed(42) + generator.manual_seed(42) + train_set = RegressionDataset(length=length, seed=42) + + train_dl = generate_baseline_dataloader(train_set, generator, batch_size, use_seedable_sampler) + model = RegressionModel() + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + for epoch in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + loss.backward() + optimizer.step() + return train_set, model + + +def training_check(use_seedable_sampler=False): + state = AcceleratorState() + generator = torch.Generator() + batch_size = 8 + length = batch_size * 4 * state.num_processes + + train_set, old_model = mock_training(length, batch_size * state.num_processes, generator, use_seedable_sampler) + assert are_the_same_tensors(old_model.a), "Did not obtain the same model on both processes." + assert are_the_same_tensors(old_model.b), "Did not obtain the same model on both processes." + + accelerator = Accelerator() + train_dl = generate_baseline_dataloader(train_set, generator, batch_size, use_seedable_sampler) + model = RegressionModel() + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + train_dl, model, optimizer = accelerator.prepare(train_dl, model, optimizer) + set_seed(42) + generator.manual_seed(42) + for _ in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + accelerator.backward(loss) + optimizer.step() + + model = accelerator.unwrap_model(model).cpu() + torch.testing.assert_close( + old_model.a, + model.a, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + torch.testing.assert_close( + old_model.b, + model.b, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + + accelerator.print("Training yielded the same results on one CPU or distributed setup with no batch split.") + + dataloader_config = DataLoaderConfiguration(split_batches=True, use_seedable_sampler=use_seedable_sampler) + accelerator = Accelerator(dataloader_config=dataloader_config) + train_dl = generate_baseline_dataloader( + train_set, generator, batch_size * state.num_processes, use_seedable_sampler + ) + model = RegressionModel() + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + train_dl, model, optimizer = accelerator.prepare(train_dl, model, optimizer) + set_seed(42) + generator.manual_seed(42) + for _ in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + accelerator.backward(loss) + optimizer.step() + + model = accelerator.unwrap_model(model).cpu() + torch.testing.assert_close( + old_model.a, + model.a, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + torch.testing.assert_close( + old_model.b, + model.b, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + + accelerator.print("Training yielded the same results on one CPU or distributed setup with batch split.") + + # FP32 wrapper check + if is_cuda_available() or is_mps_available(): + # Mostly a test that model.forward will have autocast when running unwrap_model(model, keep_fp32_wrapper=True) + print("Keep fp32 wrapper check.") + AcceleratorState._reset_state() + accelerator = Accelerator(mixed_precision="fp16") + + model = torch.nn.Linear(2, 4) + model = accelerator.prepare(model) + model_with_fp32_wrapper = accelerator.unwrap_model(model, keep_fp32_wrapper=True) + + # Run forward with fp16 as input. + # When the model is with mixed precision wrapper, no error will be raised. + input_tensor = torch.Tensor([1, 2]).to(dtype=torch.float16, device=accelerator.device) + output = model_with_fp32_wrapper(input_tensor) + + # BF16 support + if is_bf16_available(): + # Mostly a test that BF16 doesn't crash as the operation inside the model is not converted to BF16 + print("BF16 training check.") + AcceleratorState._reset_state() + dataloader_config = DataLoaderConfiguration(use_seedable_sampler=use_seedable_sampler) + accelerator = Accelerator(mixed_precision="bf16", dataloader_config=dataloader_config) + train_dl = generate_baseline_dataloader(train_set, generator, batch_size, use_seedable_sampler) + model = RegressionModel() + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + train_dl, model, optimizer = accelerator.prepare(train_dl, model, optimizer) + set_seed(42) + generator.manual_seed(42) + for _ in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + accelerator.backward(loss) + optimizer.step() + + model = accelerator.unwrap_model(model).cpu() + torch.testing.assert_close( + old_model.a, + model.a, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + torch.testing.assert_close( + old_model.b, + model.b, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + + # FP16 support (HPU fp16 model seems to be off by 10% from the CPU, which is a lot of numerical error) + if is_fp16_available() and not is_hpu_available(): + # Mostly a test that FP16 doesn't crash as the operation inside the model is not converted to FP16 + print("FP16 training check.") + AcceleratorState._reset_state() + dataloader_config = DataLoaderConfiguration(use_seedable_sampler=use_seedable_sampler) + accelerator = Accelerator(mixed_precision="fp16", dataloader_config=dataloader_config) + train_dl = generate_baseline_dataloader(train_set, generator, batch_size, use_seedable_sampler) + model = RegressionModel() + optimizer = torch.optim.SGD(model.parameters(), lr=0.1) + + train_dl, model, optimizer = accelerator.prepare(train_dl, model, optimizer) + set_seed(42) + generator.manual_seed(42) + for _ in range(3): + for batch in train_dl: + model.zero_grad() + output = model(batch["x"]) + loss = torch.nn.functional.mse_loss(output, batch["y"]) + accelerator.backward(loss) + optimizer.step() + + model = accelerator.unwrap_model(model).cpu() + torch.testing.assert_close( + old_model.a, + model.a, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + torch.testing.assert_close( + old_model.b, + model.b, + atol=ATOL, + rtol=RTOL, + msg=lambda msg: f"Did not obtain the same model on CPU or distributed training.\n{msg}", + ) + + +def test_split_between_processes_dataset(datasets_Dataset): + state = AcceleratorState() + data = datasets_Dataset.from_list([dict(k=v) for v in range(2 * state.num_processes)]) + with state.split_between_processes(data, apply_padding=False) as results: + assert len(results) == 2, ( + f"Each process did not have two items. Process index: {state.process_index}; Length: {len(results)}" + ) + + data = datasets_Dataset.from_list([dict(k=v) for v in range(2 * state.num_processes - 1)]) + with state.split_between_processes(data, apply_padding=False) as results: + if state.is_last_process: + assert len(results) == 1, ( + f"Last process did not receive a single item. Process index: {state.process_index}; Length: {len(results)}" + ) + else: + assert len(results) == 2, ( + f"One of the intermediate processes did not receive two items. Process index: {state.process_index}; Length: {len(results)}" + ) + state.wait_for_everyone() + + odd_data = datasets_Dataset.from_list([dict(k=v) for v in range(2 * state.num_processes - 1)]) + even_data = datasets_Dataset.from_list([dict(k=v) for v in range(2 * state.num_processes)]) + + for data in [odd_data, even_data]: + expected_output = data["k"] + + with state.split_between_processes(data, apply_padding=True) as results: + if state.num_processes == 1: + assert len(results) == len(data), ( + f"Single process did not receive all items. Process index: {state.process_index}; Length: {len(results)}" + ) + else: + assert len(results) == 2, ( + f"Each process did not have two items. Process index: {state.process_index}; Length: {len(results)}" + ) + + results_per_process = [] + for result in results: + results_per_process.append(result) + + state.wait_for_everyone() + + gathered_results = gather_object(results_per_process) + output = [r["k"] for r in gathered_results[: len(data)]] + + assert expected_output == output, f"Gathered results is incorrect. Expected: {expected_output}; Got: {output}" + + +def test_split_between_processes_list(): + state = AcceleratorState() + data = list(range(0, 2 * state.num_processes)) + with state.split_between_processes(data) as results: + assert len(results) == 2, ( + f"Each process did not have two items. Process index: {state.process_index}; Length: {len(results)}" + ) + state.wait_for_everyone() + + even_data = list(range(0, (2 * state.num_processes))) + odd_data = list(range(0, (2 * state.num_processes) - 1)) + for data in [odd_data, even_data]: + expected_output = data + + with state.split_between_processes(data, apply_padding=True) as results: + num_samples_per_device = math.ceil(len(data) / state.num_processes) + # Test all processes gets the correct number of item(s) + assert len(results) == num_samples_per_device, ( + f"Process {state.device} did not get the correct number of item(s). Process index: {state.process_index}; Length: {len(results)}" + ) + + results_per_process = [] + for result in results: + results_per_process.append(result) + + state.wait_for_everyone() + + gathered_results = gather_object(results_per_process) + output = gathered_results[: len(data)] + + assert expected_output == output, f"Gathered results is incorrect. Expected: {expected_output}; Got: {output}" + + +def test_split_between_processes_nested_dict(): + state = AcceleratorState() + a = [1, 2, 3, 4, 5, 6, 7, 8] + b = ["a", "b", "c", "d", "e", "f", "g", "h"] + c = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]) + if state.num_processes in (1, 2, 4): + data = {"a": a, "b": b, "c": c} + data_copy = deepcopy(data) + with state.split_between_processes(data) as results: + if state.process_index == 0: + assert results["a"] == data_copy["a"][: 8 // state.num_processes] + elif state.num_processes == 2: + assert results["a"] == data_copy["a"][4:] + elif state.process_index == 3: + # We return a list each time + assert results["a"] == data_copy["a"][-2:], f"Expected: {data_copy['a'][-2]}, Actual: {results['a']}" + if state.process_index == 0: + assert results["b"] == data_copy["b"][: 8 // state.num_processes] + elif state.num_processes == 2: + assert results["b"] == data_copy["b"][4:] + elif state.process_index == 3: + assert results["b"] == data_copy["b"][-2:] + if state.process_index == 0: + assert torch.allclose(results["c"], data_copy["c"][: 8 // state.num_processes]), ( + f"Did not obtain expected values on process 0, expected `{data['c'][: 8 // state.num_processes]}`, received: {results['c']}" + ) + elif state.num_processes == 2: + assert torch.allclose(results["c"], data_copy["c"][4:]), ( + f"Did not obtain expected values on process 2, expected `{data['c'][4:]}`, received: {results['c']}" + ) + elif state.process_index == 3: + assert torch.allclose(results["c"], data_copy["c"][-2:]), ( + f"Did not obtain expected values on process 4, expected `{data['c'][-2:]}`, received: {results['c']}" + ) + + state.wait_for_everyone() + + +def test_split_between_processes_tensor(): + state = AcceleratorState() + if state.num_processes > 1: + data = torch.tensor([[0, 1, 2, 3], [4, 5, 6, 7]]).to(state.device) + with state.split_between_processes(data) as results: + if state.process_index == 0: + expected = torch.tensor([[0, 1, 2, 3]]).to(state.device) + else: + expected = torch.tensor([[4, 5, 6, 7]]).to(state.device) + torch.testing.assert_close(results, expected) + state.wait_for_everyone() + + even_data = torch.tensor([[i] for i in range(2 * state.num_processes)]).to(state.device) + odd_data = torch.tensor([[i] for i in range(2 * state.num_processes - 1)]).to(state.device) + for data in [even_data, odd_data]: + expected_output = [torch.tensor(i) for i in data.tolist()] + + with state.split_between_processes(data, apply_padding=True) as results: + num_samples_per_device = math.ceil(len(data) / state.num_processes) + assert len(results) == num_samples_per_device, ( + f"Process {state.device} did not get the correct number of item(s). Process index: {state.process_index}; Length: {len(results)}" + ) + results_per_process = [] + for result in results: + results_per_process.append(result.to("cpu")) + + state.wait_for_everyone() + + gathered_results = gather_object(results_per_process) + output = gathered_results[: len(data)] + + assert expected_output == output, f"Gathered results is incorrect. Expected: {expected_output}; Got: {output}" + + +def test_split_between_processes_evenly(): + state = AcceleratorState() + if state.num_processes in (1, 2, 4, 8): + data = list(range(17)) + num_samples_per_process = len(data) // state.num_processes + num_extras = len(data) % state.num_processes + with state.split_between_processes(data) as results: + if state.process_index < num_extras: + assert len(results) == num_samples_per_process + 1, ( + f"Each Process should have even elements. Expected: {num_samples_per_process + 1}, Actual: {len(results)}" + ) + else: + assert len(results) == num_samples_per_process, ( + f"Each Process should have even elements. Expected: {num_samples_per_process}, Actual: {len(results)}" + ) + state.wait_for_everyone() + + +def test_trigger(): + accelerator = Accelerator() + # should start with being false + assert accelerator.check_trigger() is False + + # set a breakpoint on the main process + if accelerator.is_main_process: + accelerator.set_trigger() + + # check it's been activated across all processes + # calls `all_reduce` and triggers a sync + assert accelerator.check_trigger() is True + + # check it's been reset after the sync + assert accelerator.check_trigger() is False + + +def test_reinstantiated_state(): + import pytest + + AcceleratorState._reset_state() + simple_model = torch.nn.Linear(1, 1) + # First define an accelerator + accelerator = Accelerator() + # Then call `reset_state`, breaking the state existing in the accelerator + AcceleratorState._reset_state() + # Now try and prepare a simple model, should raise the custom error early + with pytest.raises(AttributeError) as cm: + accelerator.prepare(simple_model) + assert "`AcceleratorState` object has no attribute" in str(cm.value.args[0]) + assert "This happens if `AcceleratorState._reset_state()`" in str(cm.value.args[0]) + + +def main(): + accelerator = Accelerator() + state = accelerator.state + if state.local_process_index == 0: + print("**Initialization**") + init_state_check() + state.wait_for_everyone() + + if state.distributed_type == DistributedType.MULTI_GPU: + num_processes_per_node = torch.cuda.device_count() + else: + num_processes_per_node = state.num_processes + + # We only run this test on non-multinode + if num_processes_per_node == state.num_processes: + if state.process_index == 0: + print("\n**Test process execution**") + process_execution_check() + + if state.process_index == 0: + print("\n**Test split between processes as a list**") + test_split_between_processes_list() + + if state.process_index == 0: + print("\n**Test split between processes as a dict**") + test_split_between_processes_nested_dict() + + if state.process_index == 0: + print("\n**Test split between processes as a tensor**") + test_split_between_processes_tensor() + + if state.process_index == 0: + print("\n**Test split between processes evenly**") + test_split_between_processes_evenly() + + if state.process_index == 0: + print("\n**Test split between processes as a datasets.Dataset**") + if is_datasets_available(): + from datasets import Dataset as datasets_Dataset + + test_split_between_processes_dataset(datasets_Dataset) + else: + print("Skipped because Hugging Face datasets is not available") + + if state.local_process_index == 0: + print("\n**Test random number generator synchronization**") + rng_sync_check() + + if state.local_process_index == 0: + print("\n**DataLoader integration test**") + dl_preparation_check() + if state.distributed_type != DistributedType.XLA: + central_dl_preparation_check() + custom_sampler_check() + check_seedable_sampler() + check_seedable_sampler_with_data_seed() + + if state.num_processes > 1: + check_seedable_sampler_in_batch_sampler_shard() + + # Trainings are not exactly the same in DeepSpeed and CPU mode + if state.distributed_type == DistributedType.DEEPSPEED: + return + + if state.local_process_index == 0: + print("\n**Training integration test**") + training_check(use_seedable_sampler=False) + training_check(use_seedable_sampler=True) + + if state.local_process_index == 0: + print("\n**Breakpoint trigger test**") + test_trigger() + + if is_pytest_available(): + if state.local_process_index == 0: + print("\n**Test reinstantiated state**") + test_reinstantiated_state() + + state.destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_sync.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_sync.py new file mode 100644 index 0000000000000000000000000000000000000000..310e46e1920466fdf57866919472f619a41f5b33 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/scripts/test_sync.py @@ -0,0 +1,413 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from copy import deepcopy + +import torch +import torch.nn.functional as F +from torch.optim import AdamW +from torch.optim.lr_scheduler import LambdaLR +from torch.utils.data import DataLoader + +from accelerate.accelerator import Accelerator, DataLoaderConfiguration, GradientAccumulationPlugin +from accelerate.state import GradientState +from accelerate.test_utils import RegressionDataset, RegressionModel +from accelerate.utils import DistributedType, set_seed + + +def check_model_parameters(model_a, model_b, did_step, iteration, **kwargs): + for param, grad_param in zip(model_a.parameters(), model_b.parameters()): + if not param.requires_grad: + continue + if not did_step: + # Grads should not be in sync + assert torch.allclose(param.grad, grad_param.grad, **kwargs) is False, ( + f"Gradients in sync when they should not be at iteration {iteration}:\nmodel_a grad ({param.grad}) == model_b grad ({grad_param.grad})" + ) + else: + # Grads should be in sync + assert torch.allclose(param.grad, grad_param.grad, **kwargs) is True, ( + f"Gradients not in sync when they should be at iteration {iteration}:\nmodel_a grad ({param.grad}) != model_b grad ({grad_param.grad})" + ) + + +def step_model(model, input, target, accelerator, do_backward=True): + model.train() + output = model(input) + loss = F.mse_loss(output, target.to(output.device)) + if not do_backward: + loss /= accelerator.gradient_accumulation_steps + loss.backward() + else: + accelerator.backward(loss) + + +def get_training_setup(accelerator, sched=False): + "Returns everything needed to perform basic training" + set_seed(42) + model = RegressionModel() + ddp_model = deepcopy(model) + dset = RegressionDataset(length=80) + dataloader = DataLoader(dset, batch_size=16) + model.to(accelerator.device) + if sched: + opt = AdamW(params=model.parameters(), lr=1e-3) + ddp_opt = AdamW(params=ddp_model.parameters(), lr=1e-3) + sched = LambdaLR(opt, lr_lambda=lambda epoch: epoch**0.65) + ddp_sched = LambdaLR(ddp_opt, lr_lambda=lambda epoch: epoch**0.65) + # Make a copy of `model` + if sched: + ddp_model, ddp_opt, ddp_sched, dataloader = accelerator.prepare(ddp_model, ddp_opt, ddp_sched, dataloader) + else: + ddp_model, dataloader = accelerator.prepare(ddp_model, dataloader) + if sched: + return (model, opt, sched, dataloader, ddp_model, ddp_opt, ddp_sched) + return model, ddp_model, dataloader + + +def test_noop_sync(accelerator): + # Test when on a single CPU or GPU that the context manager does nothing + model, ddp_model, dataloader = get_training_setup(accelerator) + # Use a single batch + ddp_input, ddp_target = next(iter(dataloader)).values() + for iteration in range(3): + # Gather the distributed inputs and targs for the base model + input, target = accelerator.gather((ddp_input, ddp_target)) + input, target = input.to(accelerator.device), target.to(accelerator.device) + # Perform our initial ground truth step in non "DDP" + step_model(model, input, target, accelerator) + # Do "gradient accumulation" (noop) + if iteration % 2 == 0: + # Accumulate grads locally + with accelerator.no_sync(ddp_model): + step_model(ddp_model, ddp_input, ddp_target, accelerator) + else: + # Sync grads + step_model(ddp_model, ddp_input, ddp_target, accelerator) + + # Since `no_sync` is a noop, `ddp_model` and `model` grads should always be in sync + check_model_parameters(model, ddp_model, True, iteration) + for param, ddp_param in zip(model.parameters(), ddp_model.parameters()): + if not param.requires_grad: + continue + assert torch.allclose(param.grad, ddp_param.grad), ( + f"Gradients not in sync when they should be:\nModel grad ({param.grad}) != DDP grad ({ddp_param.grad})" + ) + + # Shuffle ddp_input on each iteration + torch.manual_seed(1337 + iteration) + ddp_input = ddp_input[torch.randperm(len(ddp_input))] + + +def test_distributed_sync(accelerator): + # Test on distributed setup that context manager behaves properly + model, ddp_model, dataloader = get_training_setup(accelerator) + # Use a single batch + ddp_input, ddp_target = next(iter(dataloader)).values() + for iteration in range(3): + # Gather the distributed inputs and targs for the base model + input, target = accelerator.gather((ddp_input, ddp_target)) + input, target = input.to(accelerator.device), target.to(accelerator.device) + # Perform our initial ground truth step in non "DDP" + step_model(model, input, target, accelerator) + # Do "gradient accumulation" (noop) + if iteration % 2 == 0: + # Accumulate grads locally + with accelerator.no_sync(ddp_model): + step_model(ddp_model, ddp_input, ddp_target, accelerator) + else: + # Sync grads + step_model(ddp_model, ddp_input, ddp_target, accelerator) + + # DDP model and model should only be in sync when not (iteration % 2 == 0) + for param, ddp_param in zip(model.parameters(), ddp_model.parameters()): + if not param.requires_grad: + continue + if iteration % 2 == 0: + # Grads should not be in sync + assert torch.allclose(param.grad, ddp_param.grad) is False, ( + f"Gradients in sync when they should not be:\nModel grad ({param.grad}) == DDP grad ({ddp_param.grad})" + ) + else: + # Grads should be in sync + assert torch.allclose(param.grad, ddp_param.grad) is True, ( + f"Gradients not in sync when they should be:\nModel grad ({param.grad}) != DDP grad ({ddp_param.grad})" + ) + + # Shuffle ddp_input on each iteration + torch.manual_seed(1337 + iteration) + ddp_input = ddp_input[torch.randperm(len(ddp_input))] + + +def test_distributed_sync_multiple_fwd(accelerator): + # Test on distributed setup that context manager behaves properly when used with multiple forwards followed by multiple backwards + model, ddp_model, dataloader = get_training_setup(accelerator) + # Do multiple forwards + losses = [] + num_iterations = 3 + for iteration in range(num_iterations): + ddp_input, ddp_target = next(iter(dataloader)).values() + + # Gather the distributed inputs and targs for the base model + input, target = accelerator.gather((ddp_input, ddp_target)) + input, target = input.to(accelerator.device), target.to(accelerator.device) + + # Perform our initial ground truth step in non "DDP" + step_model(model, input, target, accelerator) + + # Accumulate grads locally + with accelerator.no_sync(ddp_model): + ddp_output = ddp_model(ddp_input) + loss = F.mse_loss(ddp_output, ddp_target.to(ddp_output.device)) + losses.append(loss) + + # Do multiple backwards and sync only at the last backward + for iteration in range(num_iterations): + loss = losses[iteration] + + if iteration < num_iterations - 1: + # Accumulate grads locally + accelerator.backward(loss) + + # DDP model and model should only be in sync after last backward + for param, ddp_param in zip(model.parameters(), ddp_model.parameters()): + if not param.requires_grad: + continue + # Grads should not be in sync + assert torch.allclose(param.grad, ddp_param.grad) is False, ( + f"Gradients in sync when they should not be:\nModel grad ({param.grad}) == DDP grad ({ddp_param.grad})" + ) + + else: + # Sync grads if last backward + with accelerator.trigger_sync_in_backward(ddp_model): + accelerator.backward(loss) + + # DDP model and model should only be in sync after last backward + for param, ddp_param in zip(model.parameters(), ddp_model.parameters()): + if not param.requires_grad: + continue + # Grads should be in sync + assert torch.allclose(param.grad, ddp_param.grad) is True, ( + f"Gradients not in sync when they should be:\nModel grad ({param.grad}) != DDP grad ({ddp_param.grad})" + ) + + +def test_gradient_accumulation(split_batches=False, dispatch_batches=False, sync_each_batch=False): + gradient_accumulation_plugin = GradientAccumulationPlugin(num_steps=2, sync_each_batch=sync_each_batch) + dataloader_config = DataLoaderConfiguration(split_batches=split_batches, dispatch_batches=dispatch_batches) + accelerator = Accelerator( + dataloader_config=dataloader_config, + gradient_accumulation_plugin=gradient_accumulation_plugin, + ) + # Test that context manager behaves properly + model, ddp_model, dataloader = get_training_setup(accelerator) + for iteration, batch in enumerate(dataloader): + ddp_input, ddp_target = batch.values() + # Gather the distributed inputs and targs for the base model + input, target = accelerator.gather((ddp_input, ddp_target)) + input, target = input.to(accelerator.device), target.to(accelerator.device) + # Perform our initial ground truth step in non "DDP" + step_model(model, input, target, accelerator, False) + # Do "gradient accumulation" (noop) + with accelerator.accumulate(ddp_model): + step_model(ddp_model, ddp_input, ddp_target, accelerator) + + # DDP model and model should only be in sync when not (iteration % 2 == 0) + for param, ddp_param in zip(model.parameters(), ddp_model.parameters()): + if not param.requires_grad: + continue + if ((iteration + 1) % 2 == 0) or (iteration == len(dataloader) - 1) or sync_each_batch: + # Grads should be in sync + assert torch.allclose(param.grad, ddp_param.grad) is True, ( + f"Gradients not in sync when they should be at iteration {iteration}:\nModel grad ({param.grad}) != DDP grad ({ddp_param.grad})" + ) + else: + # Grads should not be in sync + assert torch.allclose(param.grad, ddp_param.grad) is False, ( + f"Gradients in sync when they should not be at iteration {iteration}:\nModel grad ({param.grad}) == DDP grad ({ddp_param.grad})" + ) + + # Shuffle ddp_input on each iteration + torch.manual_seed(1337 + iteration) + ddp_input = ddp_input[torch.randperm(len(ddp_input))] + GradientState._reset_state() + + +def test_gradient_accumulation_with_opt_and_scheduler( + split_batches=False, dispatch_batches=False, sync_each_batch=False +): + gradient_accumulation_plugin = GradientAccumulationPlugin(num_steps=2, sync_each_batch=sync_each_batch) + dataloader_config = DataLoaderConfiguration(split_batches=split_batches, dispatch_batches=dispatch_batches) + accelerator = Accelerator( + dataloader_config=dataloader_config, + gradient_accumulation_plugin=gradient_accumulation_plugin, + ) + # Test that context manager behaves properly + model, opt, sched, dataloader, ddp_model, ddp_opt, ddp_sched = get_training_setup(accelerator, True) + for iteration, batch in enumerate(dataloader): + ddp_input, ddp_target = batch.values() + # Gather the distributed inputs and targs for the base model + input, target = accelerator.gather((ddp_input, ddp_target)) + input, target = input.to(accelerator.device), target.to(accelerator.device) + # Perform our initial ground truth step in non "DDP" + model.train() + ddp_model.train() + step_model(model, input, target, accelerator, False) + opt.step() + + if ((iteration + 1) % 2 == 0) or ((iteration + 1) == len(dataloader)): + if split_batches: + sched.step() + else: + for _ in range(accelerator.num_processes): + sched.step() + + # Perform gradient accumulation under wrapper + with accelerator.accumulate(ddp_model): + step_model(ddp_model, ddp_input, ddp_target, accelerator) + ddp_opt.step() + ddp_sched.step() + + # Learning rates should be the same + assert opt.param_groups[0]["lr"] == ddp_opt.param_groups[0]["lr"], ( + f"Learning rates found in each optimizer did not align\nopt: {opt.param_groups[0]['lr']}\nDDP opt: {ddp_opt.param_groups[0]['lr']}\n" + ) + did_step = (((iteration + 1) % 2) == 0) or ((iteration + 1) == len(dataloader)) + if accelerator.num_processes > 1: + check_model_parameters( + model, + ddp_model, + did_step or sync_each_batch, # syncs at each grad_accum interval of if sync_each_batch==True + iteration, + rtol=1e-3, # needs a relative tolerance due to roundoff errors + ) + + if did_step: + opt.zero_grad() # flush gradients every accum step + ddp_opt.zero_grad() + + # Shuffle ddp_input on each iteration + torch.manual_seed(1337 + iteration) + GradientState._reset_state() + + +def test_dataloader_break(): + accelerator = Accelerator() + first_dset = RegressionDataset(length=80) + first_dataloader = DataLoader(first_dset, batch_size=16) + second_dset = RegressionDataset(length=96) + second_dataloader = DataLoader(second_dset, batch_size=16) + first_dataloader, second_dataloader = accelerator.prepare(first_dataloader, second_dataloader) + + assert accelerator.gradient_state.active_dataloader is None + for iteration, _ in enumerate(first_dataloader): + assert id(accelerator.gradient_state.active_dataloader) == id(first_dataloader) + if iteration < len(first_dataloader) - 1: + assert not accelerator.gradient_state.end_of_dataloader + if iteration == 1: + for batch_num, _ in enumerate(second_dataloader): + assert id(accelerator.gradient_state.active_dataloader) == id(second_dataloader) + if batch_num < len(second_dataloader) - 1: + assert not accelerator.gradient_state.end_of_dataloader + else: + assert accelerator.gradient_state.end_of_dataloader + else: + assert accelerator.gradient_state.end_of_dataloader + assert accelerator.gradient_state.active_dataloader is None + + +def main(): + accelerator = Accelerator() + state = accelerator.state + if state.local_process_index == 0: + print("**Test `accumulate` gradient accumulation with dataloader break**") + if state.distributed_type != DistributedType.XLA: + test_dataloader_break() + if state.distributed_type == DistributedType.NO: + if state.local_process_index == 0: + print("**Test NOOP `no_sync` context manager**") + test_noop_sync(accelerator) + if state.distributed_type in ( + DistributedType.MULTI_GPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_CPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ): + if state.local_process_index == 0: + print("**Test Distributed `no_sync` context manager**") + test_distributed_sync(accelerator) + if state.local_process_index == 0: + print("**Test Distributed `no_sync` context manager with multiple forwards**") + test_distributed_sync_multiple_fwd(accelerator) + if state.distributed_type in ( + DistributedType.MULTI_GPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ): + for split_batch in [True, False]: + for dispatch_batches in [True, False]: + for sync_each_batch in [True, False]: + if state.local_process_index == 0: + print( + "**Test `accumulate` gradient accumulation, ", + f"`split_batches={split_batch}` and `dispatch_batches={dispatch_batches}` and `sync_each_batch={sync_each_batch}`**", + ) + test_gradient_accumulation(split_batch, dispatch_batches, sync_each_batch) + + # Currently will break on torch 2.0 +, need to investigate why + if state.local_process_index == 0: + print( + "**Test `accumulate` gradient accumulation with optimizer and scheduler, ", + "`split_batches=False`, `dispatch_batches=False`, `sync_each_batch=False`**", + ) + test_gradient_accumulation_with_opt_and_scheduler() + if state.distributed_type in ( + DistributedType.MULTI_GPU, + DistributedType.MULTI_NPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + ): + for split_batch in [True, False]: + for dispatch_batches in [True, False]: + for sync_each_batch in [True, False]: + if not split_batch and not dispatch_batches and not sync_each_batch: + continue + if state.local_process_index == 0: + print( + "**Test `accumulate` gradient accumulation with optimizer and scheduler, ", + f"`split_batches={split_batch}` and `dispatch_batches={dispatch_batches}` and `sync_each_batch={sync_each_batch}`**", + ) + test_gradient_accumulation_with_opt_and_scheduler(split_batch, dispatch_batches, sync_each_batch) + state.destroy_process_group() + + +def _mp_fn(index): + # For xla_spawn (TPUs) + main() + + +if __name__ == "__main__": + main() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/testing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..c809dc97fe1070438b8e4d5ea301cd9e6f52d9dc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/testing.py @@ -0,0 +1,889 @@ +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import inspect +import io +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from contextlib import contextmanager +from functools import partial +from pathlib import Path +from typing import Optional, Union +from unittest import mock + +import torch + +import accelerate + +from ..state import AcceleratorState +from ..utils import ( + check_cuda_fp8_capability, + compare_versions, + gather, + is_aim_available, + is_bnb_available, + is_clearml_available, + is_comet_ml_available, + is_cuda_available, + is_datasets_available, + is_deepspeed_available, + is_dvclive_available, + is_fp8_available, + is_fp16_available, + is_habana_gaudi1, + is_hpu_available, + is_import_timer_available, + is_matplotlib_available, + is_mlflow_available, + is_mlu_available, + is_mps_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_pandas_available, + is_pippy_available, + is_pytest_available, + is_schedulefree_available, + is_sdaa_available, + is_swanlab_available, + is_tensorboard_available, + is_timm_available, + is_torch_version, + is_torch_xla_available, + is_torchao_available, + is_torchdata_stateful_dataloader_available, + is_torchvision_available, + is_trackio_available, + is_transformer_engine_available, + is_transformer_engine_mxfp8_available, + is_transformers_available, + is_triton_available, + is_wandb_available, + is_xpu_available, + str_to_bool, +) + + +def get_backend(): + if is_torch_xla_available(): + return "xla", torch.cuda.device_count(), torch.cuda.memory_allocated + elif is_cuda_available(): + return "cuda", torch.cuda.device_count(), torch.cuda.memory_allocated + elif is_mps_available(min_version="2.0"): + return "mps", 1, torch.mps.current_allocated_memory + elif is_mps_available(): + return "mps", 1, lambda: 0 + elif is_mlu_available(): + return "mlu", torch.mlu.device_count(), torch.mlu.memory_allocated + elif is_sdaa_available(): + return "sdaa", torch.sdaa.device_count(), torch.sdaa.memory_allocated + elif is_musa_available(): + return "musa", torch.musa.device_count(), torch.musa.memory_allocated + elif is_npu_available(): + return "npu", torch.npu.device_count(), torch.npu.memory_allocated + elif is_xpu_available(): + return "xpu", torch.xpu.device_count(), torch.xpu.memory_allocated + elif is_hpu_available(): + return "hpu", torch.hpu.device_count(), torch.hpu.memory_allocated + elif is_neuron_available(): + return "neuron", torch.neuron.device_count(), torch.neuron.memory_allocated + else: + return "cpu", 1, lambda: 0 + + +torch_device, device_count, memory_allocated_func = get_backend() + + +def get_launch_command(**kwargs) -> list: + """ + Wraps around `kwargs` to help simplify launching from `subprocess`. + + Example: + ```python + # returns ['accelerate', 'launch', '--num_processes=2', '--device_count=2'] + get_launch_command(num_processes=2, device_count=2) + ``` + """ + command = ["accelerate", "launch"] + for k, v in kwargs.items(): + if isinstance(v, bool) and v: + command.append(f"--{k}") + elif v is not None: + command.append(f"--{k}={v}") + return command + + +DEFAULT_LAUNCH_COMMAND = get_launch_command(num_processes=device_count, monitor_interval=0.1) + + +def parse_flag_from_env(key, default=False): + try: + value = os.environ[key] + except KeyError: + # KEY isn't set, default to `default`. + _value = default + else: + # KEY is set, convert it to True or False. + try: + _value = str_to_bool(value) + except ValueError: + # More values are supported, but let's keep the message simple. + raise ValueError(f"If set, {key} must be yes or no.") + return _value + + +_run_slow_tests = parse_flag_from_env("RUN_SLOW", default=False) + + +def skip(test_case): + "Decorator that skips a test unconditionally" + return unittest.skip("Test was skipped")(test_case) + + +def slow(test_case): + """ + Decorator marking a test as slow. Slow tests are skipped by default. Set the RUN_SLOW environment variable to a + truthy value to run them. + """ + return unittest.skipUnless(_run_slow_tests, "test is slow")(test_case) + + +def require_cpu(test_case): + """ + Decorator marking a test that must be only ran on the CPU. These tests are skipped when a GPU is available. + """ + return unittest.skipUnless(torch_device == "cpu", "test requires only a CPU")(test_case) + + +def require_non_cpu(test_case): + """ + Decorator marking a test that requires a hardware accelerator backend. These tests are skipped when there are no + hardware accelerator available. + """ + return unittest.skipUnless(torch_device != "cpu", "test requires a GPU")(test_case) + + +def require_cuda(test_case): + """ + Decorator marking a test that requires CUDA. These tests are skipped when there are no GPU available or when + TorchXLA is available. + """ + return unittest.skipUnless(is_cuda_available() and not is_torch_xla_available(), "test requires a GPU")(test_case) + + +def require_cuda_or_hpu(test_case): + """ + Decorator marking a test that requires CUDA or HPU. These tests are skipped when there are no GPU available or when + TorchXLA is available. + """ + return unittest.skipUnless( + (is_cuda_available() and not is_torch_xla_available()) or is_hpu_available(), "test requires a GPU or HPU" + )(test_case) + + +def require_xpu(test_case): + """ + Decorator marking a test that requires XPU. These tests are skipped when there are no XPU available. + """ + return unittest.skipUnless(is_xpu_available(), "test requires a XPU")(test_case) + + +def require_cuda_or_xpu(test_case): + """ + Decorator marking a test that requires CUDA or XPU. These tests are skipped when there are no GPU available or when + TorchXLA is available. + """ + cuda_condition = is_cuda_available() and not is_torch_xla_available() + xpu_condition = is_xpu_available() + return unittest.skipUnless(cuda_condition or xpu_condition, "test requires a CUDA GPU or XPU")(test_case) + + +def require_non_xpu(test_case): + """ + Decorator marking a test that should be skipped for XPU. + """ + return unittest.skipUnless(torch_device != "xpu", "test requires a non-XPU")(test_case) + + +def require_non_hpu(test_case): + """ + Decorator marking a test that should be skipped for HPU. + """ + return unittest.skipUnless(torch_device != "hpu", "test requires a non-HPU")(test_case) + + +def require_fp16(test_case): + """ + Decorator marking a test that requires FP16. These tests are skipped when FP16 is not supported. + """ + + return unittest.skipUnless(is_fp16_available(), "test requires FP16 support")(test_case) + + +def require_fp8(test_case): + """ + Decorator marking a test that requires FP8. These tests are skipped when FP8 is not supported. + """ + + # is_fp8_available only checks for libraries + # ideally it should check for device capability as well + fp8_is_available = is_fp8_available() + + if torch.cuda.is_available() and not check_cuda_fp8_capability(): + fp8_is_available = False + + if is_hpu_available() and is_habana_gaudi1(): + fp8_is_available = False + + return unittest.skipUnless(fp8_is_available, "test requires FP8 support")(test_case) + + +def require_fsdp2(test_case): + return unittest.skipUnless(is_torch_version(">=", "2.5.0"), "test requires FSDP2 (torch >= 2.5.0)")(test_case) + + +def require_mlu(test_case): + """ + Decorator marking a test that requires MLU. These tests are skipped when there are no MLU available. + """ + return unittest.skipUnless(is_mlu_available(), "test require a MLU")(test_case) + + +def require_sdaa(test_case): + """ + Decorator marking a test that requires SDAA. These tests are skipped when there are no SDAA available. + """ + return unittest.skipUnless(is_sdaa_available(), "test require a SDAA")(test_case) + + +def require_musa(test_case): + """ + Decorator marking a test that requires MUSA. These tests are skipped when there are no MUSA available. + """ + return unittest.skipUnless(is_musa_available(), "test require a MUSA")(test_case) + + +def require_npu(test_case): + """ + Decorator marking a test that requires NPU. These tests are skipped when there are no NPU available. + """ + return unittest.skipUnless(is_npu_available(), "test require a NPU")(test_case) + + +def require_neuron(test_case): + """ + Decorator marking a test that requires Neuron. These tests are skipped when there are no Neuron Cores available. + """ + return unittest.skipUnless(is_neuron_available(), "test require Neuron Cores")(test_case) + + +def require_mps(test_case): + """ + Decorator marking a test that requires MPS backend. These tests are skipped when torch doesn't support `mps` + backend. + """ + return unittest.skipUnless(is_mps_available(), "test requires a `mps` backend support in `torch`")(test_case) + + +def require_huggingface_suite(test_case): + """ + Decorator marking a test that requires transformers and datasets. These tests are skipped when they are not. + """ + return unittest.skipUnless( + is_transformers_available() and is_datasets_available(), + "test requires the Hugging Face suite", + )(test_case) + + +def require_transformers(test_case): + """ + Decorator marking a test that requires transformers. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_transformers_available(), "test requires the transformers library")(test_case) + + +def require_timm(test_case): + """ + Decorator marking a test that requires timm. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_timm_available(), "test requires the timm library")(test_case) + + +def require_torchvision(test_case): + """ + Decorator marking a test that requires torchvision. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_torchvision_available(), "test requires the torchvision library")(test_case) + + +def require_triton(test_case): + """ + Decorator marking a test that requires triton. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_triton_available(), "test requires the triton library")(test_case) + + +def require_schedulefree(test_case): + """ + Decorator marking a test that requires schedulefree. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_schedulefree_available(), "test requires the schedulefree library")(test_case) + + +def require_bnb(test_case): + """ + Decorator marking a test that requires bitsandbytes. These tests are skipped when they are not. + """ + return unittest.skipUnless(is_bnb_available(), "test requires the bitsandbytes library")(test_case) + + +def require_tpu(test_case): + """ + Decorator marking a test that requires TPUs. These tests are skipped when there are no TPUs available. + """ + return unittest.skipUnless(is_torch_xla_available(check_is_tpu=True), "test requires TPU")(test_case) + + +def require_non_torch_xla(test_case): + """ + Decorator marking a test as requiring an environment without TorchXLA. These tests are skipped when TorchXLA is + available. + """ + return unittest.skipUnless(not is_torch_xla_available(), "test requires an env without TorchXLA")(test_case) + + +def require_single_device(test_case): + """ + Decorator marking a test that requires a single device. These tests are skipped when there is no hardware + accelerator available or number of devices is more than one. + """ + return unittest.skipUnless( + torch_device != "cpu" and device_count == 1, "test requires a single device accelerator" + )(test_case) + + +def require_single_gpu(test_case): + """ + Decorator marking a test that requires CUDA on a single GPU. These tests are skipped when there are no GPU + available or number of GPUs is more than one. + """ + return unittest.skipUnless(torch.cuda.device_count() == 1, "test requires a GPU")(test_case) + + +def require_single_xpu(test_case): + """ + Decorator marking a test that requires CUDA on a single XPU. These tests are skipped when there are no XPU + available or number of xPUs is more than one. + """ + return unittest.skipUnless(torch.xpu.device_count() == 1, "test requires a XPU")(test_case) + + +def require_multi_device(test_case): + """ + Decorator marking a test that requires a multi-device setup. These tests are skipped on a machine without multiple + devices. + """ + return unittest.skipUnless(device_count > 1, "test requires multiple hardware accelerators")(test_case) + + +def require_multi_gpu(test_case): + """ + Decorator marking a test that requires a multi-GPU setup. These tests are skipped on a machine without multiple + GPUs. + """ + return unittest.skipUnless(torch.cuda.device_count() > 1, "test requires multiple GPUs")(test_case) + + +def require_multi_xpu(test_case): + """ + Decorator marking a test that requires a multi-XPU setup. These tests are skipped on a machine without multiple + XPUs. + """ + return unittest.skipUnless(torch.xpu.device_count() > 1, "test requires multiple XPUs")(test_case) + + +def require_multi_gpu_or_xpu(test_case): + """ + Decorator marking a test that requires a multi-GPU setup. These tests are skipped on a machine without multiple + GPUs or XPUs. + """ + return unittest.skipUnless( + (is_cuda_available() or is_xpu_available()) and device_count > 1, "test requires multiple GPUs or XPUs" + )(test_case) + + +def require_deepspeed(test_case): + """ + Decorator marking a test that requires DeepSpeed installed. These tests are skipped when DeepSpeed isn't installed + """ + return unittest.skipUnless(is_deepspeed_available(), "test requires DeepSpeed")(test_case) + + +def require_tp(test_case): + """ + Decorator marking a test that requires TP installed. These tests are skipped when TP isn't installed + """ + return unittest.skipUnless( + is_torch_version(">=", "2.3.0") and compare_versions("transformers", ">=", "4.52.0"), + "test requires torch version >= 2.3.0 and transformers version >= 4.52.0", + )(test_case) + + +def require_torch_min_version(test_case=None, version=None): + """ + Decorator marking that a test requires a particular torch version to be tested. These tests are skipped when an + installed torch version is less than the required one. + """ + if test_case is None: + return partial(require_torch_min_version, version=version) + return unittest.skipUnless(is_torch_version(">=", version), f"test requires torch version >= {version}")(test_case) + + +def require_tensorboard(test_case): + """ + Decorator marking a test that requires tensorboard installed. These tests are skipped when tensorboard isn't + installed + """ + return unittest.skipUnless(is_tensorboard_available(), "test requires Tensorboard")(test_case) + + +def require_wandb(test_case): + """ + Decorator marking a test that requires wandb installed. These tests are skipped when wandb isn't installed + """ + return unittest.skipUnless(is_wandb_available(), "test requires wandb")(test_case) + + +def require_trackio(test_case): + """ + Decorator marking a test that requires trackio installed. These tests are skipped when trackio isn't installed + """ + return unittest.skipUnless(is_trackio_available(), "test requires trackio")(test_case) + + +def require_comet_ml(test_case): + """ + Decorator marking a test that requires comet_ml installed. These tests are skipped when comet_ml isn't installed + """ + return unittest.skipUnless(is_comet_ml_available(), "test requires comet_ml")(test_case) + + +def require_aim(test_case): + """ + Decorator marking a test that requires aim installed. These tests are skipped when aim isn't installed + """ + return unittest.skipUnless(is_aim_available(), "test requires aim")(test_case) + + +def require_clearml(test_case): + """ + Decorator marking a test that requires clearml installed. These tests are skipped when clearml isn't installed + """ + return unittest.skipUnless(is_clearml_available(), "test requires clearml")(test_case) + + +def require_dvclive(test_case): + """ + Decorator marking a test that requires dvclive installed. These tests are skipped when dvclive isn't installed + """ + return unittest.skipUnless(is_dvclive_available(), "test requires dvclive")(test_case) + + +def require_swanlab(test_case): + """ + Decorator marking a test that requires swanlab installed. These tests are skipped when swanlab isn't installed + """ + return unittest.skipUnless(is_swanlab_available(), "test requires swanlab")(test_case) + + +def require_pandas(test_case): + """ + Decorator marking a test that requires pandas installed. These tests are skipped when pandas isn't installed + """ + return unittest.skipUnless(is_pandas_available(), "test requires pandas")(test_case) + + +def require_mlflow(test_case): + """ + Decorator marking a test that requires mlflow installed. These tests are skipped when mlflow isn't installed + """ + return unittest.skipUnless(is_mlflow_available(), "test requires mlflow")(test_case) + + +def require_pippy(test_case): + """ + Decorator marking a test that requires pippy installed. These tests are skipped when pippy isn't installed It is + also checked if the test is running on a Gaudi1 device which doesn't support pippy. + """ + return unittest.skipUnless(is_pippy_available() and not is_habana_gaudi1(), "test requires pippy")(test_case) + + +def require_import_timer(test_case): + """ + Decorator marking a test that requires tuna interpreter installed. These tests are skipped when tuna isn't + installed + """ + return unittest.skipUnless(is_import_timer_available(), "test requires tuna interpreter")(test_case) + + +def require_transformer_engine(test_case): + """ + Decorator marking a test that requires transformers engine installed. These tests are skipped when transformers + engine isn't installed + """ + return unittest.skipUnless(is_transformer_engine_available(), "test requires transformers engine")(test_case) + + +def require_transformer_engine_mxfp8(test_case): + """ + Decorator marking a test that requires transformers engine MXFP8 block scaling available. These tests are skipped + when transformers engine MXFP8 block scaling isn't available + """ + return unittest.skipUnless( + is_transformer_engine_mxfp8_available(), "test requires transformers engine MXFP8 block scaling" + )(test_case) + + +def require_torchao(test_case): + """ + Decorator marking a test that requires torchao installed. These tests are skipped when torchao isn't installed + """ + return unittest.skipUnless(is_torchao_available(), "test requires torchao")(test_case) + + +def require_matplotlib(test_case): + """ + Decorator marking a test that requires matplotlib installed. These tests are skipped when matplotlib isn't + installed + """ + return unittest.skipUnless(is_matplotlib_available(), "test requires matplotlib")(test_case) + + +_atleast_one_tracker_available = ( + any([is_wandb_available(), is_tensorboard_available(), is_trackio_available(), is_swanlab_available()]) + and not is_comet_ml_available() +) + + +def require_trackers(test_case): + """ + Decorator marking that a test requires at least one tracking library installed. These tests are skipped when none + are installed + """ + return unittest.skipUnless( + _atleast_one_tracker_available, + "test requires at least one tracker to be available and for `comet_ml` to not be installed", + )(test_case) + + +def require_torchdata_stateful_dataloader(test_case): + """ + Decorator marking a test that requires torchdata.stateful_dataloader. + + These tests are skipped when torchdata with stateful_dataloader module isn't installed. + + """ + return unittest.skipUnless( + is_torchdata_stateful_dataloader_available(), "test requires torchdata.stateful_dataloader" + )(test_case) + + +def run_first(test_case): + """ + Decorator marking a test with order(1). When pytest-order plugin is installed, tests marked with this decorator are + guaranteed to run first. + + This is especially useful in some test settings like on a Gaudi instance where a Gaudi device can only be used by a + single process at a time. So we make sure all tests that run in a subprocess are launched first, to avoid device + allocation conflicts. + + If pytest is not installed, test will be returned as is. + """ + + if is_pytest_available(): + import pytest + + return pytest.mark.order(1)(test_case) + return test_case + + +class TempDirTestCase(unittest.TestCase): + """ + A TestCase class that keeps a single `tempfile.TemporaryDirectory` open for the duration of the class, wipes its + data at the start of a test, and then destroys it at the end of the TestCase. + + Useful for when a class or API requires a single constant folder throughout it's use, such as Weights and Biases + + The temporary directory location will be stored in `self.tmpdir` + """ + + clear_on_setup = True + + @classmethod + def setUpClass(cls): + "Creates a `tempfile.TemporaryDirectory` and stores it in `cls.tmpdir`" + cls.tmpdir = Path(tempfile.mkdtemp()) + + @classmethod + def tearDownClass(cls): + "Remove `cls.tmpdir` after test suite has finished" + if os.path.exists(cls.tmpdir): + shutil.rmtree(cls.tmpdir) + + def setUp(self): + "Destroy all contents in `self.tmpdir`, but not `self.tmpdir`" + if self.clear_on_setup: + for path in self.tmpdir.glob("**/*"): + if path.is_file(): + path.unlink() + elif path.is_dir(): + shutil.rmtree(path) + + +class AccelerateTestCase(unittest.TestCase): + """ + A TestCase class that will reset the accelerator state at the end of every test. Every test that checks or utilizes + the `AcceleratorState` class should inherit from this to avoid silent failures due to state being shared between + tests. + """ + + def tearDown(self): + super().tearDown() + # Reset the state of the AcceleratorState singleton. + AcceleratorState._reset_state(True) + + +class MockingTestCase(unittest.TestCase): + """ + A TestCase class designed to dynamically add various mockers that should be used in every test, mimicking the + behavior of a class-wide mock when defining one normally will not do. + + Useful when a mock requires specific information available only initialized after `TestCase.setUpClass`, such as + setting an environment variable with that information. + + The `add_mocks` function should be ran at the end of a `TestCase`'s `setUp` function, after a call to + `super().setUp()` such as: + ```python + def setUp(self): + super().setUp() + mocks = mock.patch.dict(os.environ, {"SOME_ENV_VAR", "SOME_VALUE"}) + self.add_mocks(mocks) + ``` + """ + + def add_mocks(self, mocks: Union[mock.Mock, list[mock.Mock]]): + """ + Add custom mocks for tests that should be repeated on each test. Should be called during + `MockingTestCase.setUp`, after `super().setUp()`. + + Args: + mocks (`mock.Mock` or list of `mock.Mock`): + Mocks that should be added to the `TestCase` after `TestCase.setUpClass` has been run + """ + self.mocks = mocks if isinstance(mocks, (tuple, list)) else [mocks] + for m in self.mocks: + m.start() + self.addCleanup(m.stop) + + +def are_the_same_tensors(tensor): + state = AcceleratorState() + tensor = tensor[None].clone().to(state.device) + tensors = gather(tensor).cpu() + tensor = tensor[0].cpu() + for i in range(tensors.shape[0]): + if not torch.equal(tensors[i], tensor): + return False + return True + + +class _RunOutput: + def __init__(self, returncode, stdout, stderr): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +async def _read_stream(stream, callback): + while True: + line = await stream.readline() + if line: + callback(line) + else: + break + + +async def _stream_subprocess(cmd, env=None, stdin=None, timeout=None, quiet=False, echo=False) -> _RunOutput: + if echo: + print("\nRunning: ", " ".join(cmd)) + + p = await asyncio.create_subprocess_exec( + cmd[0], + *cmd[1:], + stdin=stdin, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + ) + + # note: there is a warning for a possible deadlock when using `wait` with huge amounts of data in the pipe + # https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.asyncio.subprocess.Process.wait + # + # If it starts hanging, will need to switch to the following code. The problem is that no data + # will be seen until it's done and if it hangs for example there will be no debug info. + # out, err = await p.communicate() + # return _RunOutput(p.returncode, out, err) + + out = [] + err = [] + + def tee(line, sink, pipe, label=""): + line = line.decode("utf-8").rstrip() + sink.append(line) + if not quiet: + print(label, line, file=pipe) + + # XXX: the timeout doesn't seem to make any difference here + await asyncio.wait( + [ + asyncio.create_task(_read_stream(p.stdout, lambda l: tee(l, out, sys.stdout, label="stdout:"))), + asyncio.create_task(_read_stream(p.stderr, lambda l: tee(l, err, sys.stderr, label="stderr:"))), + ], + timeout=timeout, + ) + return _RunOutput(await p.wait(), out, err) + + +def execute_subprocess_async(cmd: list, env=None, stdin=None, timeout=180, quiet=False, echo=True) -> _RunOutput: + # Cast every path in `cmd` to a string + for i, c in enumerate(cmd): + if isinstance(c, Path): + cmd[i] = str(c) + + result = asyncio.run(_stream_subprocess(cmd, env=env, stdin=stdin, timeout=timeout, quiet=quiet, echo=echo)) + + cmd_str = " ".join(cmd) + if result.returncode > 0: + stderr = "\n".join(result.stderr) + raise RuntimeError( + f"'{cmd_str}' failed with returncode {result.returncode}\n\n" + f"The combined stderr from workers follows:\n{stderr}" + ) + + return result + + +def pytest_xdist_worker_id(): + """ + Returns an int value of worker's numerical id under `pytest-xdist`'s concurrent workers `pytest -n N` regime, or 0 + if `-n 1` or `pytest-xdist` isn't being used. + """ + worker = os.environ.get("PYTEST_XDIST_WORKER", "gw0") + worker = re.sub(r"^gw", "", worker, 0, re.M) + return int(worker) + + +def get_torch_dist_unique_port(): + """ + Returns a port number that can be fed to `torch.distributed.launch`'s `--master_port` argument. + + Under `pytest-xdist` it adds a delta number based on a worker id so that concurrent tests don't try to use the same + port at once. + """ + port = 29500 + uniq_delta = pytest_xdist_worker_id() + return port + uniq_delta + + +class SubprocessCallException(Exception): + pass + + +def run_command(command: list[str], return_stdout=False, env=None): + """ + Runs `command` with `subprocess.check_output` and will potentially return the `stdout`. Will also properly capture + if an error occurred while running `command` + """ + # Cast every path in `command` to a string + for i, c in enumerate(command): + if isinstance(c, Path): + command[i] = str(c) + if env is None: + env = os.environ.copy() + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT, env=env) + if return_stdout: + if hasattr(output, "decode"): + output = output.decode("utf-8") + return output + except subprocess.CalledProcessError as e: + raise SubprocessCallException( + f"Command `{' '.join(command)}` failed with the following error:\n\n{e.output.decode()}" + ) from e + + +def path_in_accelerate_package(*components: str) -> Path: + """ + Get a path within the `accelerate` package's directory. + + Args: + *components: Components of the path to join after the package directory. + + Returns: + `Path`: The path to the requested file or directory. + """ + + accelerate_package_dir = Path(inspect.getfile(accelerate)).parent + return accelerate_package_dir.joinpath(*components) + + +@contextmanager +def assert_exception(exception_class: Exception, msg: Optional[str] = None) -> bool: + """ + Context manager to assert that the right `Exception` class was raised. + + If `msg` is provided, will check that the message is contained in the raised exception. + """ + was_ran = False + try: + yield + was_ran = True + except Exception as e: + assert isinstance(e, exception_class), f"Expected exception of type {exception_class} but got {type(e)}" + if msg is not None: + assert msg in str(e), f"Expected message '{msg}' to be in exception but got '{str(e)}'" + if was_ran: + raise AssertionError(f"Expected exception of type {exception_class} but ran without issue.") + + +def capture_call_output(func, *args, **kwargs): + """ + Takes in a `func` with `args` and `kwargs` and returns the captured stdout as a string + """ + captured_output = io.StringIO() + original_stdout = sys.stdout + try: + sys.stdout = captured_output + func(*args, **kwargs) + except Exception as e: + raise e + finally: + sys.stdout = original_stdout + return captured_output.getvalue() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/training.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/training.py new file mode 100644 index 0000000000000000000000000000000000000000..609912bf542aa4d7eccf475689db066a5a8897c4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/test_utils/training.py @@ -0,0 +1,150 @@ +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import torch +from torch.utils.data import DataLoader + +from accelerate.utils.dataclasses import DistributedType + + +class RegressionDataset: + def __init__(self, a=2, b=3, length=64, seed=None): + rng = np.random.default_rng(seed) + self.length = length + self.x = rng.normal(size=(length,)).astype(np.float32) + self.y = a * self.x + b + rng.normal(scale=0.1, size=(length,)).astype(np.float32) + + def __len__(self): + return self.length + + def __getitem__(self, i): + return {"x": self.x[i], "y": self.y[i]} + + +class RegressionModel(torch.nn.Module): + def __init__(self, a=0, b=0, double_output=False): + super().__init__() + self.a = torch.nn.Parameter(torch.tensor(a).float()) + self.b = torch.nn.Parameter(torch.tensor(b).float()) + self.first_batch = True + + def forward(self, x=None): + if self.first_batch: + print(f"Model dtype: {self.a.dtype}, {self.b.dtype}. Input dtype: {x.dtype}") + self.first_batch = False + return x * self.a + self.b + + +def mocked_dataloaders(accelerator, batch_size: int = 16): + from datasets import load_dataset + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained("bert-base-cased") + data_files = {"train": "tests/test_samples/MRPC/train.csv", "validation": "tests/test_samples/MRPC/dev.csv"} + datasets = load_dataset("csv", data_files=data_files) + label_list = datasets["train"].unique("label") + + label_to_id = {v: i for i, v in enumerate(label_list)} + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer( + examples["sentence1"], examples["sentence2"], truncation=True, max_length=None, padding="max_length" + ) + if "label" in examples: + outputs["labels"] = [label_to_id[l] for l in examples["label"]] + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + tokenized_datasets = datasets.map( + tokenize_function, + batched=True, + remove_columns=["sentence1", "sentence2", "label"], + ) + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + if accelerator.distributed_type == DistributedType.XLA: + return tokenizer.pad(examples, padding="max_length", max_length=128, return_tensors="pt") + return tokenizer.pad(examples, padding="longest", return_tensors="pt") + + # Instantiate dataloaders. + train_dataloader = DataLoader(tokenized_datasets["train"], shuffle=True, collate_fn=collate_fn, batch_size=2) + eval_dataloader = DataLoader(tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=1) + + return train_dataloader, eval_dataloader + + +def mocked_dataloaders_for_autoregressive_models(accelerator, batch_size: int = 16): + from datasets import load_dataset + from transformers import AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM-360M") + tokenizer.pad_token = tokenizer.eos_token + + data_files = {"train": "tests/test_samples/MRPC/train.csv", "validation": "tests/test_samples/MRPC/dev.csv"} + datasets = load_dataset("csv", data_files=data_files) + + def tokenize_function(examples): + # max_length=None => use the model max length (it's actually the default) + outputs = tokenizer(examples["sentence1"], truncation=True, max_length=None, return_attention_mask=False) + return outputs + + # Apply the method we just defined to all the examples in all the splits of the dataset + # starting with the main process first: + with accelerator.main_process_first(): + tokenized_datasets = datasets.map( + tokenize_function, + batched=True, + remove_columns=["sentence1", "sentence2", "label"], + ) + + def collate_fn(examples): + # On TPU it's best to pad everything to the same length or training will be very slow. + max_length = ( + 128 + if accelerator.distributed_type == DistributedType.XLA + else max([len(e["input_ids"]) for e in examples]) + ) + # When using mixed precision we want round multiples of 8/16 + if accelerator.mixed_precision == "fp8": + pad_to_multiple_of = 16 + elif accelerator.mixed_precision != "no": + pad_to_multiple_of = 8 + else: + pad_to_multiple_of = None + + batch = tokenizer.pad( + examples, + padding="max_length", + max_length=max_length + 1, + pad_to_multiple_of=pad_to_multiple_of, + return_tensors="pt", + ) + + batch["labels"] = batch["input_ids"][:, 1:] + batch["input_ids"] = batch["input_ids"][:, :-1] + if "attention_mask" in batch: + batch["attention_mask"] = batch["attention_mask"][:, :-1] + + batch["labels"] = torch.where(batch["labels"] == tokenizer.pad_token_id, -100, batch["labels"]) + + return batch + + # Instantiate dataloaders. + train_dataloader = DataLoader(tokenized_datasets["train"], shuffle=False, collate_fn=collate_fn, batch_size=2) + eval_dataloader = DataLoader(tokenized_datasets["validation"], shuffle=False, collate_fn=collate_fn, batch_size=1) + + return train_dataloader, eval_dataloader diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..76b930e83014cc2e0efb801266a75c4cdc5f4bf7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__init__.py @@ -0,0 +1,304 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from ..parallelism_config import ParallelismConfig +from .ao import convert_model_to_fp8_ao, filter_first_and_last_linear_layers, has_ao_layers +from .constants import ( + MITA_PROFILING_AVAILABLE_PYTORCH_VERSION, + MODEL_NAME, + OPTIMIZER_NAME, + PROFILE_PATTERN_NAME, + RNG_STATE_NAME, + SAFE_MODEL_NAME, + SAFE_WEIGHTS_INDEX_NAME, + SAFE_WEIGHTS_NAME, + SAFE_WEIGHTS_PATTERN_NAME, + SAMPLER_NAME, + SCALER_NAME, + SCHEDULER_NAME, + TORCH_DISTRIBUTED_OPERATION_TYPES, + TORCH_LAUNCH_PARAMS, + WEIGHTS_INDEX_NAME, + WEIGHTS_NAME, + WEIGHTS_PATTERN_NAME, + XPU_PROFILING_AVAILABLE_PYTORCH_VERSION, +) +from .dataclasses import ( + AORecipeKwargs, + AutocastKwargs, + BnbQuantizationConfig, + ComputeEnvironment, + CustomDtype, + DataLoaderConfiguration, + DDPCommunicationHookType, + DeepSpeedPlugin, + DeepSpeedSequenceParallelConfig, + DistributedDataParallelKwargs, + DistributedType, + DynamoBackend, + FP8RecipeKwargs, + FullyShardedDataParallelPlugin, + GradientAccumulationPlugin, + GradScalerKwargs, + InitProcessGroupKwargs, + KwargsHandler, + LoggerType, + MegatronLMPlugin, + MSAMPRecipeKwargs, + PrecisionType, + ProfileKwargs, + ProjectConfiguration, + RNGType, + SageMakerDistributedType, + TensorInformation, + TERecipeKwargs, + TorchContextParallelConfig, + TorchDynamoPlugin, + TorchTensorParallelConfig, + TorchTensorParallelPlugin, + add_model_config_to_megatron_parser, +) +from .environment import ( + are_libraries_initialized, + check_cuda_fp8_capability, + check_cuda_p2p_ib_support, + clear_environment, + convert_dict_to_env_variables, + get_cpu_distributed_information, + get_current_device_type, + get_gpu_info, + get_int_from_env, + parse_choice_from_env, + parse_flag_from_env, + patch_environment, + purge_accelerate_environment, + set_numa_affinity, + str_to_bool, +) +from .imports import ( + deepspeed_required, + is_4bit_bnb_available, + is_8bit_bnb_available, + is_aim_available, + is_bf16_available, + is_bitsandbytes_multi_backend_available, + is_bnb_available, + is_boto3_available, + is_clearml_available, + is_comet_ml_available, + is_cuda_available, + is_datasets_available, + is_deepspeed_available, + is_dvclive_available, + is_fp8_available, + is_fp16_available, + is_habana_gaudi1, + is_hpu_available, + is_import_timer_available, + is_lomo_available, + is_matplotlib_available, + is_megatron_lm_available, + is_mlflow_available, + is_mlu_available, + is_mps_available, + is_msamp_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_pandas_available, + is_peft_available, + is_pippy_available, + is_pynvml_available, + is_pytest_available, + is_rich_available, + is_sagemaker_available, + is_schedulefree_available, + is_sdaa_available, + is_swanlab_available, + is_tensorboard_available, + is_timm_available, + is_torch_xla_available, + is_torchao_available, + is_torchdata_available, + is_torchdata_stateful_dataloader_available, + is_torchvision_available, + is_trackio_available, + is_transformer_engine_available, + is_transformer_engine_mxfp8_available, + is_transformers_available, + is_triton_available, + is_wandb_available, + is_weights_only_available, + is_xccl_available, + is_xpu_available, + torchao_required, +) +from .modeling import ( + align_module_device, + calculate_maximum_sizes, + check_device_map, + check_tied_parameters_in_config, + check_tied_parameters_on_same_device, + compute_module_sizes, + convert_file_size_to_int, + dtype_byte_size, + find_tied_parameters, + get_balanced_memory, + get_grad_scaler, + get_max_layer_size, + get_max_memory, + get_mixed_precision_context_manager, + has_offloaded_params, + id_tensor_storage, + infer_auto_device_map, + is_peft_model, + load_checkpoint_in_model, + load_offloaded_weights, + load_state_dict, + named_module_tensors, + retie_parameters, + set_module_tensor_to_device, +) +from .offload import ( + OffloadedWeightsLoader, + PrefixedDataset, + extract_submodules_state_dict, + load_offloaded_weight, + offload_state_dict, + offload_weight, + save_offload_index, +) +from .operations import ( + CannotPadNestedTensorWarning, + GatheredParameters, + broadcast, + broadcast_object_list, + concatenate, + convert_outputs_to_fp32, + convert_to_fp32, + copy_tensor_to_devices, + find_batch_size, + find_device, + gather, + gather_object, + get_data_structure, + honor_type, + ignorant_find_batch_size, + initialize_tensors, + is_namedtuple, + is_tensor_information, + is_torch_tensor, + listify, + pad_across_processes, + pad_input_tensors, + recursively_apply, + reduce, + send_to_device, + slice_tensors, +) +from .versions import compare_versions, is_torch_version + + +if is_deepspeed_available(): + from .deepspeed import ( + DeepSpeedEngineWrapper, + DeepSpeedOptimizerWrapper, + DeepSpeedSchedulerWrapper, + DummyOptim, + DummyScheduler, + HfDeepSpeedConfig, + get_active_deepspeed_plugin, + map_pytorch_optim_to_deepspeed, + ) + +from .bnb import has_4bit_bnb_layers, load_and_quantize_model +from .fsdp_utils import ( + disable_fsdp_ram_efficient_loading, + enable_fsdp_ram_efficient_loading, + ensure_weights_retied, + fsdp2_apply_ac, + fsdp2_canonicalize_names, + fsdp2_load_full_state_dict, + fsdp2_prepare_model, + fsdp2_switch_optimizer_parameters, + get_fsdp2_grad_scaler, + load_fsdp_model, + load_fsdp_optimizer, + merge_fsdp_weights, + save_fsdp_model, + save_fsdp_optimizer, +) +from .launch import ( + PrepareForLaunch, + _filter_args, + prepare_deepspeed_cmd_env, + prepare_multi_gpu_env, + prepare_sagemager_args_inputs, + prepare_simple_launcher_cmd_env, + prepare_tpu, +) + +# For docs +from .megatron_lm import ( + AbstractTrainStep, + BertTrainStep, + GPTTrainStep, + MegatronLMDummyDataLoader, + MegatronLMDummyScheduler, + T5TrainStep, + avg_losses_across_data_parallel_group, +) + + +if is_megatron_lm_available(): + from .megatron_lm import ( + MegatronEngine, + MegatronLMOptimizerWrapper, + MegatronLMSchedulerWrapper, + gather_across_data_parallel_groups, + ) + from .megatron_lm import initialize as megatron_lm_initialize + from .megatron_lm import prepare_data_loader as megatron_lm_prepare_data_loader + from .megatron_lm import prepare_model_optimizer_scheduler as megatron_lm_prepare_model_optimizer_scheduler + from .megatron_lm import prepare_optimizer as megatron_lm_prepare_optimizer + from .megatron_lm import prepare_scheduler as megatron_lm_prepare_scheduler +from .memory import find_executable_batch_size, release_memory +from .other import ( + check_os_kernel, + clean_state_dict_for_safetensors, + compile_regions, + compile_regions_deepspeed, + convert_bytes, + extract_model_from_parallel, + get_module_children_bottom_up, + get_pretty_name, + has_compiled_regions, + is_compiled_module, + is_port_in_use, + load, + merge_dicts, + model_has_dtensor, + recursive_getattr, + save, + wait_for_everyone, + write_basic_config, +) +from .random import set_seed, synchronize_rng_state, synchronize_rng_states +from .torch_xla import install_xla +from .tqdm import tqdm +from .transformer_engine import ( + apply_fp8_autowrap, + contextual_fp8_autocast, + convert_model, + has_transformer_engine_layers, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d2ceab09f6f16d5b5f5fad05eb3e055986bf371 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/ao.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/ao.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3b171d2772cdd3c3ab1bc7db7503ddd93c2681e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/ao.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/bnb.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/bnb.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bd4b1f3a3cb8c6424dc25eb76418b890a300e1f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/bnb.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/constants.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b472283a03f05cf5a0ad54fcfd210e20b3282a07 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/constants.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/deepspeed.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/deepspeed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb2cbe4c800fc00176ba7008b917e0e183e7b631 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/deepspeed.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/environment.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/environment.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92a8a6489e72cb5dd7acd5eb77110dc3438602d2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/environment.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/fsdp_utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/fsdp_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48d27e5d8d1afb73b2ac66bbcdbb66cbf0e8047d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/fsdp_utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/imports.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/imports.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59d5516db672f0e3c096bde3a66b1b4593123cab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/imports.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/launch.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/launch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b37eaabf83ecba6566022c366dfa52cbd291295 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/launch.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/megatron_lm.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/megatron_lm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1580c0cff593b0837b5c7858771984a37815c49e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/megatron_lm.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/memory.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/memory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf2d7738fc70677f8895a539aadab328028b86bf Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/memory.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/modeling.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/modeling.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..454a5dcf3c90475a7e35c0034a8ca214cfa059f4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/modeling.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/offload.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/offload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25dd5797d2ff46eaa82e98b787a50e27d2ccbce3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/offload.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/operations.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/operations.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..466feffcb853fe0845b69eb2b4725a682bb836ab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/operations.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/other.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/other.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88b51e6f99d1c872a575de4c649800210485d796 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/other.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/random.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/random.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d6159930219c3ace1b3efa3d38b71f0356feb0a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/random.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/rich.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/rich.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82da1dcebd86476ccd4bacade0c8e4bde77af9b4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/rich.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/torch_xla.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/torch_xla.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08cb5062bb2081e7940c997013098495ca52ae1a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/torch_xla.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/tqdm.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/tqdm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..108f6a8b3d4f8adb82d0dda736238895d6c3d470 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/tqdm.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/transformer_engine.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/transformer_engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92fc31eca8b427fec9e4969abb9e0a5b90db2777 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/transformer_engine.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/versions.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/versions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c654a31f6b16ce6b770d8ce698bec8ed04d7303a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/__pycache__/versions.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/ao.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/ao.py new file mode 100644 index 0000000000000000000000000000000000000000..5cc417d72829006da38549b42fd68caae27b1204 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/ao.py @@ -0,0 +1,143 @@ +# Copyright 2025 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Needed utilities for torchao FP8 training. +""" + +from functools import partial +from typing import TYPE_CHECKING, Callable, Optional + +import torch + +from .imports import is_torchao_available, torchao_required + + +if TYPE_CHECKING: + if is_torchao_available(): + from torchao.float8.float8_linear import Float8LinearConfig + + +def find_first_last_linear_layers(model: torch.nn.Module): + """ + Finds the first and last linear layer names in a model. + + This is needed during FP8 to avoid issues with instability by keeping the first and last layers unquantized. + + Ref: https://x.com/xariusrke/status/1826669142604141052 + """ + first_linear, last_linear = None, None + for name, module in model.named_modules(): + if isinstance(module, torch.nn.Linear): + if first_linear is None: + first_linear = name + last_linear = name + return first_linear, last_linear + + +def filter_linear_layers(module, fqn: str, layers_to_filter: list[str]) -> bool: + """ + A function which will check if `module` is: + - a `torch.nn.Linear` layer + - has in_features and out_features divisible by 16 + - is not part of `layers_to_filter` + + Args: + module (`torch.nn.Module`): + The module to check. + fqn (`str`): + The fully qualified name of the layer. + layers_to_filter (`List[str]`): + The list of layers to filter. + """ + if isinstance(module, torch.nn.Linear): + if module.in_features % 16 != 0 or module.out_features % 16 != 0: + return False + if fqn in layers_to_filter: + return False + return True + + +def filter_first_and_last_linear_layers(module, fqn: str) -> bool: + """ + A filter function which will filter out all linear layers except the first and last. + + + + For stability reasons, we skip the first and last linear layers Otherwise can lead to the model not training or + converging properly + + + + Args: + module (`torch.nn.Module`): + The module to check. + fqn (`str`): + The fully qualified name of the layer. + """ + first_linear, last_linear = find_first_last_linear_layers(module) + return filter_linear_layers(module, fqn, layers_to_filter=[first_linear, last_linear]) + + +@torchao_required +def has_ao_layers(model: torch.nn.Module): + from torchao.float8.float8_linear import Float8Linear + + for name, module in model.named_modules(): + if isinstance(module, Float8Linear): + return True + return False + + +@torchao_required +def convert_model_to_fp8_ao( + model: torch.nn.Module, + config: Optional["Float8LinearConfig"] = None, + module_filter_func: Optional[Callable] = filter_first_and_last_linear_layers, +): + """ + Converts all `nn.Linear` layers in the model (except the first and last) to torchao's `Float8Linear` layer inplace. + + Args: + model (`torch.nn.Module`): + The model to convert. + config (`torchao.float8.Float8LinearConfig`, *optional*): + The configuration for the FP8 training. Recommended to utilize + `torchao.float8.recipe_name_to_linear_config` to generate this. In general, the default config should be + sufficient (what is passed when set to `None`). + module_filter_func (`Callable`, *optional*, defaults to `filter_linear_layers`): + Optional function that must take in a module and layer name, and returns a boolean indicating whether the + module should be converted to FP8. Defaults to `filter_linear_layers`. See it for an example. + + Example: + + ```python + from accelerate.utils.ao import convert_model_to_fp8_ao + from accelerate import Accelerator + + accelerator = Accelerator( + + model = MyModel() + model.to(accelerator.device) + convert_to_float8_training(model) + + model.train() + ``` + """ + from torchao.float8 import convert_to_float8_training + + first_linear, last_linear = find_first_last_linear_layers(model) + if module_filter_func is None: + module_filter_func = partial(filter_linear_layers, layers_to_filter=[first_linear, last_linear]) + convert_to_float8_training(model, module_filter_fn=module_filter_func, config=config) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/bnb.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/bnb.py new file mode 100644 index 0000000000000000000000000000000000000000..cb698e804c492e06614230f0daf43888327c5a37 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/bnb.py @@ -0,0 +1,464 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import logging +import os +from copy import deepcopy +from typing import Optional, Union + +import torch +import torch.nn as nn + +from accelerate.utils.imports import ( + is_4bit_bnb_available, + is_8bit_bnb_available, +) + +from ..big_modeling import dispatch_model, init_empty_weights +from .dataclasses import BnbQuantizationConfig +from .modeling import ( + find_tied_parameters, + get_balanced_memory, + infer_auto_device_map, + load_checkpoint_in_model, + offload_weight, + set_module_tensor_to_device, +) + + +logger = logging.getLogger(__name__) + + +def load_and_quantize_model( + model: torch.nn.Module, + bnb_quantization_config: BnbQuantizationConfig, + weights_location: Optional[Union[str, os.PathLike]] = None, + device_map: Optional[dict[str, Union[int, str, torch.device]]] = None, + no_split_module_classes: Optional[list[str]] = None, + max_memory: Optional[dict[Union[int, str], Union[int, str]]] = None, + offload_folder: Optional[Union[str, os.PathLike]] = None, + offload_state_dict: bool = False, +): + """ + This function will quantize the input model with the associated config passed in `bnb_quantization_config`. If the + model is in the meta device, we will load and dispatch the weights according to the `device_map` passed. If the + model is already loaded, we will quantize the model and put the model on the GPU, + + Args: + model (`torch.nn.Module`): + Input model. The model can be already loaded or on the meta device + bnb_quantization_config (`BnbQuantizationConfig`): + The bitsandbytes quantization parameters + weights_location (`str` or `os.PathLike`): + The folder weights_location to load. It can be: + - a path to a file containing a whole model state dict + - a path to a `.json` file containing the index to a sharded checkpoint + - a path to a folder containing a unique `.index.json` file and the shards of a checkpoint. + - a path to a folder containing a unique pytorch_model.bin file. + device_map (`Dict[str, Union[int, str, torch.device]]`, *optional*): + A map that specifies where each submodule should go. It doesn't need to be refined to each parameter/buffer + name, once a given module name is inside, every submodule of it will be sent to the same device. + no_split_module_classes (`List[str]`, *optional*): + A list of layer class names that should never be split across device (for instance any layer that has a + residual connection). + max_memory (`Dict`, *optional*): + A dictionary device identifier to maximum memory. Will default to the maximum memory available if unset. + offload_folder (`str` or `os.PathLike`, *optional*): + If the `device_map` contains any value `"disk"`, the folder where we will offload weights. + offload_state_dict (`bool`, *optional*, defaults to `False`): + If `True`, will temporarily offload the CPU state dict on the hard drive to avoid getting out of CPU RAM if + the weight of the CPU state dict + the biggest shard does not fit. + + Returns: + `torch.nn.Module`: The quantized model + """ + + load_in_4bit = bnb_quantization_config.load_in_4bit + load_in_8bit = bnb_quantization_config.load_in_8bit + + if load_in_8bit and not is_8bit_bnb_available(): + raise ImportError( + "You have a version of `bitsandbytes` that is not compatible with 8bit quantization," + " make sure you have the latest version of `bitsandbytes` installed." + ) + if load_in_4bit and not is_4bit_bnb_available(): + raise ValueError( + "You have a version of `bitsandbytes` that is not compatible with 4bit quantization," + "make sure you have the latest version of `bitsandbytes` installed." + ) + + modules_on_cpu = [] + # custom device map + if isinstance(device_map, dict) and len(device_map.keys()) > 1: + modules_on_cpu = [key for key, value in device_map.items() if value in ["disk", "cpu"]] + + # We keep some modules such as the lm_head in their original dtype for numerical stability reasons + if bnb_quantization_config.skip_modules is None: + bnb_quantization_config.skip_modules = get_keys_to_not_convert(model) + + # add cpu modules to skip modules only for 4-bit modules + if load_in_4bit: + bnb_quantization_config.skip_modules.extend(modules_on_cpu) + modules_to_not_convert = bnb_quantization_config.skip_modules + + # We add the modules we want to keep in full precision + if bnb_quantization_config.keep_in_fp32_modules is None: + bnb_quantization_config.keep_in_fp32_modules = [] + keep_in_fp32_modules = bnb_quantization_config.keep_in_fp32_modules + modules_to_not_convert.extend(keep_in_fp32_modules) + + # compatibility with peft + model.is_loaded_in_4bit = load_in_4bit + model.is_loaded_in_8bit = load_in_8bit + + model_device = get_parameter_device(model) + if model_device.type != "meta": + # quantization of an already loaded model + logger.warning( + "It is not recommended to quantize a loaded model. " + "The model should be instantiated under the `init_empty_weights` context manager." + ) + model = replace_with_bnb_layers(model, bnb_quantization_config, modules_to_not_convert=modules_to_not_convert) + # convert param to the right dtype + dtype = bnb_quantization_config.torch_dtype + for name, param in model.named_parameters(): + if any(module_to_keep_in_fp32 in name for module_to_keep_in_fp32 in keep_in_fp32_modules): + param.data = param.data.to(torch.float32) + elif torch.is_floating_point(param): + param.data = param.data.to(dtype) + if model_device.type == "cuda": + model.cuda(torch.cuda.current_device()) + torch.cuda.empty_cache() + elif torch.cuda.is_available(): + model.to(torch.cuda.current_device()) + elif torch.xpu.is_available(): + model.to(torch.xpu.current_device()) + else: + raise RuntimeError("No GPU or Intel XPU found. A GPU or Intel XPU is needed for quantization.") + logger.info( + f"The model device type is {model_device.type}. However, gpu or intel xpu is needed for quantization." + "We move the model to it." + ) + return model + + elif weights_location is None: + raise RuntimeError( + f"`weights_location` needs to be the folder path containing the weights of the model, but we found {weights_location} " + ) + + else: + with init_empty_weights(): + model = replace_with_bnb_layers( + model, bnb_quantization_config, modules_to_not_convert=modules_to_not_convert + ) + device_map = get_quantized_model_device_map( + model, + bnb_quantization_config, + device_map, + max_memory=max_memory, + no_split_module_classes=no_split_module_classes, + ) + if offload_state_dict is None and device_map is not None and "disk" in device_map.values(): + offload_state_dict = True + + offload = any(x in list(device_map.values()) for x in ["cpu", "disk"]) + + load_checkpoint_in_model( + model, + weights_location, + device_map, + dtype=bnb_quantization_config.torch_dtype, + offload_folder=offload_folder, + offload_state_dict=offload_state_dict, + keep_in_fp32_modules=bnb_quantization_config.keep_in_fp32_modules, + offload_8bit_bnb=load_in_8bit and offload, + ) + return dispatch_model(model, device_map=device_map, offload_dir=offload_folder) + + +def get_quantized_model_device_map( + model, bnb_quantization_config, device_map=None, max_memory=None, no_split_module_classes=None +): + if device_map is None: + if torch.cuda.is_available(): + device_map = {"": torch.cuda.current_device()} + elif torch.xpu.is_available(): + device_map = {"": torch.xpu.current_device()} + else: + raise RuntimeError("No GPU found. A GPU is needed for quantization.") + logger.info("The device_map was not initialized.Setting device_map to `{'':torch.cuda.current_device()}`.") + + if isinstance(device_map, str): + if device_map not in ["auto", "balanced", "balanced_low_0", "sequential"]: + raise ValueError( + "If passing a string for `device_map`, please choose 'auto', 'balanced', 'balanced_low_0' or " + "'sequential'." + ) + + special_dtypes = {} + special_dtypes.update( + { + name: bnb_quantization_config.torch_dtype + for name, _ in model.named_parameters() + if any(m in name for m in bnb_quantization_config.skip_modules) + } + ) + special_dtypes.update( + { + name: torch.float32 + for name, _ in model.named_parameters() + if any(m in name for m in bnb_quantization_config.keep_in_fp32_modules) + } + ) + + kwargs = {} + kwargs["special_dtypes"] = special_dtypes + kwargs["no_split_module_classes"] = no_split_module_classes + kwargs["dtype"] = bnb_quantization_config.target_dtype + + # get max_memory for each device. + if device_map != "sequential": + max_memory = get_balanced_memory( + model, + low_zero=(device_map == "balanced_low_0"), + max_memory=max_memory, + **kwargs, + ) + + kwargs["max_memory"] = max_memory + device_map = infer_auto_device_map(model, **kwargs) + + if isinstance(device_map, dict): + # check if don't have any quantized module on the cpu + modules_not_to_convert = bnb_quantization_config.skip_modules + bnb_quantization_config.keep_in_fp32_modules + + device_map_without_some_modules = { + key: device_map[key] for key in device_map.keys() if key not in modules_not_to_convert + } + for device in ["cpu", "disk"]: + if device in device_map_without_some_modules.values(): + if bnb_quantization_config.load_in_4bit: + raise ValueError( + """ + Some modules are dispatched on the CPU or the disk. Make sure you have enough GPU RAM to fit + the quantized model. If you want to dispatch the model on the CPU or the disk while keeping + these modules in `torch_dtype`, you need to pass a custom `device_map` to + `load_and_quantize_model`. Check + https://huggingface.co/docs/accelerate/main/en/usage_guides/quantization#offload-modules-to-cpu-and-disk + for more details. + """ + ) + else: + logger.info( + "Some modules are are offloaded to the CPU or the disk. Note that these modules will be converted to 8-bit" + ) + del device_map_without_some_modules + return device_map + + +def replace_with_bnb_layers(model, bnb_quantization_config, modules_to_not_convert=None, current_key_name=None): + """ + A helper function to replace all `torch.nn.Linear` modules by `bnb.nn.Linear8bit` modules or by `bnb.nn.Linear4bit` + modules from the `bitsandbytes`library. The function will be run recursively and replace `torch.nn.Linear` modules. + + Parameters: + model (`torch.nn.Module`): + Input model or `torch.nn.Module` as the function is run recursively. + modules_to_not_convert (`List[str]`): + Names of the modules to not quantize convert. In practice we keep the `lm_head` in full precision for + numerical stability reasons. + current_key_name (`List[str]`, *optional*): + An array to track the current key of the recursion. This is used to check whether the current key (part of + it) is not in the list of modules to not convert. + """ + + if modules_to_not_convert is None: + modules_to_not_convert = [] + + model, has_been_replaced = _replace_with_bnb_layers( + model, bnb_quantization_config, modules_to_not_convert, current_key_name + ) + if not has_been_replaced: + logger.warning( + "You are loading your model in 8bit or 4bit but no linear modules were found in your model." + " this can happen for some architectures such as gpt2 that uses Conv1D instead of Linear layers." + " Please double check your model architecture, or submit an issue on github if you think this is" + " a bug." + ) + return model + + +def _replace_with_bnb_layers( + model, + bnb_quantization_config, + modules_to_not_convert=None, + current_key_name=None, +): + """ + Private method that wraps the recursion for module replacement. + + Returns the converted model and a boolean that indicates if the conversion has been successful or not. + """ + # bitsandbytes will initialize device(e.g. CUDA, XPU) on import, so it needs to be imported lazily + import bitsandbytes as bnb + + has_been_replaced = False + for name, module in model.named_children(): + if current_key_name is None: + current_key_name = [] + current_key_name.append(name) + if isinstance(module, nn.Linear) and name not in modules_to_not_convert: + # Check if the current key is not in the `modules_to_not_convert` + current_key_name_str = ".".join(current_key_name) + proceed = True + for key in modules_to_not_convert: + if ( + (key in current_key_name_str) and (key + "." in current_key_name_str) + ) or key == current_key_name_str: + proceed = False + break + if proceed: + # Load bnb module with empty weight and replace ``nn.Linear` module + if bnb_quantization_config.load_in_8bit: + bnb_module = bnb.nn.Linear8bitLt( + module.in_features, + module.out_features, + module.bias is not None, + has_fp16_weights=False, + threshold=bnb_quantization_config.llm_int8_threshold, + ) + elif bnb_quantization_config.load_in_4bit: + bnb_module = bnb.nn.Linear4bit( + module.in_features, + module.out_features, + module.bias is not None, + bnb_quantization_config.bnb_4bit_compute_dtype, + compress_statistics=bnb_quantization_config.bnb_4bit_use_double_quant, + quant_type=bnb_quantization_config.bnb_4bit_quant_type, + ) + else: + raise ValueError("load_in_8bit and load_in_4bit can't be both False") + bnb_module.weight.data = module.weight.data + if module.bias is not None: + bnb_module.bias.data = module.bias.data + bnb_module.requires_grad_(False) + setattr(model, name, bnb_module) + has_been_replaced = True + if len(list(module.children())) > 0: + _, _has_been_replaced = _replace_with_bnb_layers( + module, bnb_quantization_config, modules_to_not_convert, current_key_name + ) + has_been_replaced = has_been_replaced | _has_been_replaced + # Remove the last key for recursion + current_key_name.pop(-1) + return model, has_been_replaced + + +def get_keys_to_not_convert(model): + r""" + An utility function to get the key of the module to keep in full precision if any For example for CausalLM modules + we may want to keep the lm_head in full precision for numerical stability reasons. For other architectures, we want + to keep the tied weights of the model. The function will return a list of the keys of the modules to not convert in + int8. + + Parameters: + model (`torch.nn.Module`): + Input model + """ + # Create a copy of the model + with init_empty_weights(): + tied_model = deepcopy(model) # this has 0 cost since it is done inside `init_empty_weights` context manager` + + tied_params = find_tied_parameters(tied_model) + # For compatibility with Accelerate < 0.18 + if isinstance(tied_params, dict): + tied_keys = sum(list(tied_params.values()), []) + list(tied_params.keys()) + else: + tied_keys = sum(tied_params, []) + has_tied_params = len(tied_keys) > 0 + + # Check if it is a base model + is_base_model = False + if hasattr(model, "base_model_prefix"): + is_base_model = not hasattr(model, model.base_model_prefix) + + # Ignore this for base models (BertModel, GPT2Model, etc.) + if (not has_tied_params) and is_base_model: + return [] + + # otherwise they have an attached head + list_modules = list(model.named_children()) + list_last_module = [list_modules[-1][0]] + + # add last module together with tied weights + intersection = set(list_last_module) - set(tied_keys) + list_untouched = list(set(tied_keys)) + list(intersection) + + # remove ".weight" from the keys + names_to_remove = [".weight", ".bias"] + filtered_module_names = [] + for name in list_untouched: + for name_to_remove in names_to_remove: + if name_to_remove in name: + name = name.replace(name_to_remove, "") + filtered_module_names.append(name) + + return filtered_module_names + + +def has_4bit_bnb_layers(model): + """Check if we have `bnb.nn.Linear4bit` or `bnb.nn.Linear8bitLt` layers inside our model""" + # bitsandbytes will initialize device(e.g. CUDA, XPU) on import, so it needs to be imported lazily + import bitsandbytes as bnb + + for m in model.modules(): + if isinstance(m, bnb.nn.Linear4bit): + return True + return False + + +def get_parameter_device(parameter: nn.Module): + return next(parameter.parameters()).device + + +def quantize_and_offload_8bit(model, param, param_name, new_dtype, offload_folder, offload_index, fp16_statistics): + # if it is not quantized, we quantize and offload the quantized weights and the SCB stats + if fp16_statistics is None: + set_module_tensor_to_device(model, param_name, 0, dtype=new_dtype, value=param) + tensor_name = param_name + module = model + if "." in tensor_name: + splits = tensor_name.split(".") + for split in splits[:-1]: + new_module = getattr(module, split) + if new_module is None: + raise ValueError(f"{module} has no attribute {split}.") + module = new_module + tensor_name = splits[-1] + # offload weights + module._parameters[tensor_name].requires_grad = False + offload_weight(module._parameters[tensor_name], param_name, offload_folder, index=offload_index) + if hasattr(module._parameters[tensor_name], "SCB"): + offload_weight( + module._parameters[tensor_name].SCB, + param_name.replace("weight", "SCB"), + offload_folder, + index=offload_index, + ) + else: + offload_weight(param, param_name, offload_folder, index=offload_index) + offload_weight(fp16_statistics, param_name.replace("weight", "SCB"), offload_folder, index=offload_index) + + set_module_tensor_to_device(model, param_name, "meta", dtype=new_dtype, value=torch.empty(*param.size())) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/constants.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..0eba7bc3d61cb012366f43a31873871f6d29a83d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/constants.py @@ -0,0 +1,108 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import operator as op + +import torch + + +SCALER_NAME = "scaler.pt" +MODEL_NAME = "pytorch_model" +SAFE_MODEL_NAME = "model" +RNG_STATE_NAME = "random_states" +OPTIMIZER_NAME = "optimizer" +SCHEDULER_NAME = "scheduler" +SAMPLER_NAME = "sampler" +PROFILE_PATTERN_NAME = "profile_{suffix}.json" +WEIGHTS_NAME = f"{MODEL_NAME}.bin" +WEIGHTS_PATTERN_NAME = "pytorch_model{suffix}.bin" +WEIGHTS_INDEX_NAME = f"{WEIGHTS_NAME}.index.json" +SAFE_WEIGHTS_NAME = f"{SAFE_MODEL_NAME}.safetensors" +SAFE_WEIGHTS_PATTERN_NAME = "model{suffix}.safetensors" +SAFE_WEIGHTS_INDEX_NAME = f"{SAFE_WEIGHTS_NAME}.index.json" +SAGEMAKER_PYTORCH_VERSION = "1.10.2" +SAGEMAKER_PYTHON_VERSION = "py38" +SAGEMAKER_TRANSFORMERS_VERSION = "4.17.0" +SAGEMAKER_PARALLEL_EC2_INSTANCES = ["ml.p3.16xlarge", "ml.p3dn.24xlarge", "ml.p4dn.24xlarge"] +FSDP_SHARDING_STRATEGY = ["FULL_SHARD", "SHARD_GRAD_OP", "NO_SHARD", "HYBRID_SHARD", "HYBRID_SHARD_ZERO2"] +FSDP_AUTO_WRAP_POLICY = ["TRANSFORMER_BASED_WRAP", "SIZE_BASED_WRAP", "NO_WRAP"] +FSDP_BACKWARD_PREFETCH = ["BACKWARD_PRE", "BACKWARD_POST", "NO_PREFETCH"] +FSDP_STATE_DICT_TYPE = ["FULL_STATE_DICT", "LOCAL_STATE_DICT", "SHARDED_STATE_DICT"] +FSDP2_STATE_DICT_TYPE = ["SHARDED_STATE_DICT", "FULL_STATE_DICT"] +FSDP_PYTORCH_VERSION = ( + "2.1.0.a0+32f93b1" # Technically should be 2.1.0, but MS-AMP uses this specific prerelease in their Docker image. +) +FSDP2_PYTORCH_VERSION = "2.6.0" +DTENSOR_PYTORCH_VERSION = "2.5.0" +FSDP_MODEL_NAME = "pytorch_model_fsdp" +DEEPSPEED_MULTINODE_LAUNCHERS = ["pdsh", "standard", "openmpi", "mvapich", "mpich", "nossh", "slurm"] +TORCH_DYNAMO_MODES = ["default", "reduce-overhead", "max-autotune"] +ELASTIC_LOG_LINE_PREFIX_TEMPLATE_PYTORCH_VERSION = "2.2.0" +XPU_PROFILING_AVAILABLE_PYTORCH_VERSION = "2.4.0" +MITA_PROFILING_AVAILABLE_PYTORCH_VERSION = "2.1.0" +BETA_TP_AVAILABLE_PYTORCH_VERSION = "2.3.0" + +BETA_TP_AVAILABLE_TRANSFORMERS_VERSION = "4.52.0" +BETA_CP_AVAILABLE_PYTORCH_VERSION = "2.6.0" +BETA_SP_AVAILABLE_DEEPSPEED_VERSION = "0.18.2" + +STR_OPERATION_TO_FUNC = {">": op.gt, ">=": op.ge, "==": op.eq, "!=": op.ne, "<=": op.le, "<": op.lt} + +# These are the args for `torch.distributed.launch` for pytorch < 1.9 +TORCH_LAUNCH_PARAMS = [ + "nnodes", + "nproc_per_node", + "rdzv_backend", + "rdzv_endpoint", + "rdzv_id", + "rdzv_conf", + "standalone", + "max_restarts", + "monitor_interval", + "start_method", + "role", + "module", + "m", + "no_python", + "run_path", + "log_dir", + "r", + "redirects", + "t", + "tee", + "node_rank", + "master_addr", + "master_port", +] + +CUDA_DISTRIBUTED_TYPES = ["DEEPSPEED", "MULTI_GPU", "FSDP", "MEGATRON_LM", "TP"] +TORCH_DISTRIBUTED_OPERATION_TYPES = CUDA_DISTRIBUTED_TYPES + [ + "MULTI_NPU", + "MULTI_MLU", + "MULTI_SDAA", + "MULTI_MUSA", + "MULTI_XPU", + "MULTI_CPU", + "MULTI_HPU", + "MULTI_NEURON", +] +SUPPORTED_PYTORCH_LAYERS_FOR_UPCASTING = ( + torch.nn.Conv1d, + torch.nn.Conv2d, + torch.nn.Conv3d, + torch.nn.ConvTranspose1d, + torch.nn.ConvTranspose2d, + torch.nn.ConvTranspose3d, + torch.nn.Linear, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/dataclasses.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/dataclasses.py new file mode 100644 index 0000000000000000000000000000000000000000..43e1d904886523744c75245d539fe3a74c73b3f9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/dataclasses.py @@ -0,0 +1,3199 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +General namespace and dataclass related classes +""" + +import argparse +import copy +import enum +import functools +import logging +import os +import warnings +from collections.abc import Iterable +from contextlib import contextmanager +from dataclasses import dataclass, field +from datetime import timedelta +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, get_args + +import torch + +from .constants import ( + BETA_CP_AVAILABLE_PYTORCH_VERSION, + BETA_TP_AVAILABLE_PYTORCH_VERSION, + BETA_TP_AVAILABLE_TRANSFORMERS_VERSION, + FSDP2_PYTORCH_VERSION, + FSDP_AUTO_WRAP_POLICY, + FSDP_BACKWARD_PREFETCH, + FSDP_SHARDING_STRATEGY, + MITA_PROFILING_AVAILABLE_PYTORCH_VERSION, + XPU_PROFILING_AVAILABLE_PYTORCH_VERSION, +) +from .environment import parse_flag_from_env, str_to_bool +from .imports import ( + is_cuda_available, + is_hpu_available, + is_mlu_available, + is_msamp_available, + is_musa_available, + is_npu_available, + is_torchao_available, + is_transformer_engine_available, + is_xpu_available, +) +from .versions import compare_versions, is_torch_version + + +if TYPE_CHECKING: + # Mock imports for type checking + from torchao.float8 import Float8LinearConfig + + +logger = logging.getLogger(__name__) + + +class KwargsHandler: + """ + Internal mixin that implements a `to_kwargs()` method for a dataclass. + """ + + def to_dict(self): + return copy.deepcopy(self.__dict__) + + def to_kwargs(self): + """ + Returns a dictionary containing the attributes with values different from the default of this class. + """ + # import clear_environment here to avoid circular import problem + from .environment import clear_environment + + with clear_environment(): + default_dict = self.__class__().to_dict() + this_dict = self.to_dict() + return {k: v for k, v in this_dict.items() if default_dict[k] != v} + + +class EnumWithContains(enum.EnumMeta): + "A metaclass that adds the ability to check if `self` contains an item with the `in` operator" + + def __contains__(cls, item): + try: + cls(item) + except ValueError: + return False + return True + + +class BaseEnum(enum.Enum, metaclass=EnumWithContains): + "An enum class that can get the value of an item with `str(Enum.key)`" + + def __str__(self): + return self.value + + @classmethod + def list(cls): + "Method to list all the possible items in `cls`" + return list(map(str, cls)) + + +@dataclass +class AutocastKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize how `torch.autocast` behaves. Please refer to the + documentation of this [context manager](https://pytorch.org/docs/stable/amp.html#torch.autocast) for more + information on each argument. + + Example: + + ```python + from accelerate import Accelerator + from accelerate.utils import AutocastKwargs + + kwargs = AutocastKwargs(cache_enabled=True) + accelerator = Accelerator(kwargs_handlers=[kwargs]) + ``` + """ + + enabled: bool = True + cache_enabled: Optional[bool] = None + + +class DDPCommunicationHookType(BaseEnum): + """ + Represents a type of communication hook used in DDP. + + Values: + + - **NO** -- no communication hook + - **FP16** -- DDP communication hook to compress the gradients in FP16 + - **BF16** -- DDP communication hook to compress the gradients in BF16 + - **POWER_SGD** -- DDP communication hook to use PowerSGD + - **BATCHED_POWER_SGD** -- DDP communication hook to use batched PowerSGD + """ + + NO = "no" + FP16 = "fp16" + BF16 = "bf16" + POWER_SGD = "power_sgd" + BATCHED_POWER_SGD = "batched_power_sgd" + + +@dataclass +class DistributedDataParallelKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize how your model is wrapped in a + `torch.nn.parallel.DistributedDataParallel`. Please refer to the documentation of this + [wrapper](https://pytorch.org/docs/stable/generated/torch.nn.parallel.DistributedDataParallel.html) for more + information on each argument. + + + + `gradient_as_bucket_view` is only available in PyTorch 1.7.0 and later versions. + + `static_graph` is only available in PyTorch 1.11.0 and later versions. + + + + Example: + + ```python + from accelerate import Accelerator + from accelerate.utils import DistributedDataParallelKwargs + + kwargs = DistributedDataParallelKwargs(find_unused_parameters=True) + accelerator = Accelerator(kwargs_handlers=[kwargs]) + ``` + """ + + dim: int = 0 + broadcast_buffers: bool = True + bucket_cap_mb: int = 25 + find_unused_parameters: bool = False + check_reduction: bool = False + gradient_as_bucket_view: bool = False + static_graph: bool = False + + comm_hook: DDPCommunicationHookType = DDPCommunicationHookType.NO + comm_wrapper: Literal[ + DDPCommunicationHookType.NO, + DDPCommunicationHookType.FP16, + DDPCommunicationHookType.BF16, + ] = DDPCommunicationHookType.NO + comm_state_option: dict = field(default_factory=dict) + + def to_dict(self, ignore_keys=("comm_hook", "comm_wrapper", "comm_state_option")): + return {k: v for k, v in super().to_dict().items() if k not in ignore_keys} + + def register_comm_hook(self, model): + from torch.distributed.algorithms.ddp_comm_hooks import ( + default_hooks, + powerSGD_hook, + ) + + hook_map: dict[DDPCommunicationHookType, Callable] = { + DDPCommunicationHookType.FP16: default_hooks.fp16_compress_hook, + DDPCommunicationHookType.BF16: default_hooks.bf16_compress_hook, + DDPCommunicationHookType.POWER_SGD: powerSGD_hook.powerSGD_hook, + DDPCommunicationHookType.BATCHED_POWER_SGD: powerSGD_hook.batched_powerSGD_hook, + } + + wrapper_map: dict[DDPCommunicationHookType, Callable] = { + DDPCommunicationHookType.FP16: default_hooks.fp16_compress_wrapper, + DDPCommunicationHookType.BF16: default_hooks.bf16_compress_wrapper, + } + + hook: Optional[Callable] = hook_map.get(self.comm_hook) + wrapper: Optional[Callable] = wrapper_map.get(self.comm_wrapper) + + if hook and wrapper: + hook = wrapper(hook) + + if hook: + state = ( + powerSGD_hook.PowerSGDState(None, **self.comm_state_option) + if self.comm_hook + in ( + DDPCommunicationHookType.POWER_SGD, + DDPCommunicationHookType.BATCHED_POWER_SGD, + ) + else None + ) + model.register_comm_hook( + state=state, + hook=hook, + ) + + +@dataclass +class GradScalerKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the behavior of mixed precision, specifically how the + `torch.amp.GradScaler` or `torch.cuda.amp.GradScaler` used is created. Please refer to the documentation of this + [scaler](https://pytorch.org/docs/stable/amp.html?highlight=gradscaler) for more information on each argument. + + + + `torch.cuda.amp.GradScaler` is only available in PyTorch 1.5.0 and later versions, and `torch.amp.GradScaler` is + only available in PyTorch 2.4.0 and later versions. + + + + Example: + + ```python + from accelerate import Accelerator + from accelerate.utils import GradScalerKwargs + + kwargs = GradScalerKwargs(backoff_factor=0.25) + accelerator = Accelerator(kwargs_handlers=[kwargs]) + ``` + """ + + init_scale: float = 65536.0 + growth_factor: float = 2.0 + backoff_factor: float = 0.5 + growth_interval: int = 2000 + enabled: bool = True + + +@dataclass +class InitProcessGroupKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the initialization of the distributed processes. Please refer + to the documentation of this + [method](https://pytorch.org/docs/stable/distributed.html#torch.distributed.init_process_group) for more + information on each argument. + + Note: If `timeout` is set to `None`, the default will be based upon how `backend` is set. + + ```python + from datetime import timedelta + from accelerate import Accelerator + from accelerate.utils import InitProcessGroupKwargs + + kwargs = InitProcessGroupKwargs(timeout=timedelta(seconds=800)) + accelerator = Accelerator(kwargs_handlers=[kwargs]) + ``` + """ + + backend: Optional[str] = "nccl" + init_method: Optional[str] = None + timeout: Optional[timedelta] = None + + def __post_init__(self): + if self.timeout is None: + seconds = 1800 if self.backend != "nccl" else 600 + self.timeout = timedelta(seconds=seconds) + + +# Literals +Backend = Literal["MSAMP", "TE"] +OptLevel = Literal["O1", "O2"] +FP8Format = Literal["HYBRID", "E4M3", "E5M2"] +AmaxComputeAlgorithm = Literal["max", "most_recent"] + + +# FP8 training recipe kwargs +@dataclass +class AORecipeKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the initialization of the recipe for FP8 mixed precision + training with `torchao` FP8. + + Args: + config (`torchao.float8.Float8LinearConfig`, *optional*, default to `None`): + The configuration for the FP8 training. If `None`, a default config will be created with sensible + defaults for most use cases: + - `pad_inner_dim=True`: Pads matrix dimensions to be divisible by 16, required for `torch._scaled_mm` + operations to prevent runtime errors. + - `enable_fsdp_float8_all_gather=True`: Enables FP8 all-gather for FSDP2. This provides memory bandwidth + savings by casting parameters before the all-gather operation, saving 50% bandwidth compared to BF16. + + You can override these defaults by providing your own `Float8LinearConfig` instance. + module_filter_func (`Callable`, *optional*, default to `None`): + Optional function that must take in a module and layer name, and returns a boolean indicating whether the + module should be converted to FP8. Defaults to `accelerate.utils.ao.filter_linear_layers`. See it for an + example. + """ + + config: Optional["Float8LinearConfig"] = None + module_filter_func: Optional[Callable] = None + pad_inner_dim: Optional[bool] = None + enable_fsdp_float8_all_gather: Optional[bool] = None + + def __post_init__(self): + env_prefix = "ACCELERATE_FP8_" + if not is_torchao_available(): + raise ImportError("TorchAO is not available. Please install it or use a different backend.") + + if self.config is None: + from torchao.float8 import Float8LinearConfig + + # Check environment variables for overrides + if self.pad_inner_dim is None: + self.pad_inner_dim = parse_flag_from_env(env_prefix + "PAD_INNER_DIM", default=True) + if self.enable_fsdp_float8_all_gather is None: + self.enable_fsdp_float8_all_gather = parse_flag_from_env( + env_prefix + "ENABLE_FSDP_FLOAT8_ALL_GATHER", default=True + ) + self.config = Float8LinearConfig( + pad_inner_dim=self.pad_inner_dim, + enable_fsdp_float8_all_gather=self.enable_fsdp_float8_all_gather, + ) + + +@dataclass +class TERecipeKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the initialization of the recipe for FP8 mixed precision + training with `transformer-engine`. + + + + For more information on the args, please refer to the API + [documentation](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/api/common.html). + + + + ```python + from accelerate import Accelerator + from accelerate.utils import TERecipeKwargs + + kwargs = TERecipeKwargs(fp8_format="HYBRID") + accelerator = Accelerator(mixed_precision="fp8", kwargs_handlers=[kwargs]) + ``` + + Args: + use_autocast_during_eval (`bool`, *optional*, default to `False`): + Whether to use FP8 autocast during eval mode. Generally better metrics are found when this is `False`. + margin (`int`, *optional*, default to 0): + The margin to use for the gradient scaling. + interval (`int`, *optional*, default to 1): + The interval to use for how often the scaling factor is recomputed. + fp8_format (`str`, *optional*, default to "HYBRID"): + The format to use for the FP8 recipe. Must be one of `HYBRID`, `E4M3` or `E5M2`. (Generally `HYBRID` for + training, `E4M3` or `E5M2` for evaluation) + amax_history_len (`int`, *optional*, default to 1024): + The length of the history to use for the scaling factor computation + amax_compute_algo (`str`, *optional*, default to "most_recent"): + The algorithm to use for the scaling factor computation. Must be one of `max` or `most_recent`. + override_linear_precision (`tuple` of three `bool`, *optional*, default to `(False, False, False)`): + Whether or not to execute `fprop`, `dgrad`, and `wgrad` GEMMS in higher precision. + """ + + use_autocast_during_eval: Optional[bool] = None + margin: Optional[int] = None + interval: Optional[int] = None + fp8_format: FP8Format = None + amax_history_len: Optional[int] = None + amax_compute_algo: AmaxComputeAlgorithm = None + override_linear_precision: tuple[bool, bool, bool] = None + use_mxfp8_block_scaling: Optional[bool] = None + + def __post_init__(self): + env_prefix = "ACCELERATE_FP8_" + if not is_transformer_engine_available(): + raise ImportError("TransformerEngine is not available. Please install it or use a different backend.") + if self.use_autocast_during_eval is None: + self.use_autocast_during_eval = parse_flag_from_env(env_prefix + "USE_AUTOCAST_DURING_EVAL") + if self.margin is None: + self.margin = int(os.environ.get(env_prefix + "MARGIN", 0)) + if self.interval is None: + self.interval = int(os.environ.get(env_prefix + "INTERVAL", 1)) + if self.fp8_format is None: + self.fp8_format = os.environ.get(env_prefix + "FORMAT", "HYBRID") + self.fp8_format = self.fp8_format.upper() + if self.fp8_format not in get_args(FP8Format): + raise ValueError(f"`fp8_format` must be one of {' or '.join(get_args(FP8Format))}.") + if self.amax_compute_algo is None: + self.amax_compute_algo = os.environ.get(env_prefix + "AMAX_COMPUTE_ALGO", "most_recent") + self.amax_compute_algo = self.amax_compute_algo.lower() + if self.amax_compute_algo not in get_args(AmaxComputeAlgorithm): + raise ValueError(f"`amax_compute_algo` must be one of {' or '.join(get_args(AmaxComputeAlgorithm))}") + if self.amax_history_len is None: + self.amax_history_len = int(os.environ.get(env_prefix + "AMAX_HISTORY_LEN", 1024)) + if self.override_linear_precision is None: + fprop = parse_flag_from_env(env_prefix + "OVERRIDE_FPROP") + dgrad = parse_flag_from_env(env_prefix + "OVERRIDE_DGRAD") + wgrad = parse_flag_from_env(env_prefix + "OVERRIDE_WGRAD") + self.override_linear_precision = (fprop, dgrad, wgrad) + if self.use_mxfp8_block_scaling is None: + self.use_mxfp8_block_scaling = parse_flag_from_env(env_prefix + "USE_MXFP8_BLOCK_SCALING") + + +@dataclass +class MSAMPRecipeKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the initialization of the recipe for FP8 mixed precision + training with `ms-amp`. + """ + + opt_level: OptLevel = None + + def __post_init__(self): + env_prefix = "ACCELERATE_FP8_" + if self.opt_level is None: + self.opt_level = os.environ.get(env_prefix + "OPT_LEVEL", "O2") + if self.opt_level not in get_args(OptLevel): + raise ValueError(f"`opt_level` must be one of {' or '.join(get_args(OptLevel))}") + + +@dataclass +class FP8RecipeKwargs(TERecipeKwargs, MSAMPRecipeKwargs): + """ + Deprecated. Please use one of the proper FP8 recipe kwargs classes such as `TERecipeKwargs` or `MSAMPRecipeKwargs` + instead. + """ + + backend: Backend = None + + def __post_init__(self): + env_prefix = "ACCELERATE_FP8_" + warnings.warn( + "FP8RecipeKwargs is deprecated and will be removed in Accelerate v2.0.0. " + "Please use one of the proper FP8 recipe kwargs classes such as TERecipeKwargs or MSAMPRecipeKwargs instead.", + FutureWarning, + ) + default_backend = "msamp" if is_msamp_available() else "te" + if self.backend is None: + self.backend = os.environ.get(env_prefix + "BACKEND", default_backend) + self.backend = self.backend.upper() + if self.backend not in get_args(Backend): + raise ValueError("`backend` must be 'MSAMP' or 'TE' (TransformerEngine) to use `FP8RecipeKwargs`.") + super().__post_init__() + + +# Literal +ProfilerActivity = Literal["cpu", "xpu", "mtia", "cuda", "hpu"] + + +@dataclass +class ProfileKwargs(KwargsHandler): + """ + Use this object in your [`Accelerator`] to customize the initialization of the profiler. Please refer to the + documentation of this [context manager](https://pytorch.org/docs/stable/profiler.html#torch.profiler.profile) for + more information on each argument. + + + + `torch.profiler` is only available in PyTorch 1.8.1 and later versions. + + + + Example: + + ```python + from accelerate import Accelerator + from accelerate.utils import ProfileKwargs + + kwargs = ProfileKwargs(activities=["cpu", "cuda"]) + accelerator = Accelerator(kwargs_handlers=[kwargs]) + ``` + + Args: + activities (`List[str]`, *optional*, default to `None`): + The list of activity groups to use in profiling. Must be one of `"cpu"`, `"xpu"`, `"mtia"`, "hpu" or + `"cuda"`. + schedule_option (`Dict[str, int]`, *optional*, default to `None`): + The schedule option to use for the profiler. Available keys are `wait`, `warmup`, `active`, `repeat` and + `skip_first`. The profiler will skip the first `skip_first` steps, then wait for `wait` steps, then do the + warmup for the next `warmup` steps, then do the active recording for the next `active` steps and then + repeat the cycle starting with `wait` steps. The optional number of cycles is specified with the `repeat` + parameter, the zero value means that the cycles will continue until the profiling is finished. + on_trace_ready (`Callable`, *optional*, default to `None`): + Callable that is called at each step when schedule returns `ProfilerAction.RECORD_AND_SAVE` during the + profiling. + record_shapes (`bool`, *optional*, default to `False`): + Save information about operator’s input shapes. + profile_memory (`bool`, *optional*, default to `False`): + Track tensor memory allocation/deallocation + with_stack (`bool`, *optional*, default to `False`): + Record source information (file and line number) for the ops. + with_flops (`bool`, *optional*, default to `False`): + Use formula to estimate the FLOPS of specific operators + with_modules (`bool`, *optional*, default to `False`): + Record module hierarchy (including function names) corresponding to the callstack of the op. + output_trace_dir (`str`, *optional*, default to `None`): + Exports the collected trace in Chrome JSON format. Chrome use 'chrome://tracing' view json file. Defaults + to None, which means profiling does not store json files. + """ + + activities: Optional[list[ProfilerActivity]] = None + schedule_option: Optional[dict[str, int]] = None + on_trace_ready: Optional[Callable] = None + record_shapes: bool = False + profile_memory: bool = False + with_stack: bool = False + with_flops: bool = False + with_modules: bool = False + output_trace_dir: Optional[str] = None + + def _get_profiler_activity(self, activity: ProfilerActivity) -> torch.profiler.ProfilerActivity: + """Get the profiler activity from the string. + + Args: + activity (str): The profiler activity name. + + Returns: + torch.profiler.ProfilerActivity: The profiler activity. + """ + + profiler_activity_map: dict[str, torch.profiler.ProfilerActivity] = { + "cpu": torch.profiler.ProfilerActivity.CPU, + "cuda": torch.profiler.ProfilerActivity.CUDA, + } + + if is_hpu_available(): + profiler_activity_map["hpu"] = torch.profiler.ProfilerActivity.HPU + + if is_torch_version(">=", XPU_PROFILING_AVAILABLE_PYTORCH_VERSION): + if torch.xpu.is_available(): + profiler_activity_map["xpu"] = torch.profiler.ProfilerActivity.XPU + + if is_torch_version(">=", MITA_PROFILING_AVAILABLE_PYTORCH_VERSION): + if torch.mtia.is_available(): + profiler_activity_map["mtia"] = torch.profiler.ProfilerActivity.MTIA + + if activity not in profiler_activity_map: + raise ValueError(f"Invalid profiler activity: {activity}. Must be one of {list(profiler_activity_map)}.") + return profiler_activity_map[activity] + + def build(self) -> torch.profiler.profile: + """ + Build a profiler object with the current configuration. + + Returns: + torch.profiler.profile: The profiler object. + """ + activities: Optional[list[ProfilerActivity]] = None + if self.activities is not None: + activities = [self._get_profiler_activity(activity) for activity in self.activities] + schedule: Optional[torch.profiler.schedule] = None + if self.schedule_option is not None: + schedule = torch.profiler.schedule(**self.schedule_option) + + return torch.profiler.profile( + activities=activities, + schedule=schedule, + on_trace_ready=self.on_trace_ready, + record_shapes=self.record_shapes, + profile_memory=self.profile_memory, + with_stack=self.with_stack, + with_flops=self.with_flops, + with_modules=self.with_modules, + ) + + +class DistributedType(str, enum.Enum): + """ + Represents a type of distributed environment. + + Values: + + - **NO** -- Not a distributed environment, just a single process. + - **MULTI_CPU** -- Distributed on multiple CPU nodes. + - **MULTI_GPU** -- Distributed on multiple GPUs. + - **MULTI_MLU** -- Distributed on multiple MLUs. + - **MULTI_SDAA** -- Distributed on multiple SDAAs. + - **MULTI_MUSA** -- Distributed on multiple MUSAs. + - **MULTI_NPU** -- Distributed on multiple NPUs. + - **MULTI_XPU** -- Distributed on multiple XPUs. + - **MULTI_HPU** -- Distributed on multiple HPUs. + - **MULTI_NEURON** -- Distributed on multiple Neuron cores. + - **DEEPSPEED** -- Using DeepSpeed. + - **XLA** -- Using TorchXLA. + """ + + # Subclassing str as well as Enum allows the `DistributedType` to be JSON-serializable out of the box. + NO = "NO" + MULTI_CPU = "MULTI_CPU" + MULTI_GPU = "MULTI_GPU" + MULTI_NPU = "MULTI_NPU" + MULTI_MLU = "MULTI_MLU" + MULTI_SDAA = "MULTI_SDAA" + MULTI_MUSA = "MULTI_MUSA" + MULTI_XPU = "MULTI_XPU" + DEEPSPEED = "DEEPSPEED" + FSDP = "FSDP" + XLA = "XLA" + MEGATRON_LM = "MEGATRON_LM" + MULTI_HPU = "MULTI_HPU" + MULTI_NEURON = "MULTI_NEURON" + + +class SageMakerDistributedType(str, enum.Enum): + """ + Represents a type of distributed environment. + + Values: + + - **NO** -- Not a distributed environment, just a single process. + - **DATA_PARALLEL** -- using sagemaker distributed data parallelism. + - **MODEL_PARALLEL** -- using sagemaker distributed model parallelism. + """ + + # Subclassing str as well as Enum allows the `SageMakerDistributedType` to be JSON-serializable out of the box. + NO = "NO" + DATA_PARALLEL = "DATA_PARALLEL" + MODEL_PARALLEL = "MODEL_PARALLEL" + + +class FP8BackendType(str, enum.Enum): + """ + Represents the backend used for FP8. + + Values: + + - **TE** -- using TransformerEngine. + - **MSAMP** -- using msamp. + """ + + # Subclassing str as well as Enum allows the `FP8BackendType` to be JSON-serializable out of the box. + NO = "NO" + TE = "TE" + MSAMP = "MSAMP" + AO = "AO" + + +class ComputeEnvironment(str, enum.Enum): + """ + Represents a type of the compute environment. + + Values: + + - **LOCAL_MACHINE** -- private/custom cluster hardware. + - **AMAZON_SAGEMAKER** -- Amazon SageMaker as compute environment. + """ + + # Subclassing str as well as Enum allows the `ComputeEnvironment` to be JSON-serializable out of the box. + LOCAL_MACHINE = "LOCAL_MACHINE" + AMAZON_SAGEMAKER = "AMAZON_SAGEMAKER" + + +class DynamoBackend(str, BaseEnum): + """ + Represents a dynamo backend (see https://pytorch.org/docs/stable/torch.compiler.html). + + Values: + + - **NO** -- Do not use torch dynamo. + - **EAGER** -- Uses PyTorch to run the extracted GraphModule. This is quite useful in debugging TorchDynamo + issues. + - **AOT_EAGER** -- Uses AotAutograd with no compiler, i.e, just using PyTorch eager for the AotAutograd's + extracted forward and backward graphs. This is useful for debugging, and unlikely to give speedups. + - **INDUCTOR** -- Uses TorchInductor backend with AotAutograd and cudagraphs by leveraging codegened Triton + kernels. [Read + more](https://dev-discuss.pytorch.org/t/torchinductor-a-pytorch-native-compiler-with-define-by-run-ir-and-symbolic-shapes/747) + - **AOT_TS_NVFUSER** -- nvFuser with AotAutograd/TorchScript. [Read + more](https://dev-discuss.pytorch.org/t/tracing-with-primitives-update-1-nvfuser-and-its-primitives/593) + - **NVPRIMS_NVFUSER** -- nvFuser with PrimTorch. [Read + more](https://dev-discuss.pytorch.org/t/tracing-with-primitives-update-1-nvfuser-and-its-primitives/593) + - **CUDAGRAPHS** -- cudagraphs with AotAutograd. [Read more](https://github.com/pytorch/torchdynamo/pull/757) + - **OFI** -- Uses Torchscript optimize_for_inference. Inference only. [Read + more](https://pytorch.org/docs/stable/generated/torch.jit.optimize_for_inference.html) + - **FX2TRT** -- Uses Nvidia TensorRT for inference optimizations. Inference only. [Read + more](https://github.com/pytorch/TensorRT/blob/master/docsrc/tutorials/getting_started_with_fx_path.rst) + - **ONNXRT** -- Uses ONNXRT for inference on CPU/GPU. Inference only. [Read more](https://onnxruntime.ai/) + - **TENSORRT** -- Uses ONNXRT to run TensorRT for inference optimizations. [Read + more](https://github.com/onnx/onnx-tensorrt) + - **AOT_TORCHXLA_TRACE_ONCE** -- Uses Pytorch/XLA with TorchDynamo optimization, for training. [Read + more](https://github.com/pytorch/xla/blob/r2.0/docs/dynamo.md) + - **TORCHXLA_TRACE_ONCE** -- Uses Pytorch/XLA with TorchDynamo optimization, for inference. [Read + more](https://github.com/pytorch/xla/blob/r2.0/docs/dynamo.md) + - **TVM** -- Uses Apache TVM for inference optimizations. [Read more](https://tvm.apache.org/) + - **HPU_BACKEND** -- Uses HPU backend for inference optimizations. + + """ + + # Subclassing str as well as Enum allows the `SageMakerDistributedType` to be JSON-serializable out of the box. + NO = "NO" + EAGER = "EAGER" + AOT_EAGER = "AOT_EAGER" + INDUCTOR = "INDUCTOR" + AOT_TS_NVFUSER = "AOT_TS_NVFUSER" + NVPRIMS_NVFUSER = "NVPRIMS_NVFUSER" + CUDAGRAPHS = "CUDAGRAPHS" + OFI = "OFI" + FX2TRT = "FX2TRT" + ONNXRT = "ONNXRT" + TENSORRT = "TENSORRT" + AOT_TORCHXLA_TRACE_ONCE = "AOT_TORCHXLA_TRACE_ONCE" + TORCHXLA_TRACE_ONCE = "TORCHXLA_TRACE_ONCE" + TVM = "TVM" + HPU_BACKEND = "HPU_BACKEND" + + +class LoggerType(BaseEnum): + """Represents a type of supported experiment tracker + + Values: + + - **ALL** -- all available trackers in the environment that are supported + - **TENSORBOARD** -- TensorBoard as an experiment tracker + - **WANDB** -- wandb as an experiment tracker + - **TRACKIO** -- trackio as an experiment tracker + - **COMETML** -- comet_ml as an experiment tracker + - **MLFLOW** -- mlflow as an experiment tracker + - **CLEARML** -- clearml as an experiment tracker + - **DVCLIVE** -- dvclive as an experiment tracker + - **SWANLAB** -- swanlab as an experiment tracker + """ + + ALL = "all" + AIM = "aim" + TENSORBOARD = "tensorboard" + WANDB = "wandb" + TRACKIO = "trackio" + COMETML = "comet_ml" + MLFLOW = "mlflow" + CLEARML = "clearml" + DVCLIVE = "dvclive" + SWANLAB = "swanlab" + + +class PrecisionType(str, BaseEnum): + """Represents a type of precision used on floating point values + + Values: + + - **NO** -- using full precision (FP32) + - **FP16** -- using half precision + - **BF16** -- using brain floating point precision + """ + + NO = "no" + FP8 = "fp8" + FP16 = "fp16" + BF16 = "bf16" + + +class RNGType(BaseEnum): + TORCH = "torch" + CUDA = "cuda" + MLU = "mlu" + SDAA = "sdaa" + MUSA = "musa" + NPU = "npu" + XLA = "xla" + XPU = "xpu" + HPU = "hpu" + NEURON = "neuron" + GENERATOR = "generator" + + +class CustomDtype(enum.Enum): + r""" + An enum that contains multiple custom dtypes that can be used for `infer_auto_device_map`. + """ + + FP8 = "fp8" + INT4 = "int4" + INT2 = "int2" + + +# data classes + + +@dataclass +class TensorInformation: + shape: torch.Size + dtype: torch.dtype + + +@dataclass +class DataLoaderConfiguration: + """ + Configuration for dataloader-related items when calling `accelerator.prepare`. + + Args: + split_batches (`bool`, defaults to `False`): + Whether or not the accelerator should split the batches yielded by the dataloaders across the devices. If + `True`, the actual batch size used will be the same on any kind of distributed processes, but it must be a + round multiple of `num_processes` you are using. If `False`, actual batch size used will be the one set in + your script multiplied by the number of processes. + dispatch_batches (`bool`, defaults to `None`): + If set to `True`, the dataloader prepared by the Accelerator is only iterated through on the main process + and then the batches are split and broadcast to each process. Will default to `True` for `DataLoader` whose + underlying dataset is an `IterableDataset`, `False` otherwise. + even_batches (`bool`, defaults to `True`): + If set to `True`, in cases where the total batch size across all processes does not exactly divide the + dataset, samples at the start of the dataset will be duplicated so the batch can be divided equally among + all workers. + use_seedable_sampler (`bool`, defaults to `False`): + Whether or not use a fully seedable random sampler ([`data_loader.SeedableRandomSampler`]). Ensures + training results are fully reproducible using a different sampling technique. While seed-to-seed results + may differ, on average the differences are negligible when using multiple different seeds to compare. + Should also be ran with [`~utils.set_seed`] for the best results. + data_seed (`int`, defaults to `None`): + The seed to use for the underlying generator when using `use_seedable_sampler`. If `None`, the generator + will use the current default seed from torch. + non_blocking (`bool`, defaults to `False`): + If set to `True`, the dataloader prepared by the Accelerator will utilize non-blocking host-to-device + transfers, allowing for better overlap between dataloader communication and computation. Recommended that + the prepared dataloader has `pin_memory` set to `True` to work properly. + use_stateful_dataloader (`bool`, defaults to `False`): + If set to `True`, the dataloader prepared by the Accelerator will be backed by + [torchdata.StatefulDataLoader](https://github.com/pytorch/data/tree/main/torchdata/stateful_dataloader). + This requires `torchdata` version 0.8.0 or higher that supports StatefulDataLoader to be installed. + """ + + split_batches: bool = field( + default=False, + metadata={ + "help": "Whether or not the accelerator should split the batches yielded by the dataloaders across the devices. If" + " `True` the actual batch size used will be the same on any kind of distributed processes, but it must be a" + " round multiple of the `num_processes` you are using. If `False`, actual batch size used will be the one set" + " in your script multiplied by the number of processes." + }, + ) + dispatch_batches: bool = field( + default=None, + metadata={ + "help": "If set to `True`, the dataloader prepared by the Accelerator is only iterated through on the main process" + " and then the batches are split and broadcast to each process. Will default to `True` for `DataLoader` whose" + " underlying dataset is an `IterableDataset`, `False` otherwise." + }, + ) + even_batches: bool = field( + default=True, + metadata={ + "help": "If set to `True`, in cases where the total batch size across all processes does not exactly divide the" + " dataset, samples at the start of the dataset will be duplicated so the batch can be divided equally among" + " all workers." + }, + ) + use_seedable_sampler: bool = field( + default=False, + metadata={ + "help": "Whether or not use a fully seedable random sampler ([`data_loader.SeedableRandomSampler`])." + "Ensures training results are fully reproducible using a different sampling technique. " + "While seed-to-seed results may differ, on average the differences are negligible when using" + "multiple different seeds to compare. Should also be ran with [`~utils.set_seed`] for the best results." + }, + ) + data_seed: int = field( + default=None, + metadata={ + "help": "The seed to use for the underlying generator when using `use_seedable_sampler`. If `None`, the generator" + " will use the current default seed from torch." + }, + ) + non_blocking: bool = field( + default=False, + metadata={ + "help": "If set to `True`, the dataloader prepared by the Accelerator will utilize non-blocking host-to-device" + " transfers, allowing for better overlap between dataloader communication and computation. Recommended that the" + " prepared dataloader has `pin_memory` set to `True` to work properly." + }, + ) + use_stateful_dataloader: bool = field( + default=False, + metadata={ + "help": "If set to `True`, the dataloader prepared by the Accelerator will be backed by " + "[torchdata.StatefulDataLoader](https://github.com/pytorch/data/tree/main/torchdata/stateful_dataloader). This requires `torchdata` version 0.8.0 or higher that supports StatefulDataLoader to be installed." + }, + ) + + +@dataclass +class ProjectConfiguration: + """ + Configuration for the Accelerator object based on inner-project needs. + + Args: + project_dir (`str`, defaults to `None`): + A path to a directory for storing data. + logging_dir (`str`, defaults to `None`): + A path to a directory for storing logs of locally-compatible loggers. If None, defaults to `project_dir`. + automatic_checkpoint_naming (`bool`, defaults to `False`): + Whether saved states should be automatically iteratively named. + total_limit (`int`, defaults to `None`): + The maximum number of total saved states to keep. + iteration (`int`, defaults to `0`): + The current save iteration. + save_on_each_node (`bool`, defaults to `False`): + When doing multi-node distributed training, whether to save models and checkpoints on each node, or only on + the main one. + """ + + project_dir: str = field(default=None, metadata={"help": "A path to a directory for storing data."}) + logging_dir: str = field( + default=None, + metadata={ + "help": "A path to a directory for storing logs of locally-compatible loggers. If None, defaults to `project_dir`." + }, + ) + automatic_checkpoint_naming: bool = field( + default=False, + metadata={"help": "Whether saved states should be automatically iteratively named."}, + ) + + total_limit: int = field( + default=None, + metadata={"help": "The maximum number of total saved states to keep."}, + ) + + iteration: int = field( + default=0, + metadata={"help": "The current save iteration."}, + ) + + save_on_each_node: bool = field( + default=False, + metadata={ + "help": ( + "When doing multi-node distributed training, whether to save models and checkpoints on each node, or" + " only on the main one" + ) + }, + ) + + def set_directories(self, project_dir: Optional[str] = None): + "Sets `self.project_dir` and `self.logging_dir` to the appropriate values." + self.project_dir = project_dir + if self.logging_dir is None: + self.logging_dir = project_dir + + def __post_init__(self): + self.set_directories(self.project_dir) + + +@dataclass +class GradientAccumulationPlugin(KwargsHandler): + """ + A plugin to configure gradient accumulation behavior. You can only pass one of `gradient_accumulation_plugin` or + `gradient_accumulation_steps` to [`Accelerator`]. Passing both raises an error. + + Parameters: + num_steps (`int`): + The number of steps to accumulate gradients for. + adjust_scheduler (`bool`, *optional*, defaults to `True`): + Whether to adjust the scheduler steps to account for the number of steps being accumulated. Should be + `True` if the used scheduler was not adjusted for gradient accumulation. + sync_with_dataloader (`bool`, *optional*, defaults to `True`): + Whether to synchronize setting the gradients when at the end of the dataloader. + sync_each_batch (`bool`, *optional*): + Whether to synchronize setting the gradients at each data batch. Setting to `True` may reduce memory + requirements when using gradient accumulation with distributed training, at expense of speed. + + Example: + + ```python + from accelerate.utils import GradientAccumulationPlugin + + gradient_accumulation_plugin = GradientAccumulationPlugin(num_steps=2) + accelerator = Accelerator(gradient_accumulation_plugin=gradient_accumulation_plugin) + ``` + """ + + num_steps: int = field( + default=None, + metadata={"help": "The number of steps to accumulate gradients for."}, + ) + adjust_scheduler: bool = field( + default=True, + metadata={ + "help": "Whether to adjust the scheduler steps to account for the number of steps being accumulated. Should be `True` if the used scheduler was not adjusted for gradient accumulation." + }, + ) + sync_with_dataloader: bool = field( + default=True, + metadata={ + "help": "Whether to synchronize setting the gradients when at the end of the dataloader. Should only be set to `False` if you know what you're doing." + }, + ) + sync_each_batch: bool = field( + default=False, + metadata={ + "help": "Whether to synchronize setting the gradients at each data batch. Setting to `True` may reduce memory requirements when using gradient accumulation with distributed training, at expense of speed." + }, + ) + + +@dataclass +class TorchDynamoPlugin(KwargsHandler): + """ + This plugin is used to compile a model with PyTorch 2.0 + + Args: + backend (`DynamoBackend`, defaults to `None`): + A valid Dynamo backend. See https://pytorch.org/docs/stable/torch.compiler.html for more details. + mode (`str`, defaults to `None`): + Possible options are 'default', 'reduce-overhead' or 'max-autotune'. + fullgraph (`bool`, defaults to `None`): + Whether it is ok to break model into several subgraphs. + dynamic (`bool`, defaults to `None`): + Whether to use dynamic shape for tracing. + options (`Any`, defaults to `None`): + A dictionary of options to pass to the backend. + disable (`bool`, defaults to `False`): + Turn torch.compile() into a no-op for testing + use_regional_compilation (`bool`, defaults to `None`): + Use it to reduce the cold start compilation time of torch.compile() by targeting repeated blocks of the + same class and compiling them sequentially to hit the compiler's cache. For example, in `GPT2LMHeadModel`, + the repeated block/class is `GPT2Block`, and can be accessed as `model.transformer.h[0]`. The rest of the + model (e.g model.lm_head) is compiled separately. + """ + + backend: DynamoBackend = field( + default=None, + metadata={"help": f"Possible options are {[b.value.lower() for b in DynamoBackend]}"}, + ) + mode: str = field( + default=None, + metadata={"help": "Possible options are 'default', 'reduce-overhead' or 'max-autotune'"}, + ) + fullgraph: bool = field( + default=None, + metadata={"help": "Whether it is ok to break model into several subgraphs"}, + ) + dynamic: bool = field(default=None, metadata={"help": "Whether to use dynamic shape for tracing"}) + options: Any = field( + default=None, + metadata={"help": "A dictionary of options to pass to the backend."}, + ) + disable: bool = field( + default=False, + metadata={"help": "Turn torch.compile() into a no-op for testing"}, + ) + + use_regional_compilation: bool = field( + default=None, + metadata={ + "help": ( + # https://pytorch.org/tutorials/recipes/regional_compilation.html + "Use it to reduce the cold start compilation time of torch.compile() by targeting repeated " + "blocks of the same class and compiling them sequentially to hit the compiler's cache. For " + "example, in `GPT2LMHeadModel`, the repeated block/class is `GPT2Block`, and can be accessed " + "as `model.transformer.h[0]`. The rest of the model (e.g model.lm_head) is compiled separately." + ) + }, + ) + + def __post_init__(self): + prefix = "ACCELERATE_DYNAMO_" + if self.backend is None: + self.backend = os.environ.get(prefix + "BACKEND", "no") + self.backend = DynamoBackend(self.backend.upper()) + + if self.mode is None: + self.mode = os.environ.get(prefix + "MODE", "default") + if self.fullgraph is None: + self.fullgraph = str_to_bool(os.environ.get(prefix + "USE_FULLGRAPH", "False")) == 1 + if self.use_regional_compilation is None: + self.use_regional_compilation = ( + str_to_bool(os.environ.get(prefix + "USE_REGIONAL_COMPILATION", "False")) == 1 + ) + + if self.dynamic is None and os.environ.get(prefix + "USE_DYNAMIC", None) is not None: + self.dynamic = str_to_bool(os.environ.get(prefix + "USE_DYNAMIC", "False")) == 1 + + def to_dict(self): + dynamo_config = copy.deepcopy(self.__dict__) + dynamo_config["backend"] = dynamo_config["backend"].value.lower() + return dynamo_config + + def to_kwargs(self): + kwargs = super().to_kwargs() + kwargs.pop("use_regional_compilation", None) + return kwargs + + +@dataclass +class DeepSpeedPlugin: + """ + This plugin is used to integrate DeepSpeed. + + Args: + hf_ds_config (`Any`, defaults to `None`): + Path to DeepSpeed config file or dict or an object of class `accelerate.utils.deepspeed.HfDeepSpeedConfig`. + gradient_accumulation_steps (`int`, defaults to `None`): + Number of steps to accumulate gradients before updating optimizer states. If not set, will use the value + from the `Accelerator` directly. + gradient_clipping (`float`, defaults to `None`): + Enable gradient clipping with value. + zero_stage (`int`, defaults to `None`): + Possible options are 0, 1, 2, 3. Default will be taken from environment variable. + is_train_batch_min (`bool`, defaults to `True`): + If both train & eval dataloaders are specified, this will decide the `train_batch_size`. + offload_optimizer_device (`str`, defaults to `None`): + Possible options are none|cpu|nvme. Only applicable with ZeRO Stages 2 and 3. + offload_param_device (`str`, defaults to `None`): + Possible options are none|cpu|nvme. Only applicable with ZeRO Stage 3. + offload_optimizer_nvme_path (`str`, defaults to `None`): + Possible options are /nvme|/local_nvme. Only applicable with ZeRO Stage 3. + offload_param_nvme_path (`str`, defaults to `None`): + Possible options are /nvme|/local_nvme. Only applicable with ZeRO Stage 3. + zero3_init_flag (`bool`, defaults to `None`): + Flag to indicate whether to save 16-bit model. Only applicable with ZeRO Stage-3. + zero3_save_16bit_model (`bool`, defaults to `None`): + Flag to indicate whether to save 16-bit model. Only applicable with ZeRO Stage-3. + transformer_moe_cls_names (`str`, defaults to `None`): + Comma-separated list of Transformers MoE layer class names (case-sensitive). For example, + `MixtralSparseMoeBlock`, `Qwen2MoeSparseMoeBlock`, `JetMoEAttention`, `JetMoEBlock`, etc. + enable_msamp (`bool`, defaults to `None`): + Flag to indicate whether to enable MS-AMP backend for FP8 training. + msasmp_opt_level (`Optional[Literal["O1", "O2"]]`, defaults to `None`): + Optimization level for MS-AMP (defaults to 'O1'). Only applicable if `enable_msamp` is True. Should be one + of ['O1' or 'O2']. + """ + + hf_ds_config: Any = field( + default=None, + metadata={ + "help": "path to DeepSpeed config file or dict or an object of class `accelerate.utils.deepspeed.HfDeepSpeedConfig`." + }, + ) + gradient_accumulation_steps: int = field( + default=None, + metadata={ + "help": "Number of steps to accumulate gradients before updating optimizer states. If not set, will use the value from the `Accelerator` directly." + }, + ) + gradient_clipping: float = field(default=None, metadata={"help": "Enable gradient clipping with value"}) + zero_stage: int = field( + default=None, + metadata={"help": "Possible options are 0,1,2,3; Default will be taken from environment variable"}, + ) + is_train_batch_min: bool = field( + default=True, + metadata={"help": "If both train & eval dataloaders are specified, this will decide the train_batch_size"}, + ) + offload_optimizer_device: str = field( + default=None, + metadata={"help": "Possible options are none|cpu|nvme. Only applicable with ZeRO Stages 2 and 3."}, + ) + offload_param_device: str = field( + default=None, + metadata={"help": "Possible options are none|cpu|nvme. Only applicable with ZeRO Stage 3."}, + ) + offload_optimizer_nvme_path: str = field( + default=None, + metadata={"help": "Possible options are /nvme|/local_nvme. Only applicable with ZeRO Stage 3."}, + ) + offload_param_nvme_path: str = field( + default=None, + metadata={"help": "Possible options are /nvme|/local_nvme. Only applicable with ZeRO Stage 3."}, + ) + zero3_init_flag: bool = field( + default=None, + metadata={ + "help": "Flag to indicate whether to enable `deepspeed.zero.Init` for constructing massive models." + "Only applicable with ZeRO Stage-3." + }, + ) + zero3_save_16bit_model: bool = field( + default=None, + metadata={"help": "Flag to indicate whether to save 16-bit model. Only applicable with ZeRO Stage-3."}, + ) + transformer_moe_cls_names: str = field( + default=None, + metadata={ + "help": "comma-separated list of transformers MoE layer class names (case-sensitive), e.g : " + " `MixtralSparseMoeBlock`, `Qwen2MoeSparseMoeBlock`, `JetMoEAttention,JetMoEBlock` ..." + }, + ) + enable_msamp: bool = field( + default=None, + metadata={"help": "Flag to indicate whether to enable MS-AMP backend for FP8 training."}, + ) + msamp_opt_level: Optional[Literal["O1", "O2"]] = field( + default=None, + metadata={ + "help": "Optimization level for MS-AMP (defaults to 'O1'). Only applicable if `enable_msamp` is True. Should be one of ['O1' or 'O2']." + }, + ) + + def __post_init__(self): + from .deepspeed import HfDeepSpeedConfig + + if self.gradient_accumulation_steps is None: + gas = os.environ.get("ACCELERATE_GRADIENT_ACCUMULATION_STEPS", "auto") + self.gradient_accumulation_steps = int(gas) if gas.isdigit() else gas + + if self.gradient_clipping is None: + gradient_clipping = os.environ.get("ACCELERATE_GRADIENT_CLIPPING", "auto") + self.gradient_clipping = gradient_clipping if gradient_clipping == "auto" else float(gradient_clipping) + + if self.zero_stage is None: + self.zero_stage = int(os.environ.get("ACCELERATE_DEEPSPEED_ZERO_STAGE", 2)) + + if self.offload_optimizer_device is None: + self.offload_optimizer_device = os.environ.get("ACCELERATE_DEEPSPEED_OFFLOAD_OPTIMIZER_DEVICE", "none") + + if self.offload_param_device is None: + self.offload_param_device = os.environ.get("ACCELERATE_DEEPSPEED_OFFLOAD_PARAM_DEVICE", "none") + + if self.offload_optimizer_nvme_path is None: + self.offload_optimizer_nvme_path = os.environ.get( + "ACCELERATE_DEEPSPEED_OFFLOAD_OPTIMIZER_NVME_PATH", "none" + ) + + if self.offload_param_nvme_path is None: + self.offload_param_nvme_path = os.environ.get("ACCELERATE_DEEPSPEED_OFFLOAD_PARAM_NVME_PATH", "none") + + if self.zero3_save_16bit_model is None: + self.zero3_save_16bit_model = ( + os.environ.get("ACCELERATE_DEEPSPEED_ZERO3_SAVE_16BIT_MODEL", "false").lower() == "true" + ) + if self.enable_msamp is None: + self.enable_msamp = os.environ.get("ACCELERATE_FP8_BACKEND", None) == "MSAMP" + + if self.msamp_opt_level is None: + self.msamp_opt_level = os.environ.get("ACCELERATE_FP8_OPT_LEVEL", "O1") + + if self.hf_ds_config is None: + self.hf_ds_config = os.environ.get("ACCELERATE_DEEPSPEED_CONFIG_FILE", "none") + + if ( + isinstance(self.hf_ds_config, dict) + or (isinstance(self.hf_ds_config, str) and self.hf_ds_config != "none") + or isinstance(self.hf_ds_config, HfDeepSpeedConfig) + ): + if not isinstance(self.hf_ds_config, HfDeepSpeedConfig): + self.hf_ds_config = HfDeepSpeedConfig(self.hf_ds_config) + if "gradient_accumulation_steps" not in self.hf_ds_config.config: + self.hf_ds_config.config["gradient_accumulation_steps"] = 1 + if "zero_optimization" not in self.hf_ds_config.config: + raise ValueError("Please specify the ZeRO optimization config in the DeepSpeed config.") + + self._deepspeed_config_checks() + plugin_to_config_mapping = { + "gradient_accumulation_steps": "gradient_accumulation_steps", + "gradient_clipping": "gradient_clipping", + "zero_stage": "zero_optimization.stage", + "offload_optimizer_device": "zero_optimization.offload_optimizer.device", + "offload_param_device": "zero_optimization.offload_param.device", + "offload_param_nvme_path": "zero_optimization.offload_param.nvme_path", + "offload_optimizer_nvme_path": "zero_optimization.offload_optimizer.nvme_path", + "zero3_save_16bit_model": "zero_optimization.stage3_gather_16bit_weights_on_model_save", + } + kwargs = {v: getattr(self, k) for k, v in plugin_to_config_mapping.items() if getattr(self, k) is not None} + for key in kwargs.keys(): + self.fill_match(key, **kwargs, must_match=False) + self.hf_ds_config.set_stage_and_offload() + + # filling the missing values in the class attributes from the DeepSpeed config + # when using the DeepSpeed config file. + for key, value in plugin_to_config_mapping.items(): + config_value = self.hf_ds_config.get_value(value) + if config_value is not None and config_value != "auto": + setattr(self, key, config_value) + else: + config = { + "train_batch_size": "auto", + "train_micro_batch_size_per_gpu": "auto", + "gradient_accumulation_steps": self.gradient_accumulation_steps, + "zero_optimization": { + "stage": self.zero_stage, + "offload_optimizer": { + "device": self.offload_optimizer_device, + "nvme_path": ( + self.offload_optimizer_nvme_path if self.offload_optimizer_device == "nvme" else None + ), + }, + "offload_param": { + "device": self.offload_param_device, + "nvme_path": (self.offload_param_nvme_path if self.offload_param_device == "nvme" else None), + }, + "stage3_gather_16bit_weights_on_model_save": self.zero3_save_16bit_model, + }, + } + if self.gradient_clipping: + config["gradient_clipping"] = self.gradient_clipping + self.hf_ds_config = HfDeepSpeedConfig(config) + + self.deepspeed_config = self.hf_ds_config.config + self.deepspeed_config["steps_per_print"] = float("inf") # this will stop deepspeed from logging @ stdout + if self.zero3_init_flag is None: + self.zero3_init_flag = ( + str_to_bool( + os.environ.get( + "ACCELERATE_DEEPSPEED_ZERO3_INIT", + str(self.hf_ds_config.is_zero3()), + ) + ) + == 1 + ) + if self.zero3_init_flag and not self.hf_ds_config.is_zero3(): + warnings.warn("DeepSpeed Zero3 Init flag is only applicable for ZeRO Stage 3. Setting it to False.") + self.zero3_init_flag = False + # NOTE: Set to False by default, will be set to `True` automatically if it's the first plugin passed + # to the `Accelerator`'s `deepspeed_plugin` param, *or* `AcceleratorState().enable_deepspeed_plugin(plugin_key)` is manually called + self._set_selected(False) + + # Ignore if it's already set + if self.enable_msamp and "msamp" not in self.deepspeed_config: + if self.zero_stage == 3: + raise NotImplementedError( + "MS-AMP is not supported for ZeRO Stage 3. Please use ZeRO Stage 0, 1, or 2 instead." + ) + if self.msamp_opt_level not in ["O1", "O2"]: + raise ValueError("Invalid optimization level for MS-AMP. Please use one of ['O1' or'O2'].") + self.deepspeed_config["msamp"] = { + "enabled": True, + "opt_level": self.msamp_opt_level, + } + + def fill_match(self, ds_key_long, mismatches=None, must_match=True, **kwargs): + mismatches = [] if mismatches is None else mismatches + config, ds_key = self.hf_ds_config.find_config_node(ds_key_long) + if config is None: + return + + if config.get(ds_key) == "auto": + if ds_key_long in kwargs: + config[ds_key] = kwargs[ds_key_long] + return + else: + raise ValueError( + f"`{ds_key_long}` not found in kwargs. " + f"Please specify `{ds_key_long}` without `auto` (set to correct value) in the DeepSpeed config file or " + "pass it in kwargs." + ) + + if not must_match: + return + + ds_val = config.get(ds_key) + if ds_val is not None and ds_key_long in kwargs: + if ds_val != kwargs[ds_key_long]: + mismatches.append(f"- ds {ds_key_long}={ds_val} vs arg {ds_key_long}={kwargs[ds_key_long]}") + + def is_auto(self, ds_key_long): + val = self.hf_ds_config.get_value(ds_key_long) + if val is None: + return False + else: + return val == "auto" + + def get_value(self, ds_key_long, default=None): + return self.hf_ds_config.get_value(ds_key_long, default) + + def deepspeed_config_process(self, prefix="", mismatches=None, config=None, must_match=True, **kwargs): + """Process the DeepSpeed config with the values from the kwargs.""" + mismatches = [] if mismatches is None else mismatches + if config is None: + config = self.deepspeed_config + for key, value in config.items(): + if isinstance(value, dict): + self.deepspeed_config_process( + prefix=prefix + key + ".", + mismatches=mismatches, + config=value, + must_match=must_match, + **kwargs, + ) + else: + self.fill_match(prefix + key, mismatches, must_match=must_match, **kwargs) + if len(mismatches) > 0 and prefix == "": + mismatches_msg = "\n".join(mismatches) + raise ValueError( + "Please correct the following DeepSpeed config values that mismatch kwargs " + f" values:\n{mismatches_msg}\nThe easiest method is to set these DeepSpeed config values to 'auto'." + ) + + def set_mixed_precision(self, mixed_precision): + ds_config = self.deepspeed_config + kwargs = { + "fp16.enabled": mixed_precision == "fp16", + # When training in fp8, we still rely on bf16 autocast for the core mixed precision + "bf16.enabled": mixed_precision in ("bf16", "fp8"), + } + if mixed_precision == "fp16": + if "fp16" not in ds_config: + ds_config["fp16"] = {"enabled": True, "auto_cast": True} + elif mixed_precision in ("bf16", "fp8"): + if "bf16" not in ds_config: + ds_config["bf16"] = {"enabled": True} + + if mixed_precision == "fp8" and self.enable_msamp: + if "msamp" not in ds_config: + ds_config["msamp"] = { + "enabled": True, + "opt_level": self.msamp_opt_level, + } + + if mixed_precision != "no": + diff_dtype = "bf16" if mixed_precision == "fp16" else "fp16" + if str(ds_config.get(diff_dtype, {}).get("enabled", "False")).lower() == "true": + raise ValueError( + f"`--mixed_precision` arg cannot be set to `{mixed_precision}` when `{diff_dtype}` is set in the DeepSpeed config file." + ) + for dtype in ["fp16", "bf16"]: + if dtype not in ds_config: + ds_config[dtype] = {"enabled": False} + self.fill_match("fp16.enabled", must_match=False, **kwargs) + self.fill_match("bf16.enabled", must_match=False, **kwargs) + + def set_deepspeed_weakref(self): + from .imports import is_transformers_available + + ds_config = copy.deepcopy(self.deepspeed_config) + if self.zero3_init_flag: + if not is_transformers_available(): + raise Exception( + "When `zero3_init_flag` is set, it requires Transformers to be installed. " + "Please run `pip install transformers`." + ) + if "gradient_accumulation_steps" not in ds_config or ds_config["gradient_accumulation_steps"] == "auto": + ds_config["gradient_accumulation_steps"] = 1 + if "train_micro_batch_size_per_gpu" not in ds_config or ds_config["train_micro_batch_size_per_gpu"] == "auto": + ds_config["train_micro_batch_size_per_gpu"] = 1 + if ds_config.get("train_batch_size", None) == "auto": + del ds_config["train_batch_size"] + + if compare_versions("transformers", "<", "4.46"): + from transformers.deepspeed import ( + HfDeepSpeedConfig, + unset_hf_deepspeed_config, + ) + else: + from transformers.integrations import ( + HfDeepSpeedConfig, + unset_hf_deepspeed_config, + ) + + unset_hf_deepspeed_config() + self.dschf = HfDeepSpeedConfig(ds_config) # keep this object alive # noqa + + def is_zero3_init_enabled(self): + return self.zero3_init_flag + + @contextmanager + def zero3_init_context_manager(self, enable=False): + old = self.zero3_init_flag + if old == enable: + yield + else: + self.zero3_init_flag = enable + self.dschf = None + self.set_deepspeed_weakref() + yield + self.zero3_init_flag = old + self.dschf = None + self.set_deepspeed_weakref() + + def _deepspeed_config_checks(self): + env_variable_names_to_ignore = [ + "ACCELERATE_GRADIENT_ACCUMULATION_STEPS", + "ACCELERATE_GRADIENT_CLIPPING", + "ACCELERATE_DEEPSPEED_ZERO_STAGE", + "ACCELERATE_DEEPSPEED_OFFLOAD_OPTIMIZER_DEVICE", + "ACCELERATE_DEEPSPEED_OFFLOAD_PARAM_DEVICE", + "ACCELERATE_DEEPSPEED_OFFLOAD_PARAM_NVME_PATH", + "ACCELERATE_DEEPSPEED_OFFLOAD_OPTIMIZER_NVME_PATH", + "ACCELERATE_DEEPSPEED_ZERO3_SAVE_16BIT_MODEL", + "ACCELERATE_MIXED_PRECISION", + ] + env_variable_names_to_ignore = [ + name.replace("ACCELERATE_", "").replace("DEEPSPEED_", "").lower() for name in env_variable_names_to_ignore + ] + + deepspeed_fields_from_accelerate_config = os.environ.get("ACCELERATE_CONFIG_DS_FIELDS", "").split(",") + + if any(name in env_variable_names_to_ignore for name in deepspeed_fields_from_accelerate_config): + raise ValueError( + f"When using `deepspeed_config_file`, the following accelerate config variables will be ignored: {env_variable_names_to_ignore}.\n" + "Please specify them appropriately in the DeepSpeed config file.\n" + "If you are using an accelerate config file, remove others config variables mentioned in the above specified list.\n" + "The easiest method is to create a new config following the questionnaire via `accelerate config`.\n" + "It will only ask for the necessary config variables when using `deepspeed_config_file`." + ) + + def set_moe_leaf_modules(self, model): + if self.transformer_moe_cls_names is None: + self.transformer_moe_cls_names = os.environ.get("ACCELERATE_DEEPSPEED_MOE_LAYER_CLS_NAMES", None) + if self.transformer_moe_cls_names is not None: + if compare_versions("deepspeed", "<", "0.14.0"): + raise ImportError("DeepSpeed version must be >= 0.14.0 to use MOE support. Please update DeepSpeed.") + from deepspeed.utils import set_z3_leaf_modules + + class_names = self.transformer_moe_cls_names.split(",") + transformer_moe_cls = [] + for layer_class in class_names: + transformer_cls = get_module_class_from_name(model, layer_class) + if transformer_cls is None: + raise Exception( + f"Could not find a transformer layer class called '{layer_class}' to wrap in the model." + ) + else: + transformer_moe_cls.append(transformer_cls) + set_z3_leaf_modules(model, transformer_moe_cls) # z3_leaf + + def select(self, _from_accelerator_state: bool = False): + """ + Sets the HfDeepSpeedWeakref to use the current deepspeed plugin configuration + """ + if not _from_accelerator_state: + raise ValueError( + "A `DeepSpeedPlugin` object must be enabled manually by calling `AcceleratorState().enable_deepspeed_plugin(plugin_key)`." + ) + self.set_deepspeed_weakref() + self._set_selected(True) + + def _unselect(self): + self._set_selected(False) + + def _set_selected(self, value: bool): + """ + Private setter for the 'enabled' attribute. + """ + self._selected = value + + @property + def selected(self): + return self._selected + + @selected.setter + def selected(self, value): + raise NotImplementedError( + "'enabled' can only be set through calling 'AcceleratorState().enable_deepspeed_plugin(key)'." + ) + + +@dataclass +class FullyShardedDataParallelPlugin: + """ + This plugin is used to enable fully sharded data parallelism. + + Args: + fsdp_version (`int`, defaults to `1`): + The version of FSDP to use. Defaults to 1. If set to 2, launcher expects the config to be converted to + FSDP2 format. + sharding_strategy (`Union[str, torch.distributed.fsdp.ShardingStrategy]`, defaults to `'FULL_SHARD'`): + Sharding strategy to use. Should be either a `str` or an instance of + `torch.distributed.fsdp.fully_sharded_data_parallel.ShardingStrategy`. Is deprecated in favor of + `reshard_after_forward`. + reshard_after_forward (`Union[str, torch.distributed.fsdp.ShardingStrategy, bool]`, defaults to `'FULL_SHARD'` for `fsdp_version=1` and `True` for `fsdp_version=2`): + Sharding strategy to use. Should be a bool if `fsdp_version` is set to 2 else a `str` or an instance of + `torch.distributed.fsdp.fully_sharded_data_parallel.ShardingStrategy`. + backward_prefetch (`Union[str, torch.distributed.fsdp.BackwardPrefetch]`, defaults to `'NO_PREFETCH'`): + Backward prefetch strategy to use. Should be either a `str` or an instance of + `torch.distributed.fsdp.fully_sharded_data_parallel.BackwardPrefetch`. + mixed_precision_policy (`Optional[Union[dict, str, torch.distributed.fsdp.MixedPrecision, torch.distributed.fsdp.MixedPrecisionPolicy]]`, defaults to `None`): + A config to enable mixed precision training with FullyShardedDataParallel. If passing in a `dict`, it + should have the following keys: `param_dtype`, `reduce_dtype`, and `buffer_dtype`, can be an instance of + `torch.distributed.fsdp.MixedPrecisionPolicy` if `fsdp_version` is set to 2. If passing in a `str`, it + should be one of the following values: fp8, fp16, bf16, fp32, and used to set `param_dtype`, + `reduce_dtype`, and `buffer_dtype`. + auto_wrap_policy (`Optional(Union[Callable, Literal["transformer_based_wrap", "size_based_wrap", "no_wrap"]]), defaults to `NO_WRAP`): + A callable or string specifying a policy to recursively wrap layers with FSDP. If a string, it must be one + of `transformer_based_wrap`, `size_based_wrap`, or `no_wrap`. See + `torch.distributed.fsdp.wrap.size_based_wrap_policy` for a direction on what it should look like. + cpu_offload (`Union[bool, torch.distributed.fsdp.CPUOffload, torch.distributed.fsdp.CPUOffloadPolicy]`, defaults to `False`): + Whether to offload parameters to CPU. Should be either a `bool` or an instance of + `torch.distributed.fsdp.fully_sharded_data_parallel.CPUOffload` or + `torch.distributed.fsdp.fully_sharded_data_parallel.CPUOffloadPolicy` if `fsdp_version` is set to 2. + ignored_modules (`Optional[Union[Iterable[torch.nn.Module], str]]`, defaults to `None`): + A list of modules to ignore when wrapping with FSDP. When passing a string, will match the modules by name + using regex fullmatch. If `fsdp_version` is set to 2, the modules are converted to parameters and used. + state_dict_type (`Union[str, torch.distributed.fsdp.StateDictType]`, defaults to `'FULL_STATE_DICT'`): + State dict type to use. If a string, it must be one of `full_state_dict`, `local_state_dict`, or + `sharded_state_dict`. + state_dict_config (`Optional[Union[torch.distributed.fsdp.FullStateDictConfig, torch.distributed.fsdp.ShardedStateDictConfig]`, defaults to `None`): + State dict config to use. Is determined based on the `state_dict_type` if not passed in. + optim_state_dict_config (`Optional[Union[torch.distributed.fsdp.FullOptimStateDictConfig, torch.distributed.fsdp.ShardedOptimStateDictConfig]`, defaults to `None`): + Optim state dict config to use. Is determined based on the `state_dict_type` if not passed in. + limit_all_gathers (`bool`, defaults to `True`): + Whether to have FSDP explicitly synchronizes the CPU thread to prevent too many in-flight all-gathers. This + bool only affects the sharded strategies that schedule all-gathers. Enabling this can help lower the number + of CUDA malloc retries. + use_orig_params (`bool`, defaults to `False`): + Whether to use the original parameters for the optimizer. + param_init_fn (`Optional[Callable[[torch.nn.Module], None]`, defaults to `None`): + A `Callable[torch.nn.Module] -> None` that specifies how modules that are currently on the meta device + should be initialized onto an actual device. Only applicable when `sync_module_states` is `True`. By + default is a `lambda` which calls `to_empty` on the module. + sync_module_states (`bool`, defaults to `False`): + Whether each individually wrapped FSDP unit should broadcast module parameters from rank 0 to ensure they + are the same across all ranks after initialization. Defaults to `False` unless `cpu_ram_efficient_loading` + is `True`, then will be forcibly enabled. + forward_prefetch (`bool`, defaults to `False`): + Whether to have FSDP explicitly prefetches the next upcoming all-gather while executing in the forward + pass. only use with Static graphs. + activation_checkpointing (`bool`, defaults to `False`): + A technique to reduce memory usage by clearing activations of certain layers and recomputing them during a + backward pass. Effectively, this trades extra computation time for reduced memory usage. + cpu_ram_efficient_loading (`bool`, defaults to `None`): + If True, only the first process loads the pretrained model checkoint while all other processes have empty + weights. Only applicable for Transformers. When using this, `sync_module_states` needs to be `True`. + transformer_cls_names_to_wrap (`Optional[List[str]]`, defaults to `None`): + A list of transformer layer class names to wrap. Only applicable when `auto_wrap_policy` is + `transformer_based_wrap`. + min_num_params (`Optional[int]`, defaults to `None`): + The minimum number of parameters a module must have to be wrapped. Only applicable when `auto_wrap_policy` + is `size_based_wrap`. + """ + + fsdp_version: int = field( + default=None, + metadata={ + "help": "The version of FSDP to use. Defaults to 1. If set to 2, launcher expects the config to be converted to FSDP2 format." + }, + ) + + sharding_strategy: Union[str, "torch.distributed.fsdp.ShardingStrategy"] = field( + default=None, + metadata={ + "help": "Sharding strategy to use. Should be either a `str` or an instance of `torch.distributed.fsdp.fully_sharded_data_parallel.ShardingStrategy`. Defaults to 'FULL_SHARD'. Is deprecated in favor of `reshard_after_forward` " + }, + ) + + reshard_after_forward: Union[str, "torch.distributed.fsdp.ShardingStrategy", bool] = field( + default=None, + metadata={ + "help": "Sharding strategy to use. Should be a bool if `fsdp_version` is set to 2 else a `str` or an instance of `torch.distributed.fsdp.fully_sharded_data_parallel.ShardingStrategy`. Defaults to 'FULL_SHARD'" + }, + ) + backward_prefetch: Optional[Union[str, "torch.distributed.fsdp.BackwardPrefetch"]] = field( + default=None, + metadata={ + "help": "Backward prefetch strategy to use. Should be either a `str` or an instance of `torch.distributed.fsdp.fully_sharded_data_parallel.BackwardPrefetch`. Defaults to 'NO_PREFETCH'. This becomes obsolete in FSDP2." + }, + ) + mixed_precision_policy: Optional[ + Union[ + dict, + str, + "torch.distributed.fsdp.MixedPrecision", + "torch.distributed.fsdp.MixedPrecisionPolicy", + ] + ] = field( + default=None, + metadata={ + "help": "A config to enable mixed precision training with FullyShardedDataParallel. " + "If passing in a `dict`, it should have the following keys: `param_dtype`, `reduce_dtype`, and `buffer_dtype`." + "Can also be an instance of `torch.distributed.fsdp.MixedPrecisionPolicy` if `fsdp_version` is set to 2." + }, + ) + auto_wrap_policy: Optional[Union[Callable, Literal["transformer_based_wrap", "size_based_wrap", "no_wrap"]]] = ( + field( + default=None, + metadata={ + "help": "A callable or string specifying a policy to recursively wrap layers with FSDP. If a string, it must be one of `transformer_based_wrap`, `size_based_wrap`, or `no_wrap`. " + "Defaults to `NO_WRAP`. See `torch.distributed.fsdp.wrap.size_based_wrap_policy` for a direction on what it should look like" + }, + ) + ) + cpu_offload: Union[ + bool, + "torch.distributed.fsdp.CPUOffload", + "torch.distributed.fsdp.CPUOffloadPolicy", + ] = field( + default=None, + metadata={ + "help": "Whether to offload parameters to CPU. Should be either a `bool` or an instance of `torch.distributed.fsdp.fully_sharded_data_parallel.CPUOffload` or `torch.distributed.fsdp.fully_sharded_data_parallel.CPUOffloadPolicy` if `fsdp_version` is set to 2. Defaults to `False`" + }, + ) + ignored_modules: Optional[Union[Iterable[torch.nn.Module], str]] = field( + default=None, + metadata={"help": "A list of modules to ignore when wrapping with FSDP."}, + ) + + state_dict_type: Union[str, "torch.distributed.fsdp.StateDictType"] = field( + default=None, + metadata={ + "help": "State dict type to use. If a string, it must be one of `full_state_dict`, `local_state_dict`, or `sharded_state_dict`. Defaults to `FULL_STATE_DICT`" + }, + ) + state_dict_config: Optional[ + Union[ + "torch.distributed.fsdp.FullStateDictConfig", + "torch.distributed.fsdp.ShardedStateDictConfig", + ] + ] = field( + default=None, + metadata={"help": "State dict config to use. Is determined based on the `state_dict_type` if not passed in."}, + ) + optim_state_dict_config: Optional[ + Union[ + "torch.distributed.fsdp.FullOptimStateDictConfig", + "torch.distributed.fsdp.ShardedOptimStateDictConfig", + ] + ] = field( + default=None, + metadata={ + "help": "Optim state dict config to use. Is determined based on the `state_dict_type` if not passed in." + }, + ) + limit_all_gathers: bool = field( + default=True, + metadata={ + "help": "Whether to have FSDP explicitly synchronizes the CPU thread to prevent " + "too many in-flight all-gathers. This bool only affects the sharded strategies that schedule all-gathers. " + "Enabling this can help lower the number of CUDA malloc retries." + }, + ) + use_orig_params: Optional[bool] = field( + default=None, + metadata={ + "help": "Whether to use the original parameters for the optimizer. Defaults to `False`. This becomes obsolete in FSDP2." + }, + ) + param_init_fn: Optional[Callable[[torch.nn.Module], None]] = field( + default=None, + metadata={ + "help": "A Callable[torch.nn.Module] -> None that specifies how modules " + "that are currently on the meta device should be initialized onto an actual device. " + "Only applicable when `sync_module_states` is `True`. By default is a `lambda` which calls `to_empty` on the module." + }, + ) + sync_module_states: Optional[bool] = field( + default=None, + metadata={ + "help": "Whether each individually wrapped FSDP unit should broadcast module parameters from rank 0 " + "to ensure they are the same across all ranks after initialization. Defaults to `False` unless " + "`cpu_ram_efficient_loading` is `True`, then will be forcibly enabled. This becomes obsolete in FSDP2." + }, + ) + forward_prefetch: bool = field( + default=None, + metadata={ + "help": "Whether to have FSDP explicitly prefetches the next upcoming " + "all-gather while executing in the forward pass. only use with Static graphs. Defaults to `False`" + }, + ) + activation_checkpointing: bool = field( + default=None, + metadata={ + "help": "A technique to reduce memory usage by clearing activations of " + "certain layers and recomputing them during a backward pass. Effectively, this trades extra computation time " + "for reduced memory usage. Defaults to `False`" + }, + ) + cpu_ram_efficient_loading: bool = field( + default=None, + metadata={ + "help": "If True, only the first process loads the pretrained model checkoint while all other processes have empty weights. " + "Only applicable for 🤗 Transformers. When using this, `sync_module_states` needs to be `True`. Defaults to `False`." + }, + ) + transformer_cls_names_to_wrap: Optional[list[str]] = field( + default=None, + metadata={ + "help": "A list of transformer layer class names to wrap. Only applicable when `auto_wrap_policy` is `transformer_based_wrap`." + }, + ) + min_num_params: Optional[int] = field( + default=None, + metadata={ + "help": "The minimum number of parameters a module must have to be wrapped. Only applicable when `auto_wrap_policy` is `size_based_wrap`." + }, + ) + + def __post_init__(self): + from torch.distributed.fsdp import BackwardPrefetch, ShardingStrategy + + _fsdp2_warnings = set() + + env_prefix = "FSDP_" + # Strategy: By default we should always assume that values are passed in, else we check the environment variables + if self.fsdp_version is None: + self.fsdp_version = int(os.environ.get(env_prefix + "VERSION", "1")) + + if self.fsdp_version == 2: + if not is_torch_version(">=", FSDP2_PYTORCH_VERSION): + raise ImportError(f"FSDP2 requires PyTorch >= {FSDP2_PYTORCH_VERSION}") + + if self.sharding_strategy is not None: + # We cannot properly detect all of the cases, as by default `args.fsdp_sharding_strategy` is set to `fully_shard` + # Therefore we issue a warning only if the user has explicitly set it inside their plugin + _fsdp2_warnings.add( + "sharding_strategy is deprecated in favor of reshard_after_forward. " + "This will be removed in a future version of Accelerate." + ) + if self.fsdp_version == 1: + if self.sharding_strategy is None: + self.sharding_strategy = os.environ.get(env_prefix + "SHARDING_STRATEGY", "FULL_SHARD") + if isinstance(self.sharding_strategy, str): + if self.sharding_strategy.upper() in FSDP_SHARDING_STRATEGY: + self.sharding_strategy = FSDP_SHARDING_STRATEGY.index(self.sharding_strategy.upper()) + 1 + if isinstance(self.sharding_strategy, int) or self.sharding_strategy.isdigit(): + self.sharding_strategy = ShardingStrategy(int(self.sharding_strategy)) + else: + self.sharding_strategy = ShardingStrategy[self.sharding_strategy.upper()] + + # Fallback to `reshard_after_forward` in FSDP1 if `sharding_strategy` is not set + if self.reshard_after_forward is None and self.sharding_strategy is None: + reshard_after_forward = os.environ.get( + env_prefix + "RESHARD_AFTER_FORWARD", + "true" if self.fsdp_version == 2 else "FULL_SHARD", + ) + if self.fsdp_version == 2: + self.reshard_after_forward = str_to_bool(reshard_after_forward.lower(), to_bool=True) + else: + self.reshard_after_forward = reshard_after_forward + if isinstance(self.reshard_after_forward, str): + if self.fsdp_version == 2: + self.reshard_after_forward = str_to_bool(self.reshard_after_forward.lower(), to_bool=True) + else: + # We need to remap based on custom enum values for user readability + if self.reshard_after_forward.upper() in FSDP_SHARDING_STRATEGY: + self.reshard_after_forward = FSDP_SHARDING_STRATEGY.index(self.reshard_after_forward.upper()) + 1 + if isinstance(self.reshard_after_forward, int) or self.reshard_after_forward.isdigit(): + self.reshard_after_forward = ShardingStrategy(int(self.reshard_after_forward)) + else: + self.reshard_after_forward = ShardingStrategy[self.reshard_after_forward.upper()] + + if self.fsdp_version == 2 and not isinstance(self.reshard_after_forward, bool): + raise ValueError( + f"reshard_after_forward set to {self.reshard_after_forward}. This is not supported with FSDP2, please set to a `bool`" + ) + if self.fsdp_version == 1 and isinstance(self.reshard_after_forward, bool): + raise ValueError( + f"reshard_after_forward set to {self.reshard_after_forward}. This is not supported with FSDP1, please set to a `str` or an instance of `torch.distributed.fsdp.fully_sharded_data_parallel.ShardingStrategy`" + ) + + if self.cpu_offload is None: + self.cpu_offload = str_to_bool(os.environ.get(env_prefix + "OFFLOAD_PARAMS", "False")) == 1 + + self.set_cpu_offload() # abstracted away to hide imports due to version checks + self.validate_cpu_offload() + + if self.backward_prefetch is None: + self.backward_prefetch = os.environ.get(env_prefix + "BACKWARD_PREFETCH", None) + if isinstance(self.backward_prefetch, str) and self.backward_prefetch.upper() == "NO_PREFETCH": + self.backward_prefetch = None + if self.backward_prefetch is not None and not isinstance(self.backward_prefetch, BackwardPrefetch): + if isinstance(self.backward_prefetch, str) and self.backward_prefetch.upper() in FSDP_BACKWARD_PREFETCH: + self.backward_prefetch = FSDP_BACKWARD_PREFETCH.index(self.backward_prefetch.upper()) + 1 + if isinstance(self.backward_prefetch, int) or self.backward_prefetch.isdigit(): + self.backward_prefetch = BackwardPrefetch(int(self.backward_prefetch)) + else: + self.backward_prefetch = BackwardPrefetch[self.backward_prefetch.upper()] + if self.fsdp_version == 2 and self.backward_prefetch is not None: + _fsdp2_warnings.add("backward_prefetch is not supported in FSDP2. Setting backward prefetch to None.") + self.backward_prefetch = None + + self.set_state_dict_type() + + if self.auto_wrap_policy is None: + self.auto_wrap_policy = os.environ.get(env_prefix + "AUTO_WRAP_POLICY", "NO_WRAP") + if isinstance(self.auto_wrap_policy, str): + if self.auto_wrap_policy.upper() not in FSDP_AUTO_WRAP_POLICY: + raise ValueError( + f"Invalid auto wrap policy: {self.auto_wrap_policy}. Must be one of {FSDP_AUTO_WRAP_POLICY}" + ) + from torch.distributed.fsdp.wrap import ( + size_based_auto_wrap_policy, + transformer_auto_wrap_policy, + ) + + if self.auto_wrap_policy.upper() == "TRANSFORMER_BASED_WRAP": + self.auto_wrap_policy = transformer_auto_wrap_policy + if self.transformer_cls_names_to_wrap is None: + self.transformer_cls_names_to_wrap = os.environ.get(env_prefix + "TRANSFORMER_CLS_TO_WRAP", None) + if isinstance(self.transformer_cls_names_to_wrap, str): + self.transformer_cls_names_to_wrap = self.transformer_cls_names_to_wrap.split(",") + elif self.auto_wrap_policy.upper() == "SIZE_BASED_WRAP": + self.auto_wrap_policy = size_based_auto_wrap_policy + if self.min_num_params is None: + self.min_num_params = int(os.environ.get(env_prefix + "MIN_NUM_PARAMS", 0)) + elif not isinstance(self.min_num_params, int): + raise ValueError( + f"`min_num_params` must be an integer. Got {self.min_num_params} of type {type(self.min_num_params)}" + ) + elif self.auto_wrap_policy.upper() == "NO_WRAP": + self.auto_wrap_policy = None + + if self.use_orig_params is None and self.fsdp_version == 1: + self.use_orig_params = str_to_bool(os.environ.get(env_prefix + "USE_ORIG_PARAMS", "False")) == 1 + if self.fsdp_version == 2 and self.use_orig_params is not None: + _fsdp2_warnings.add("use_orig_params is obsolete in FSDP2, as FSDP2 always uses the original parameters.") + self.use_orig_params = None + + if self.sync_module_states is None and self.fsdp_version == 1: + self.sync_module_states = str_to_bool(os.environ.get(env_prefix + "SYNC_MODULE_STATES", "False")) == 1 + if self.fsdp_version == 2 and self.sync_module_states is not None: + _fsdp2_warnings.add( + "sync_module_states is obsolete in FSDP2, as it is not needed anymore." + "Setting sync_module_states to None." + ) + self.sync_module_states = None + + if self.forward_prefetch is None and self.fsdp_version == 1: + self.forward_prefetch = str_to_bool(os.environ.get(env_prefix + "FORWARD_PREFETCH", "False")) == 1 + if self.fsdp_version == 2 and self.forward_prefetch is not None: + raise ValueError("forward_prefetch is not yet implemented in FSDP2, set to None or use `fsdp_version=1`") + + if self.activation_checkpointing is None: + self.activation_checkpointing = ( + str_to_bool(os.environ.get(env_prefix + "ACTIVATION_CHECKPOINTING", "False")) == 1 + ) + + if self.ignored_modules is None: + self.ignored_modules = os.environ.get(env_prefix + "IGNORED_MODULES", None) + + if self.cpu_ram_efficient_loading is None: + self.cpu_ram_efficient_loading = ( + str_to_bool(os.environ.get(env_prefix + "CPU_RAM_EFFICIENT_LOADING", "False")) == 1 + ) + else: + # We still need to set it for transformers + os.environ[env_prefix + "CPU_RAM_EFFICIENT_LOADING"] = str(self.cpu_ram_efficient_loading) + # There's no need to specify sync_module_states in FSDP2 + if self.fsdp_version == 1 and self.cpu_ram_efficient_loading and not self.sync_module_states: + warnings.warn( + "sync_module_states cannot be False since efficient cpu ram loading enabled. " + "Setting sync_module_states to True." + ) + self.sync_module_states = True + if isinstance(self.mixed_precision_policy, str): + # override is True since self.mixed_precision_policy is not None + # has to be overwritten with the correct mixed precision object + self.set_mixed_precision(self.mixed_precision_policy, override=True) + elif isinstance(self.mixed_precision_policy, dict): + self.set_mixed_precision(self.mixed_precision_policy) + if self.mixed_precision_policy is not None: + self.validate_mixed_precision_policy() + + if self.sync_module_states: + if is_npu_available(): + device = torch.npu.current_device() + elif is_mlu_available(): + device = torch.mlu.current_device() + elif is_musa_available(): + device = torch.musa.current_device() + elif is_cuda_available(): + device = torch.cuda.current_device() + elif is_xpu_available(): + device = torch.xpu.current_device() + elif is_hpu_available(): + device = torch.hpu.current_device() + else: + raise RuntimeError( + "There are currently no available devices found, must be one of 'XPU', 'CUDA', 'MLU', 'NPU', 'MUSA', or 'HPU'." + ) + # Create a function that will be used to initialize the parameters of the model + # when using `sync_module_states` + self.param_init_fn = lambda x: x.to_empty(device=device, recurse=False) + if is_torch_version("<", "2.7.0") and self.fsdp_version == 2 and self.ignored_modules is not None: + _fsdp2_warnings.add( + "FSDP2 ignored_params/ignored_modules is not available for torch version < 2.7.0" + "Setting ignored_modules to None." + ) + self.ignored_modules = None + # Single warning for all deprecation warnings due to FSDP2 conversion + if _fsdp2_warnings: + logger.warning("Multiple deprecation warnings due to FSDP2 conversion:\n".join(_fsdp2_warnings)) + + def set_state_dict_type(self, state_dict_type=None): + """ + Set the state dict config based on the `StateDictType`. + """ + from torch.distributed.fsdp.fully_sharded_data_parallel import ( + FullOptimStateDictConfig, + FullStateDictConfig, + ShardedOptimStateDictConfig, + ShardedStateDictConfig, + StateDictType, + ) + + # Override the state_dict_type if provided, typical use case: + # user trains with sharded, but final save is with full + if state_dict_type is not None: + self.state_dict_type = state_dict_type + + if self.state_dict_type is None: + self.state_dict_type = os.environ.get( + "FSDP_STATE_DICT_TYPE", + "FULL_STATE_DICT" if self.fsdp_version == 1 else "SHARDED_STATE_DICT", + ) + if isinstance(self.state_dict_type, str): + if self.state_dict_type.isdigit(): + self.state_dict_type = StateDictType(int(self.state_dict_type)) + else: + self.state_dict_type = StateDictType[self.state_dict_type.upper()] + + if self.state_dict_type == StateDictType.FULL_STATE_DICT: + if self.state_dict_config is None: + self.state_dict_config = FullStateDictConfig(offload_to_cpu=True, rank0_only=True) + if self.optim_state_dict_config is None: + self.optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=True) + elif self.state_dict_type == StateDictType.SHARDED_STATE_DICT: + if self.state_dict_config is None: + self.state_dict_config = ShardedStateDictConfig(offload_to_cpu=True) + if self.optim_state_dict_config is None: + self.optim_state_dict_config = ShardedOptimStateDictConfig(offload_to_cpu=True) + + if self.fsdp_version == 2 and self.state_dict_type == StateDictType.LOCAL_STATE_DICT: + raise ValueError( + "FSDP2 does not support LOCAL_STATE_DICT. " + "Please set `fsdp_state_dict_type` to `SHARDED_STATE_DICT` or `FULL_STATE_DICT`." + ) + + def set_auto_wrap_policy(self, model): + """ + Given `model`, creates an `auto_wrap_policy` based on the passed in policy and if we can use the + `transformer_cls_to_wrap` + """ + from torch.distributed.fsdp.wrap import ( + size_based_auto_wrap_policy, + transformer_auto_wrap_policy, + ) + + # First base off of `_no_split_modules` + no_split_modules = getattr(model, "_no_split_modules", None) + default_transformer_cls_names_to_wrap = list(no_split_modules) if no_split_modules is not None else [] + if self.auto_wrap_policy == transformer_auto_wrap_policy: + if self.transformer_cls_names_to_wrap is None: + self.transformer_cls_names_to_wrap = default_transformer_cls_names_to_wrap + transformer_cls_to_wrap = set() + for layer_class in self.transformer_cls_names_to_wrap: + transformer_cls = get_module_class_from_name(model, layer_class) + if transformer_cls is None: + raise ValueError(f"Could not find the transformer layer class {layer_class} in the model.") + transformer_cls_to_wrap.add(transformer_cls) + # Finally we set the auto_wrap_policy to a callable + self.auto_wrap_policy = functools.partial( + self.auto_wrap_policy, transformer_layer_cls=transformer_cls_to_wrap + ) + + elif self.auto_wrap_policy == size_based_auto_wrap_policy: + # If zero, we silently ignore it. + if self.min_num_params > 0: + self.auto_wrap_policy = functools.partial(self.auto_wrap_policy, min_num_params=self.min_num_params) + else: + self.auto_wrap_policy = None + + def set_mixed_precision(self, mixed_precision, buffer_autocast=False, override=False): + "Sets the mixed precision policy for FSDP" + mixed_precision_mapping = { + "fp8": torch.bfloat16, + "fp16": torch.float16, + "bf16": torch.bfloat16, + "fp32": torch.float32, + } + dtype = mixed_precision + if isinstance(mixed_precision, str): + dtype = mixed_precision_mapping.get(mixed_precision, None) + if dtype is None: + raise ValueError( + f"Invalid mixed precision: {mixed_precision}. Must be one of {list(mixed_precision_mapping.keys())}" + ) + elif isinstance(mixed_precision, torch.dtype) and mixed_precision not in mixed_precision_mapping.values(): + raise ValueError( + f"Invalid mixed precision: {mixed_precision}. Must be one of {list(mixed_precision_mapping.values())}" + ) + + buffer_type = torch.float32 if buffer_autocast else dtype + + if self.fsdp_version == 1: + from torch.distributed.fsdp import MixedPrecision + elif self.fsdp_version == 2: + from torch.distributed.fsdp import MixedPrecisionPolicy as MixedPrecision + + if override or self.mixed_precision_policy is None: + dtype_args = {"param_dtype": dtype, "reduce_dtype": dtype} + if self.fsdp_version == 1: + dtype_args["buffer_dtype"] = buffer_type + else: + dtype_args["output_dtype"] = dtype + # TODO(s1ro1): `cast_forward_inputs` for FSDP2? + self.mixed_precision_policy = MixedPrecision(**dtype_args) + elif isinstance(self.mixed_precision_policy, dict): + # Check for incompatible types + valid_keys = ["param_dtype", "reduce_dtype"] + ( + ["buffer_dtype"] if self.fsdp_version == 1 else ["output_dtype"] + ) + missing_keys = [k for k in valid_keys if k not in self.mixed_precision_policy] + invalid_values = [ + k for k, v in self.mixed_precision_policy.items() if v not in mixed_precision_mapping.values() + ] + if missing_keys or invalid_values: + raise ValueError( + f"Invalid mixed precision policy: {self.mixed_precision_policy}. " + f"Must be a `dict` with keys {valid_keys}." + f"Values must be one of {list(mixed_precision_mapping.values())}" + ) + self.mixed_precision_policy = MixedPrecision(**self.mixed_precision_policy) + + def validate_mixed_precision_policy(self): + """ + Validates the mixed precision policy, abstracted away to not bring in the imports if not needed. + """ + if self.fsdp_version == 2: + from torch.distributed.fsdp import MixedPrecisionPolicy as MixedPrecision + else: + from torch.distributed.fsdp import MixedPrecision + + if not isinstance(self.mixed_precision_policy, MixedPrecision): + required_type = ( + "`torch.distributed.fsdp.MixedPrecisionPolicy`" + if self.fsdp_version == 2 + else "`torch.distributed.fsdp.MixedPrecision`" + ) + raise ValueError(f"mixed_precision_policy must be an instance of {required_type}.") + + def set_cpu_offload(self): + if self.fsdp_version == 2: + from torch.distributed.fsdp import CPUOffloadPolicy, OffloadPolicy + else: + from torch.distributed.fsdp import CPUOffload + + if isinstance(self.cpu_offload, bool): + if self.fsdp_version == 2: + if not self.cpu_offload: + self.cpu_offload = OffloadPolicy() + else: + self.cpu_offload = CPUOffloadPolicy() + else: + self.cpu_offload = CPUOffload(offload_params=self.cpu_offload) + + def validate_cpu_offload(self): + if self.fsdp_version == 2: + from torch.distributed.fsdp import OffloadPolicy + else: + from torch.distributed.fsdp import CPUOffload + + if self.fsdp_version == 2 and not isinstance(self.cpu_offload, OffloadPolicy): + raise ValueError( + f"`cpu_offload` must be an instance of `torch.distributed.fsdp.OffloadPolicy` in FSDP2, got {self.cpu_offload}" + ) + if self.fsdp_version == 1 and not isinstance(self.cpu_offload, CPUOffload): + raise ValueError( + f"`cpu_offload` must be an instance of `torch.distributed.fsdp.CPUOffload` in FSDP1, got {self.cpu_offload}" + ) + + +@dataclass +class TorchTensorParallelPlugin: + """ + This plugin is used to enable tensor parallelism using PyTorch >= 2.0. + """ + + tp_size: int = field( + default=1, + metadata={"help": "tensor parallel size will be used in the device mesh preparation"}, + ) + + # torch_device_mesh is of type "torch.distributed.DeviceMesh" + torch_device_mesh: Optional["torch.distributed.DeviceMesh"] = field(default=None) + + +@dataclass +class TorchContextParallelConfig: + """ + This class holds the configuration for context parallelism in PyTorch. + """ + + cp_comm_strategy: Optional[str] = field( + default=None, + metadata={ + "help": "Communication strategy for context parallelism. Can be one of 'allgather' or 'alltoall'. Defaults to 'allgather'." + }, + ) + + def __post_init__(self): + if not is_torch_version(">=", BETA_CP_AVAILABLE_PYTORCH_VERSION): + raise ValueError( + f"FSDP2-based Context parallelism is only available in PyTorch {BETA_CP_AVAILABLE_PYTORCH_VERSION} and later versions. " + "Please upgrade your PyTorch version." + ) + + if self.cp_comm_strategy is None: + self.cp_comm_strategy = os.environ.get("PARALLELISM_CONFIG_CP_COMM_STRATEGY", "allgather") + if self.cp_comm_strategy not in ["allgather", "alltoall"]: + raise ValueError( + f"Invalid cp_comm_strategy: {self.cp_comm_strategy}. Must be one of 'allgather' or 'alltoall'." + ) + + +@dataclass +class DeepSpeedSequenceParallelConfig: + sp_seq_length: Optional[int] = field( + default=None, + metadata={ + "help": "Sequence length for when batches are all of the same length. For variable sequence lengths across batches set `sp_seq_length_is_variable=True` and leave this field unset" + }, + ) + sp_seq_length_is_variable: Optional[bool] = field( + default=None, + metadata={ + "help": "If `True` will work with a sequence length that may change between batches, in which case `sp_seq_length` value can be set to anything divisible by cp size or remain unset. If `False` then `sp_seq_length` needs to match the batch's sequence length dimension. The default is `True`." + }, + ) + sp_attn_implementation: Optional[str] = field( + default=None, + metadata={ + "help": "Attention implementation to use. Can be one of 'flash_attention_2', 'flash_attention_3' or 'sdpa'. Defaults to `sdpa`." + }, + ) + + def __post_init__(self): + # sp_seq_length_is_variable and sp_seq_length are interconnected + if self.sp_seq_length_is_variable is None: + self.sp_seq_length_is_variable = ( + os.environ.get("PARALLELISM_CONFIG_SP_SEQ_LENGTH_IS_VARIABLE", "true").lower() == "true" + ) + + if not self.sp_seq_length_is_variable and self.sp_seq_length is None: + if "PARALLELISM_CONFIG_SP_SEQ_LENGTH" not in os.environ: + raise ValueError( + "when `sp_seq_length_is_variable` is `False` `sp_seq_length` must be provided either through the constructor or the environment variable PARALLELISM_CONFIG_SP_SEQ_LENGTH" + ) + else: + self.sp_seq_length = os.environ.get("PARALLELISM_CONFIG_SP_SEQ_LENGTH") + self.sp_seq_length = None if self.sp_seq_length == "None" else int(self.sp_seq_length) + + if self.sp_attn_implementation is None: + self.sp_attn_implementation = os.environ.get("PARALLELISM_CONFIG_SP_ATTN_IMPLEMENTATION", None) + + if self.sp_attn_implementation is not None and self.sp_attn_implementation not in [ + "flash_attention_2", + "flash_attention_3", + "sdpa", + ]: + raise ValueError( + f"Invalid sp_attn_implementation: {self.sp_attn_implementation}. Must be one of 'flash_attention_2', 'flash_attention_3' or 'sdpa'." + ) + + +@dataclass +class TorchTensorParallelConfig: + """ + Use this object in your [`Accelerator`] to customize your torch tensor parallelism. + """ + + enable_async_tp: bool = False + + def __post_init__(self): + if not is_torch_version(">=", BETA_TP_AVAILABLE_PYTORCH_VERSION): + raise ValueError( + f"Torch tensor parallelism is only available in PyTorch {BETA_TP_AVAILABLE_PYTORCH_VERSION} and later versions. " + "Please upgrade your PyTorch version." + ) + + if not compare_versions("transformers", ">=", BETA_TP_AVAILABLE_TRANSFORMERS_VERSION): + raise ValueError(f"TP requires transformers >= {BETA_TP_AVAILABLE_TRANSFORMERS_VERSION}") + + if self.enable_async_tp: + warnings.warn("Async tensor parallelism is currently not supported, ignoring this option.") + + +@dataclass +class MegatronLMPlugin: + """ + Plugin for Megatron-LM to enable tensor, pipeline, sequence and data parallelism. Also to enable selective + activation recomputation and optimized fused kernels. + + Args: + tp_degree (`int`, defaults to `None`): + Tensor parallelism degree. + pp_degree (`int`, defaults to `None`): + Pipeline parallelism degree. + num_micro_batches (`int`, defaults to `None`): + Number of micro-batches. + gradient_clipping (`float`, defaults to `None`): + Gradient clipping value based on global L2 Norm (0 to disable). + sequence_parallelism (`bool`, defaults to `None`): + Enable sequence parallelism. + recompute_activations (`bool`, defaults to `None`): + Enable selective activation recomputation. + use_distributed_optimizr (`bool`, defaults to `None`): + Enable distributed optimizer. + pipeline_model_parallel_split_rank (`int`, defaults to `None`): + Rank where encoder and decoder should be split. + num_layers_per_virtual_pipeline_stage (`int`, defaults to `None`): + Number of layers per virtual pipeline stage. + is_train_batch_min (`str`, defaults to `True`): + If both tran & eval dataloaders are specified, this will decide the `micro_batch_size`. + train_iters (`int`, defaults to `None`): + Total number of samples to train over all training runs. Note that either train-iters or train-samples + should be provided when using `MegatronLMDummyScheduler`. + train_samples (`int`, defaults to `None`): + Total number of samples to train over all training runs. Note that either train-iters or train-samples + should be provided when using `MegatronLMDummyScheduler`. + weight_decay_incr_style (`str`, defaults to `'constant'`): + Weight decay increment function. choices=["constant", "linear", "cosine"]. + start_weight_decay (`float`, defaults to `None`): + Initial weight decay coefficient for L2 regularization. + end_weight_decay (`float`, defaults to `None`): + End of run weight decay coefficient for L2 regularization. + lr_decay_style (`str`, defaults to `'linear'`): + Learning rate decay function. choices=['constant', 'linear', 'cosine']. + lr_decay_iters (`int`, defaults to `None`): + Number of iterations for learning rate decay. If None defaults to `train_iters`. + lr_decay_samples (`int`, defaults to `None`): + Number of samples for learning rate decay. If None defaults to `train_samples`. + lr_warmup_iters (`int`, defaults to `None`): + Number of iterations to linearly warmup learning rate over. + lr_warmup_samples (`int`, defaults to `None`): + Number of samples to linearly warmup learning rate over. + lr_warmup_fraction (`float`, defaults to `None`): + Fraction of lr-warmup-(iters/samples) to linearly warmup learning rate over. + min_lr (`float`, defaults to `0`): + Minimum value for learning rate. The scheduler clip values below this threshold. + consumed_samples (`List`, defaults to `None`): + Number of samples consumed in the same order as the dataloaders to `accelerator.prepare` call. + no_wd_decay_cond (`Optional`, defaults to `None`): + Condition to disable weight decay. + scale_lr_cond (`Optional`, defaults to `None`): + Condition to scale learning rate. + lr_mult (`float`, defaults to `1.0`): + Learning rate multiplier. + megatron_dataset_flag (`bool`, defaults to `False`): + Whether the format of dataset follows Megatron-LM Indexed/Cached/MemoryMapped format. + seq_length (`int`, defaults to `None`): + Maximum sequence length to process. + encoder_seq_length (`int`, defaults to `None`): + Maximum sequence length to process for the encoder. + decoder_seq_length (`int`, defaults to `None`): + Maximum sequence length to process for the decoder. + tensorboard_dir (`str`, defaults to `None`): + Path to save tensorboard logs. + set_all_logging_options (`bool`, defaults to `False`): + Whether to set all logging options. + eval_iters (`int`, defaults to `100`): + Number of iterations to run for evaluation validation/test for. + eval_interval (`int`, defaults to `1000`): + Interval between running evaluation on validation set. + return_logits (`bool`, defaults to `False`): + Whether to return logits from the model. + custom_train_step_class (`Optional`, defaults to `None`): + Custom train step class. + custom_train_step_kwargs (`Optional`, defaults to `None`): + Custom train step kwargs. + custom_model_provider_function (`Optional`, defaults to `None`): + Custom model provider function. + custom_prepare_model_function (`Optional`, defaults to `None`): + Custom prepare model function. + custom_megatron_datasets_provider_function (`Optional`, defaults to `None`): + Custom megatron train_valid_test datasets provider function. + custom_get_batch_function (`Optional`, defaults to `None`): + Custom get batch function. + custom_loss_function (`Optional`, defaults to `None`): + Custom loss function. + other_megatron_args (`Optional`, defaults to `None`): + Other Megatron-LM arguments. Please refer Megatron-LM. + """ + + tp_degree: int = field(default=None, metadata={"help": "tensor parallelism degree."}) + pp_degree: int = field(default=None, metadata={"help": "pipeline parallelism degree."}) + use_custom_fsdp: bool = field(default=None, metadata={"help": "use custom fsdp."}) + overlap_cpu_optimizer_d2h_h2d: bool = field( + default=None, metadata={"help": "overlap CPU optimizer step, gradients D2H and updated parameters H2D."} + ) + no_load_optim: bool = field(default=None, metadata={"help": "do not load optimizer."}) + eod_mask_loss: bool = field(default=None, metadata={"help": "use eod mask loss."}) + no_save_optim: bool = field(default=None, metadata={"help": "do not save optimizer."}) + optimizer_cpu_offload: bool = field(default=None, metadata={"help": "use CPU offload for optimizer."}) + use_precision_aware_optimizer: bool = field(default=None, metadata={"help": "use precision aware optimizer."}) + decoder_last_pipeline_num_layers: int = field( + default=None, + metadata={ + "help": "decoder last pipeline number of layers, default None is even split of transformer layers across all pipeline stages." + }, + ) + recompute_granularity: str = field(default=None, metadata={"help": "recompute granularity (full, selective)."}) + recompute_method: str = field(default=None, metadata={"help": "recompute method (uniform, block)."}) + recompute_num_layers: int = field(default=None, metadata={"help": "number of layers to recompute."}) + attention_backend: bool = field(default=None, metadata={"help": "enable attention backend."}) + expert_model_parallel_size: int = field(default=None, metadata={"help": "expert model parallel size."}) + context_parallel_size: int = field(default=None, metadata={"help": "context parallel size."}) + attention_dropout: float = field(default=None, metadata={"help": "attention dropout rate."}) + hidden_dropout: float = field(default=None, metadata={"help": "hidden dropout rate."}) + attention_softmax_in_fp32: bool = field(default=None, metadata={"help": "use fp32 for attention softmax."}) + expert_tensor_parallel_size: int = field(default=None, metadata={"help": "expert tensor parallel size."}) + calculate_per_token_loss: bool = field(default=None, metadata={"help": "calculate per token loss."}) + use_rotary_position_embeddings: bool = field(default=None, metadata={"help": "use rotary position embeddings."}) + num_micro_batches: int = field(default=None, metadata={"help": "number of micro-batches."}) + gradient_clipping: float = field( + default=None, + metadata={"help": "gradient clipping value based on global L2 Norm (0 to disable)"}, + ) + sequence_parallelism: bool = field( + default=None, + metadata={"help": "enable sequence parallelism"}, + ) + recompute_activations: bool = field( + default=None, + metadata={"help": "enable selective activation recomputation"}, + ) + use_distributed_optimizer: bool = field( + default=None, + metadata={"help": "enable distributed optimizer"}, + ) + pipeline_model_parallel_split_rank: int = field( + default=None, + metadata={"help": "Rank where encoder and decoder should be split."}, + ) + num_layers_per_virtual_pipeline_stage: int = field( + default=None, metadata={"help": "Number of layers per virtual pipeline stage."} + ) + is_train_batch_min: str = field( + default=True, + metadata={"help": "If both train & eval dataloaders are specified, this will decide the micro_batch_size"}, + ) + train_iters: int = field( + default=None, + metadata={ + "help": "Total number of iterations to train over all training runs. " + "Note that either train-iters or train-samples should be provided when using `MegatronLMDummyScheduler`" + }, + ) + train_samples: int = field( + default=None, + metadata={ + "help": "Total number of samples to train over all training runs. " + "Note that either train-iters or train-samples should be provided when using `MegatronLMDummyScheduler`" + }, + ) + weight_decay_incr_style: str = field( + default="constant", + metadata={"help": 'Weight decay increment function. choices=["constant", "linear", "cosine"]. '}, + ) + start_weight_decay: float = field( + default=None, + metadata={"help": "Initial weight decay coefficient for L2 regularization."}, + ) + end_weight_decay: float = field( + default=None, + metadata={"help": "End of run weight decay coefficient for L2 regularization."}, + ) + lr_decay_style: str = field( + default="linear", + metadata={"help": "Learning rate decay function. choices=['constant', 'linear', 'cosine']."}, + ) + lr_decay_iters: int = field( + default=None, + metadata={"help": "Number of iterations for learning rate decay. If None defaults to `train_iters`."}, + ) + lr_decay_samples: int = field( + default=None, + metadata={"help": "Number of samples for learning rate decay. If None defaults to `train_samples`."}, + ) + lr_warmup_iters: int = field( + default=None, + metadata={"help": "number of iterations to linearly warmup learning rate over."}, + ) + lr_warmup_samples: int = field( + default=None, + metadata={"help": "number of samples to linearly warmup learning rate over."}, + ) + lr_warmup_fraction: float = field( + default=None, + metadata={"help": "fraction of lr-warmup-(iters/samples) to linearly warmup learning rate over."}, + ) + min_lr: float = field( + default=0, + metadata={"help": "Minimum value for learning rate. The scheduler clip values below this threshold."}, + ) + consumed_samples: list[int] = field( + default=None, + metadata={ + "help": "Number of samples consumed in the same order as the dataloaders to `accelerator.prepare` call." + }, + ) + no_wd_decay_cond: Optional[Callable] = field(default=None, metadata={"help": "Condition to disable weight decay."}) + scale_lr_cond: Optional[Callable] = field(default=None, metadata={"help": "Condition to scale learning rate."}) + lr_mult: float = field(default=1.0, metadata={"help": "Learning rate multiplier."}) + megatron_dataset_flag: bool = field( + default=False, + metadata={"help": "Whether the format of dataset follows Megatron-LM Indexed/Cached/MemoryMapped format."}, + ) + seq_length: int = field( + default=None, + metadata={"help": "Maximum sequence length to process."}, + ) + encoder_seq_length: int = field( + default=None, + metadata={"help": "Maximum sequence length to process for the encoder."}, + ) + decoder_seq_length: int = field( + default=None, + metadata={"help": "Maximum sequence length to process for the decoder."}, + ) + tensorboard_dir: str = field( + default=None, + metadata={"help": "Path to save tensorboard logs."}, + ) + set_all_logging_options: bool = field( + default=False, + metadata={"help": "Whether to set all logging options."}, + ) + eval_iters: int = field( + default=100, + metadata={"help": "Number of iterations to run for evaluation validation/test for."}, + ) + eval_interval: int = field( + default=1000, + metadata={"help": "Interval between running evaluation on validation set."}, + ) + return_logits: bool = field( + default=False, + metadata={"help": "Whether to return logits from the model."}, + ) + + # custom train step args + custom_train_step_class: Optional[Any] = field( + default=None, + metadata={"help": "Custom train step class."}, + ) + custom_train_step_kwargs: Optional[dict[str, Any]] = field( + default=None, + metadata={"help": "Custom train step kwargs."}, + ) + + # custom model args + custom_model_provider_function: Optional[Callable] = field( + default=None, + metadata={"help": "Custom model provider function."}, + ) + custom_prepare_model_function: Optional[Callable] = field( + default=None, + metadata={"help": "Custom prepare model function."}, + ) + custom_megatron_datasets_provider_function: Optional[Callable] = field( + default=None, + metadata={"help": "Custom megatron train_valid_test datasets provider function."}, + ) + custom_get_batch_function: Optional[Callable] = field( + default=None, + metadata={"help": "Custom get batch function."}, + ) + custom_loss_function: Optional[Callable] = field( + default=None, + metadata={"help": "Custom loss function."}, + ) + + # remaining args such as enabling Alibi/ROPE positional embeddings, + # wandb logging, Multi-Query Attention, etc. + other_megatron_args: Optional[dict[str, Any]] = field( + default=None, + metadata={"help": "Other Megatron-LM arguments. Please refer Megatron-LM"}, + ) + + def __post_init__(self): + prefix = "MEGATRON_LM_" + if self.tp_degree is None: + self.tp_degree = int(os.environ.get(prefix + "TP_DEGREE", 1)) + if self.pp_degree is None: + self.pp_degree = int(os.environ.get(prefix + "PP_DEGREE", 1)) + if self.use_custom_fsdp is None: + self.use_custom_fsdp = str_to_bool(os.environ.get(prefix + "USE_CUSTOM_FSDP", "False")) == 1 + if self.no_load_optim is None: + self.no_load_optim = str_to_bool(os.environ.get(prefix + "NO_LOAD_OPTIM", "False")) == 1 + if self.eod_mask_loss is None: + self.eod_mask_loss = str_to_bool(os.environ.get(prefix + "EOD_MASK_LOSS", "False")) == 1 + if self.no_save_optim is None: + self.no_save_optim = str_to_bool(os.environ.get(prefix + "NO_SAVE_OPTIM", "False")) == 1 + if self.optimizer_cpu_offload is None: + self.optimizer_cpu_offload = str_to_bool(os.environ.get(prefix + "OPTIMIZER_CPU_OFFLOAD", "False")) == 1 + if self.overlap_cpu_optimizer_d2h_h2d is None: + self.overlap_cpu_optimizer_d2h_h2d = ( + str_to_bool(os.environ.get(prefix + "OVERLAP_CPU_OPTIMIZER_D2H_H2D", "False")) == 1 + ) + if self.use_precision_aware_optimizer is None: + self.use_precision_aware_optimizer = ( + str_to_bool(os.environ.get(prefix + "USE_PRECISION_AWARE_OPTIMIZER", "False")) == 1 + ) + if self.decoder_last_pipeline_num_layers is None: + if os.environ.get(prefix + "DECODER_LAST_PIPELINE_NUM_LAYERS") is not None: + self.decoder_last_pipeline_num_layers = int( + os.environ.get(prefix + "DECODER_LAST_PIPELINE_NUM_LAYERS", 0) + ) + else: + self.decoder_last_pipeline_num_layers = None + if self.num_micro_batches is None: + self.num_micro_batches = int(os.environ.get(prefix + "NUM_MICRO_BATCHES", 1)) + if self.gradient_clipping is None: + self.gradient_clipping = float(os.environ.get(prefix + "GRADIENT_CLIPPING", 1.0)) + if self.recompute_activations is None: + self.recompute_activations = str_to_bool(os.environ.get(prefix + "RECOMPUTE_ACTIVATIONS", "False")) == 1 + if self.use_distributed_optimizer is None: + self.use_distributed_optimizer = ( + str_to_bool(os.environ.get(prefix + "USE_DISTRIBUTED_OPTIMIZER", "False")) == 1 + ) + if self.sequence_parallelism is None: + self.sequence_parallelism = str_to_bool(os.environ.get(prefix + "SEQUENCE_PARALLELISM", "False")) == 1 + if self.recompute_granularity is None: + self.recompute_granularity = os.environ.get(prefix + "RECOMPUTE_GRANULARITY", "full") + if self.recompute_method is None: + self.recompute_method = os.environ.get(prefix + "RECOMPUTE_METHOD", "uniform") + if self.recompute_num_layers is None: + self.recompute_num_layers = int(os.environ.get(prefix + "RECOMPUTE_NUM_LAYERS", 1)) + if self.attention_backend is None: + self.attention_backend = str_to_bool(os.environ.get(prefix + "ATTENTION_BACKEND", "True")) == 1 + if self.expert_model_parallel_size is None: + self.expert_model_parallel_size = int(os.environ.get(prefix + "EXPERT_MODEL_PARALLEL_SIZE", 1)) + if self.context_parallel_size is None: + self.context_parallel_size = int(os.environ.get(prefix + "CONTEXT_PARALLEL_SIZE", 2)) + if self.attention_dropout is None: + self.attention_dropout = float(os.environ.get(prefix + "ATTENTION_DROPOUT", "0.0")) + if self.hidden_dropout is None: + self.hidden_dropout = float(os.environ.get(prefix + "HIDDEN_DROPOUT", "0.0")) + if self.attention_softmax_in_fp32 is None: + self.attention_softmax_in_fp32 = ( + str_to_bool(os.environ.get(prefix + "ATTENTION_SOFTMAX_IN_FP32", "True")) == 1 + ) + if self.expert_tensor_parallel_size is None: + self.expert_tensor_parallel_size = int(os.environ.get(prefix + "EXPERT_TENSOR_PARALLEL_SIZE", 1)) + if self.calculate_per_token_loss is None: + self.calculate_per_token_loss = ( + str_to_bool(os.environ.get(prefix + "CALCULATE_PER_TOKEN_LOSS", "True")) == 1 + ) + if self.use_rotary_position_embeddings is None: + self.use_rotary_position_embeddings = ( + str_to_bool(os.environ.get(prefix + "USE_ROTARY_POSITION_EMBEDDINGS", "True")) == 1 + ) + + if self.pp_degree > 1 or self.use_distributed_optimizer: + self.DDP_impl = "local" + else: + self.DDP_impl = "torch" + + if self.consumed_samples is not None: + if len(self.consumed_samples) == 1: + self.consumed_samples.extend([0, 0]) + elif len(self.consumed_samples) == 2: + self.consumed_samples.append(0) + + self.megatron_lm_default_args = { + "tensor_model_parallel_size": self.tp_degree, + "pipeline_model_parallel_size": self.pp_degree, + "pipeline_model_parallel_split_rank": self.pipeline_model_parallel_split_rank, + "num_layers_per_virtual_pipeline_stage": self.num_layers_per_virtual_pipeline_stage, + "DDP_impl": self.DDP_impl, + "use_distributed_optimizer": self.use_distributed_optimizer, + "sequence_parallel": self.sequence_parallelism, + "clip_grad": self.gradient_clipping, + "num_micro_batches": self.num_micro_batches, + "consumed_samples": self.consumed_samples, + "no_wd_decay_cond": self.no_wd_decay_cond, + "scale_lr_cond": self.scale_lr_cond, + "lr_mult": self.lr_mult, + "megatron_dataset_flag": self.megatron_dataset_flag, + "eval_iters": self.eval_iters, + "eval_interval": self.eval_interval, + "use_custom_fsdp": self.use_custom_fsdp, + "no_load_optim": self.no_load_optim, + "eod_mask_loss": self.eod_mask_loss, + "no_save_optim": self.no_save_optim, + "optimizer_cpu_offload": self.optimizer_cpu_offload, + "overlap_cpu_optimizer_d2h_h2d": self.overlap_cpu_optimizer_d2h_h2d, + "use_precision_aware_optimizer": self.use_precision_aware_optimizer, + "decoder_last_pipeline_num_layers": self.decoder_last_pipeline_num_layers, + "recompute_granularity": self.recompute_granularity, + "recompute_method": self.recompute_method, + "recompute_num_layers": self.recompute_num_layers, + "attention_backend": self.attention_backend, + "expert_model_parallel_size": self.expert_model_parallel_size, + "context_parallel_size": self.context_parallel_size, + "attention_dropout": self.attention_dropout, + "hidden_dropout": self.hidden_dropout, + "attention_softmax_in_fp32": self.attention_softmax_in_fp32, + "expert_tensor_parallel_size": self.expert_tensor_parallel_size, + "calculate_per_token_loss": self.calculate_per_token_loss, + "use_rotary_position_embeddings": self.use_rotary_position_embeddings, + } + if self.tensorboard_dir is not None: + self.megatron_lm_default_args["tensorboard_dir"] = self.tensorboard_dir + if self.set_all_logging_options: + self.set_tensorboard_logging_options() + if self.other_megatron_args is not None: + self.megatron_lm_default_args.update(self.other_megatron_args) + + def set_network_size_args(self, model, batch_data=None): + model_config_type = model.config.model_type.lower() + for model_type in MODEL_CONFIGS_TO_MEGATRON_PARSERS.keys(): + if model_type in model_config_type: + MODEL_CONFIGS_TO_MEGATRON_PARSERS[model_type](self, model, batch_data) + return + raise ValueError( + f"Accelerate Megatron-LM integration not supports {model_config_type} model. " + "You can add your own model config parser." + ) + + def set_mixed_precision(self, mixed_precision): + if mixed_precision == "fp16": + self.megatron_lm_default_args["fp16"] = True + elif mixed_precision == "bf16": + self.megatron_lm_default_args["bf16"] = True + self.DDP_impl = "local" + self.megatron_lm_default_args["DDP_impl"] = self.DDP_impl + + def set_training_args(self, micro_batch_size, dp_degree): + self.data_parallel_size = dp_degree + self.micro_batch_size = micro_batch_size + self.global_batch_size = dp_degree * micro_batch_size * self.num_micro_batches + self.megatron_lm_default_args["data_parallel_size"] = self.data_parallel_size + self.megatron_lm_default_args["micro_batch_size"] = self.micro_batch_size + self.megatron_lm_default_args["global_batch_size"] = self.global_batch_size + + def set_optimizer_type(self, optimizer): + optimizer_name = optimizer.__class__.__name__.lower() + if "adam" in optimizer_name: + self.megatron_lm_default_args["optimizer"] = "adam" + self.megatron_lm_default_args["adam_beta1"] = optimizer.defaults["betas"][0] + self.megatron_lm_default_args["adam_beta2"] = optimizer.defaults["betas"][1] + self.megatron_lm_default_args["adam_eps"] = optimizer.defaults["eps"] + elif "sgd" in optimizer_name: + self.megatron_lm_default_args["optimizer"] = "sgd" + self.megatron_lm_default_args["sgd_momentum"] = optimizer.defaults["momentum"] + else: + raise ValueError(f"Optimizer {optimizer_name} is not supported by Megatron-LM") + + self.megatron_lm_default_args["lr"] = optimizer.defaults["lr"] + self.megatron_lm_default_args["weight_decay"] = optimizer.defaults["weight_decay"] + + def set_scheduler_args(self, scheduler): + if self.train_iters is None: + self.train_iters = scheduler.total_num_steps // self.megatron_lm_default_args["data_parallel_size"] + if self.train_samples is not None: + self.train_samples = None + warnings.warn( + "Ignoring `train_samples` as `train_iters` based on scheduler is being used for training." + ) + if self.lr_warmup_iters is None: + self.lr_warmup_iters = scheduler.warmup_num_steps // self.megatron_lm_default_args["data_parallel_size"] + if self.lr_warmup_samples is not None: + warnings.warn( + "Ignoring `lr_warmup_samples` as `lr_warmup_iters` based on scheduler is being used for training." + ) + self.lr_warmup_samples = 0 + + self.megatron_lm_default_args["train_iters"] = self.train_iters + self.megatron_lm_default_args["lr_warmup_iters"] = self.lr_warmup_iters + self.megatron_lm_default_args["train_samples"] = self.train_samples + self.megatron_lm_default_args["lr_warmup_samples"] = self.lr_warmup_samples + self.megatron_lm_default_args["lr_decay_iters"] = self.lr_decay_iters + self.megatron_lm_default_args["lr_decay_samples"] = self.lr_decay_samples + self.megatron_lm_default_args["lr_warmup_fraction"] = self.lr_warmup_fraction + self.megatron_lm_default_args["lr_decay_style"] = self.lr_decay_style + self.megatron_lm_default_args["weight_decay_incr_style"] = self.weight_decay_incr_style + self.megatron_lm_default_args["start_weight_decay"] = self.start_weight_decay + self.megatron_lm_default_args["end_weight_decay"] = self.end_weight_decay + self.megatron_lm_default_args["min_lr"] = self.min_lr + + def set_tensorboard_logging_options(self): + from megatron.training.arguments import _add_logging_args + + parser = argparse.ArgumentParser() + parser = _add_logging_args(parser) + logging_args = parser.parse_known_args() + self.dataset_args = vars(logging_args[0]) + for key, value in self.dataset_args.items(): + if key.startswith("log_"): + self.megatron_lm_default_args[key] = True + elif key.startswith("no_log_"): + self.megatron_lm_default_args[key.replace("no_", "")] = True + + +MODEL_CONFIGS_TO_MEGATRON_PARSERS = {} + + +def add_model_config_to_megatron_parser(model_type: str): + def add_model_config_parser_helper(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + MODEL_CONFIGS_TO_MEGATRON_PARSERS[model_type] = func + return wrapper + + return add_model_config_parser_helper + + +@add_model_config_to_megatron_parser("megatron-bert") +def parse_bert_config(megatron_lm_plugin, model, batch_data): + model_type_name = "bert" + num_layers = model.config.num_hidden_layers + hidden_size = model.config.hidden_size + num_attention_heads = model.config.num_attention_heads + max_position_embeddings = model.config.max_position_embeddings + num_labels = model.config.num_labels + orig_vocab_size = model.config.vocab_size + pretraining_flag = False + if "maskedlm" in model.__class__.__name__.lower(): + pretraining_flag = True + if megatron_lm_plugin.seq_length is not None: + if megatron_lm_plugin.encoder_seq_length is not None: + warnings.warn("Both `seq_length` and `encoder_seq_length` are set. Using `encoder_seq_length`.") + megatron_lm_plugin.seq_length = megatron_lm_plugin.encoder_seq_length + elif megatron_lm_plugin.encoder_seq_length is not None: + megatron_lm_plugin.seq_length = megatron_lm_plugin.encoder_seq_length + elif batch_data is not None: + megatron_lm_plugin.seq_length = batch_data["input_ids"].shape[1] + else: + megatron_lm_plugin.seq_length = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["seq_length"] = megatron_lm_plugin.seq_length + megatron_lm_plugin.megatron_lm_default_args["model_type_name"] = model_type_name + megatron_lm_plugin.megatron_lm_default_args["num_layers"] = num_layers + megatron_lm_plugin.megatron_lm_default_args["hidden_size"] = hidden_size + megatron_lm_plugin.megatron_lm_default_args["num_attention_heads"] = num_attention_heads + megatron_lm_plugin.megatron_lm_default_args["max_position_embeddings"] = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["pretraining_flag"] = pretraining_flag + megatron_lm_plugin.megatron_lm_default_args["orig_vocab_size"] = orig_vocab_size + megatron_lm_plugin.megatron_lm_default_args["model_return_dict"] = model.config.return_dict + megatron_lm_plugin.megatron_lm_default_args["num_labels"] = num_labels + + +@add_model_config_to_megatron_parser("gpt2") +def parse_gpt2_config(megatron_lm_plugin, model, batch_data): + model_type_name = "gpt" + num_layers = model.config.n_layer + hidden_size = model.config.n_embd + num_attention_heads = model.config.n_head + max_position_embeddings = model.config.n_positions + orig_vocab_size = model.config.vocab_size + pretraining_flag = True + if megatron_lm_plugin.seq_length is not None: + if megatron_lm_plugin.decoder_seq_length is not None: + warnings.warn("Both `seq_length` and `decoder_seq_length` are set. Using `decoder_seq_length`.") + megatron_lm_plugin.seq_length = megatron_lm_plugin.decoder_seq_length + elif megatron_lm_plugin.decoder_seq_length is not None: + megatron_lm_plugin.seq_length = megatron_lm_plugin.decoder_seq_length + elif batch_data is not None: + megatron_lm_plugin.seq_length = batch_data["input_ids"].shape[1] + else: + megatron_lm_plugin.seq_length = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["seq_length"] = megatron_lm_plugin.seq_length + megatron_lm_plugin.megatron_lm_default_args["return_logits"] = megatron_lm_plugin.return_logits + megatron_lm_plugin.megatron_lm_default_args["tokenizer_type"] = "GPT2BPETokenizer" + megatron_lm_plugin.megatron_lm_default_args["model_type_name"] = model_type_name + megatron_lm_plugin.megatron_lm_default_args["num_layers"] = num_layers + megatron_lm_plugin.megatron_lm_default_args["hidden_size"] = hidden_size + megatron_lm_plugin.megatron_lm_default_args["num_attention_heads"] = num_attention_heads + megatron_lm_plugin.megatron_lm_default_args["max_position_embeddings"] = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["pretraining_flag"] = pretraining_flag + megatron_lm_plugin.megatron_lm_default_args["orig_vocab_size"] = orig_vocab_size + megatron_lm_plugin.megatron_lm_default_args["model_return_dict"] = model.config.return_dict + + +@add_model_config_to_megatron_parser("t5") +def parse_t5_config(megatron_lm_plugin, model, batch_data): + model_type_name = "t5" + num_layers = model.config.num_layers + hidden_size = model.config.d_model + num_attention_heads = model.config.num_heads + max_position_embeddings = model.config.n_positions if hasattr(model.config, "n_positions") else 1024 + orig_vocab_size = model.config.vocab_size + pretraining_flag = True + if megatron_lm_plugin.encoder_seq_length is None: + if batch_data is not None: + megatron_lm_plugin.encoder_seq_length = batch_data["input_ids"].shape[1] + else: + megatron_lm_plugin.encoder_seq_length = max_position_embeddings + if megatron_lm_plugin.decoder_seq_length is None: + if batch_data is not None: + megatron_lm_plugin.decoder_seq_length = batch_data["labels"].shape[1] + else: + megatron_lm_plugin.decoder_seq_length = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["encoder_seq_length"] = megatron_lm_plugin.encoder_seq_length + megatron_lm_plugin.megatron_lm_default_args["decoder_seq_length"] = megatron_lm_plugin.decoder_seq_length + megatron_lm_plugin.megatron_lm_default_args["model_type_name"] = model_type_name + megatron_lm_plugin.megatron_lm_default_args["num_layers"] = num_layers + megatron_lm_plugin.megatron_lm_default_args["hidden_size"] = hidden_size + megatron_lm_plugin.megatron_lm_default_args["num_attention_heads"] = num_attention_heads + megatron_lm_plugin.megatron_lm_default_args["max_position_embeddings"] = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["pretraining_flag"] = pretraining_flag + megatron_lm_plugin.megatron_lm_default_args["orig_vocab_size"] = orig_vocab_size + megatron_lm_plugin.megatron_lm_default_args["model_return_dict"] = model.config.return_dict + + +@add_model_config_to_megatron_parser("llama") +def parse_llama_config(megatron_lm_plugin, model, batch_data): + model_type_name = "gpt" + num_layers = model.config.num_hidden_layers + pretraining_flag = True + hidden_size = model.config.hidden_size + num_attention_heads = model.config.num_attention_heads + orig_vocab_size = model.config.vocab_size + + max_position_embeddings = model.config.max_position_embeddings + seq_length = getattr(model.config, "max_sequence_length", None) + if megatron_lm_plugin.seq_length is None: + if seq_length is not None: + megatron_lm_plugin.seq_length = seq_length + elif megatron_lm_plugin.decoder_seq_length is not None: + megatron_lm_plugin.seq_length = megatron_lm_plugin.decoder_seq_length + elif batch_data is not None: + megatron_lm_plugin.seq_length = batch_data["input_ids"].shape[1] + else: + megatron_lm_plugin.seq_length = max_position_embeddings + + megatron_lm_plugin.megatron_lm_default_args["return_logits"] = megatron_lm_plugin.return_logits + megatron_lm_plugin.megatron_lm_default_args["tokenizer_type"] = "Llama2Tokenizer" + megatron_lm_plugin.megatron_lm_default_args["model_type_name"] = model_type_name + megatron_lm_plugin.megatron_lm_default_args["num_layers"] = num_layers + megatron_lm_plugin.megatron_lm_default_args["pretraining_flag"] = pretraining_flag + megatron_lm_plugin.megatron_lm_default_args["hidden_size"] = hidden_size + megatron_lm_plugin.megatron_lm_default_args["num_attention_heads"] = num_attention_heads + megatron_lm_plugin.megatron_lm_default_args["orig_vocab_size"] = orig_vocab_size + megatron_lm_plugin.megatron_lm_default_args["max_position_embeddings"] = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["seq_length"] = megatron_lm_plugin.seq_length + megatron_lm_plugin.megatron_lm_default_args["model_return_dict"] = model.config.return_dict + + +@add_model_config_to_megatron_parser("glm4_moe") +def parse_glm4_moe_config(megatron_lm_plugin, model, batch_data): + model_type_name = "gpt" + num_layers = model.config.num_hidden_layers + pretraining_flag = False + hidden_size = model.config.hidden_size + num_attention_heads = model.config.num_attention_heads + orig_vocab_size = model.config.vocab_size + + max_position_embeddings = model.config.max_position_embeddings + seq_length = getattr(model.config, "max_sequence_length", None) + if megatron_lm_plugin.seq_length is None: + if seq_length is not None: + megatron_lm_plugin.seq_length = seq_length + elif megatron_lm_plugin.decoder_seq_length is not None: + megatron_lm_plugin.seq_length = megatron_lm_plugin.decoder_seq_length + elif batch_data is not None: + megatron_lm_plugin.seq_length = batch_data["input_ids"].shape[1] + else: + megatron_lm_plugin.seq_length = max_position_embeddings + + megatron_lm_plugin.megatron_lm_default_args["return_logits"] = megatron_lm_plugin.return_logits + megatron_lm_plugin.megatron_lm_default_args["tokenizer_type"] = "HuggingFaceTokenizer" + megatron_lm_plugin.megatron_lm_default_args["model_type_name"] = model_type_name + megatron_lm_plugin.megatron_lm_default_args["num_layers"] = num_layers + megatron_lm_plugin.megatron_lm_default_args["pretraining_flag"] = pretraining_flag + megatron_lm_plugin.megatron_lm_default_args["hidden_size"] = hidden_size + megatron_lm_plugin.megatron_lm_default_args["num_attention_heads"] = num_attention_heads + megatron_lm_plugin.megatron_lm_default_args["kv_channels"] = model.config.head_dim + megatron_lm_plugin.megatron_lm_default_args["orig_vocab_size"] = orig_vocab_size + megatron_lm_plugin.megatron_lm_default_args["max_position_embeddings"] = max_position_embeddings + megatron_lm_plugin.megatron_lm_default_args["seq_length"] = megatron_lm_plugin.seq_length + megatron_lm_plugin.megatron_lm_default_args["model_return_dict"] = model.config.return_dict + megatron_lm_plugin.megatron_lm_default_args["position_embedding_type"] = "rope" + megatron_lm_plugin.megatron_lm_default_args["original_model_type"] = model.config.model_type + megatron_lm_plugin.megatron_lm_default_args["qk_layernorm"] = ( + model.config.use_qk_norm + ) # this is true for glm4.5 but False for glm4.5-air. + megatron_lm_plugin.megatron_lm_default_args["add_bias_linear"] = False + megatron_lm_plugin.megatron_lm_default_args["group_query_attention"] = True + megatron_lm_plugin.megatron_lm_default_args["num_query_groups"] = model.config.num_key_value_heads + megatron_lm_plugin.megatron_lm_default_args["ffn_hidden_size"] = model.config.intermediate_size + megatron_lm_plugin.megatron_lm_default_args["add_qkv_bias"] = True + megatron_lm_plugin.megatron_lm_default_args["normalization"] = "RMSNorm" + megatron_lm_plugin.megatron_lm_default_args["rotary-percent"] = 0.5 + megatron_lm_plugin.megatron_lm_default_args["swiglu"] = True + megatron_lm_plugin.megatron_lm_default_args["moe_ffn_hidden_size"] = model.config.moe_intermediate_size + megatron_lm_plugin.megatron_lm_default_args["moe_shared_expert_intermediate_size"] = ( + model.config.moe_intermediate_size + ) + megatron_lm_plugin.megatron_lm_default_args["moe_router_pre_softmax"] = True + megatron_lm_plugin.megatron_lm_default_args["moe_router_score_function"] = "sigmoid" + megatron_lm_plugin.megatron_lm_default_args["moe_router_enable_expert_bias"] = True + megatron_lm_plugin.megatron_lm_default_args["moe_router_bias_update_rate"] = 0 + megatron_lm_plugin.megatron_lm_default_args["moe_router_load_balancing_type"] = "seq_aux_loss" + megatron_lm_plugin.megatron_lm_default_args["moe_token_dispatcher_type"] = "alltoall" + megatron_lm_plugin.megatron_lm_default_args["moe_router_topk"] = model.config.num_experts_per_tok + megatron_lm_plugin.megatron_lm_default_args["moe_router_topk_scaling_factor"] = model.config.routed_scaling_factor + megatron_lm_plugin.megatron_lm_default_args["moe_layer_freq"] = [0] * model.config.first_k_dense_replace + [1] * ( + model.config.num_hidden_layers - model.config.first_k_dense_replace + ) + megatron_lm_plugin.megatron_lm_default_args["num_experts"] = model.config.n_routed_experts + megatron_lm_plugin.megatron_lm_default_args["moe_grouped_gemm"] = True + megatron_lm_plugin.megatron_lm_default_args["moe_router_dtype"] = "fp32" + megatron_lm_plugin.megatron_lm_default_args["moe_permute_fusion"] = True + megatron_lm_plugin.megatron_lm_default_args["moe_aux_loss_coeff"] = 0 + megatron_lm_plugin.megatron_lm_default_args["rotary_base"] = model.config.rope_theta + megatron_lm_plugin.megatron_lm_default_args["rope_type"] = "rope" + megatron_lm_plugin.megatron_lm_default_args["rotary_percent"] = model.config.partial_rotary_factor + megatron_lm_plugin.megatron_lm_default_args["norm_epsilon"] = 1e-3 + megatron_lm_plugin.megatron_lm_default_args["use_flash_attn"] = True + megatron_lm_plugin.megatron_lm_default_args["eos_token_id"] = model.config.eos_token_id + if getattr(model.config, "fp8_param", False): + megatron_lm_plugin.megatron_lm_default_args["fp8"] = model.config.fp8 + megatron_lm_plugin.megatron_lm_default_args["fp8_param"] = model.config.fp8_param + megatron_lm_plugin.megatron_lm_default_args["fp8_param_gather"] = model.config.fp8_param_gather + megatron_lm_plugin.megatron_lm_default_args["fp8_recipe"] = model.config.fp8_recipe + megatron_lm_plugin.megatron_lm_default_args["bf16"] = model.config.bf16 + megatron_lm_plugin.megatron_lm_default_args[ + "untie_embeddings_and_output_weights" + ] = not model.config.tie_word_embeddings + logger.info(f"Parsed GLM4 MoE config: {megatron_lm_plugin.megatron_lm_default_args}") + + +@dataclass +class BnbQuantizationConfig: + """ + A plugin to enable BitsAndBytes 4bit and 8bit quantization + + Args: + load_in_8bit (`bool`, defaults to `False`): + Enable 8bit quantization. + llm_int8_threshold (`float`, defaults to `6.0`): + Value of the outliner threshold. Only relevant when `load_in_8bit=True`. + load_in_4bit (`bool`, defaults to `False`): + Enable 4bit quantization. + bnb_4bit_quant_type (`str`, defaults to `fp4`): + Set the quantization data type in the `bnb.nn.Linear4Bit` layers. Options are {'fp4','np4'}. + bnb_4bit_use_double_quant (`bool`, defaults to `False`): + Enable nested quantization where the quantization constants from the first quantization are quantized + again. + bnb_4bit_compute_dtype (`bool`, defaults to `fp16`): + This sets the computational type which might be different than the input time. For example, inputs might be + fp32, but computation can be set to bf16 for speedups. Options are {'fp32','fp16','bf16'}. + torch_dtype (`torch.dtype`, defaults to `None`): + This sets the dtype of the remaining non quantized layers. `bitsandbytes` library suggests to set the value + to `torch.float16` for 8 bit model and use the same dtype as the compute dtype for 4 bit model. + skip_modules (`List[str]`, defaults to `None`): + An explicit list of the modules that we don't quantize. The dtype of these modules will be `torch_dtype`. + keep_in_fp32_modules (`List`, defaults to `None`): + An explicit list of the modules that we don't quantize. We keep them in `torch.float32`. + """ + + load_in_8bit: bool = field(default=False, metadata={"help": "enable 8bit quantization."}) + + llm_int8_threshold: float = field( + default=6.0, + metadata={"help": "value of the outliner threshold. only relevant when load_in_8bit=True"}, + ) + + load_in_4bit: bool = field(default=False, metadata={"help": "enable 4bit quantization."}) + + bnb_4bit_quant_type: str = field( + default="fp4", + metadata={ + "help": "set the quantization data type in the `bnb.nn.Linear4Bit` layers. Options are {'fp4','nf4'}." + }, + ) + + bnb_4bit_use_double_quant: bool = field( + default=False, + metadata={ + "help": "enable nested quantization where the quantization constants from the first quantization are quantized again." + }, + ) + + bnb_4bit_compute_dtype: str = field( + default="fp16", + metadata={ + "help": "This sets the computational type which might be different than the input time. For example, inputs might be " + "fp32, but computation can be set to bf16 for speedups. Options are {'fp32','fp16','bf16'}." + }, + ) + + torch_dtype: torch.dtype = field( + default=None, + metadata={ + "help": "this sets the dtype of the remaining non quantized layers. `bitsandbytes` library suggests to set the value" + "to `torch.float16` for 8 bit model and use the same dtype as the compute dtype for 4 bit model " + }, + ) + + skip_modules: list[str] = field( + default=None, + metadata={ + "help": "an explicit list of the modules that we don't quantize. The dtype of these modules will be `torch_dtype`." + }, + ) + + keep_in_fp32_modules: list[str] = field( + default=None, + metadata={"help": "an explicit list of the modules that we don't quantize. We keep them in `torch.float32`."}, + ) + + def __post_init__(self): + """ + Safety checker that arguments are correct - also replaces some NoneType arguments with their default values. + """ + if not isinstance(self.load_in_8bit, bool): + raise ValueError("load_in_8bit must be a boolean") + + if not isinstance(self.load_in_4bit, bool): + raise ValueError("load_in_4bit must be a boolean") + + if self.load_in_4bit and self.load_in_8bit: + raise ValueError("load_in_4bit and load_in_8bit can't be both True") + + if not self.load_in_4bit and not self.load_in_8bit: + raise ValueError("load_in_4bit and load_in_8bit can't be both False") + + if not isinstance(self.llm_int8_threshold, (int, float)): + raise ValueError("llm_int8_threshold must be a float or an int") + + if not isinstance(self.bnb_4bit_quant_type, str): + raise ValueError("bnb_4bit_quant_type must be a string") + elif self.bnb_4bit_quant_type not in ["fp4", "nf4"]: + raise ValueError(f"bnb_4bit_quant_type must be in ['fp4','nf4'] but found {self.bnb_4bit_quant_type}") + + if not isinstance(self.bnb_4bit_use_double_quant, bool): + raise ValueError("bnb_4bit_use_double_quant must be a boolean") + + if isinstance(self.bnb_4bit_compute_dtype, str): + if self.bnb_4bit_compute_dtype == "fp32": + self.bnb_4bit_compute_dtype = torch.float32 + elif self.bnb_4bit_compute_dtype == "fp16": + self.bnb_4bit_compute_dtype = torch.float16 + elif self.bnb_4bit_compute_dtype == "bf16": + self.bnb_4bit_compute_dtype = torch.bfloat16 + else: + raise ValueError( + f"bnb_4bit_compute_dtype must be in ['fp32','fp16','bf16'] but found {self.bnb_4bit_compute_dtype}" + ) + elif not isinstance(self.bnb_4bit_compute_dtype, torch.dtype): + raise ValueError("bnb_4bit_compute_dtype must be a string or a torch.dtype") + + if self.skip_modules is not None and not isinstance(self.skip_modules, list): + raise ValueError("skip_modules must be a list of strings") + + if self.keep_in_fp32_modules is not None and not isinstance(self.keep_in_fp32_modules, list): + raise ValueError("keep_in_fp_32_modules must be a list of strings") + + if self.load_in_4bit: + self.target_dtype = CustomDtype.INT4 + + if self.load_in_8bit: + self.target_dtype = torch.int8 + + if self.load_in_4bit and self.llm_int8_threshold != 6.0: + warnings.warn("llm_int8_threshold can only be used for model loaded in 8bit") + + if isinstance(self.torch_dtype, str): + if self.torch_dtype == "fp32": + self.torch_dtype = torch.float32 + elif self.torch_dtype == "fp16": + self.torch_dtype = torch.float16 + elif self.torch_dtype == "bf16": + self.torch_dtype = torch.bfloat16 + else: + raise ValueError(f"torch_dtype must be in ['fp32','fp16','bf16'] but found {self.torch_dtype}") + if self.load_in_8bit and self.torch_dtype is None: + self.torch_dtype = torch.float16 + + if self.load_in_4bit and self.torch_dtype is None: + self.torch_dtype = self.bnb_4bit_compute_dtype + + if not isinstance(self.torch_dtype, torch.dtype): + raise ValueError("torch_dtype must be a torch.dtype") + + +def get_module_class_from_name(module, name): + """ + Gets a class from a module by its name. + + Args: + module (`torch.nn.Module`): The module to get the class from. + name (`str`): The name of the class. + """ + modules_children = list(module.children()) + if module.__class__.__name__ == name: + return module.__class__ + elif len(modules_children) == 0: + return + else: + for child_module in modules_children: + module_class = get_module_class_from_name(child_module, name) + if module_class is not None: + return module_class diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/deepspeed.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/deepspeed.py new file mode 100644 index 0000000000000000000000000000000000000000..22db891c63d9bd48691acd87a15a206c270017a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/deepspeed.py @@ -0,0 +1,385 @@ +# Copyright 2021 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import json +import os +from copy import deepcopy + +from torch import optim + +from ..optimizer import AcceleratedOptimizer +from ..scheduler import AcceleratedScheduler +from .dataclasses import DistributedType +from .imports import is_bnb_available +from .versions import compare_versions + + +def map_pytorch_optim_to_deepspeed(optimizer): + """ + Args: + optimizer: torch.optim.Optimizer + + Returns the DeepSeedCPUOptimizer (deepspeed.ops) version of the optimizer. + """ + + defaults = {k: v for k, v in optimizer.defaults.items() if k in ["lr", "weight_decay"]} + + # Select the DeepSpeedCPUOptimizer based on the original optimizer class. + # DeepSpeedCPUAdam is the default + from deepspeed.ops.adam import DeepSpeedCPUAdam + + optimizer_class = DeepSpeedCPUAdam + + # For DeepSpeedCPUAdam (adamw_mode) + if compare_versions("deepspeed", ">=", "0.3.1"): + defaults["adamw_mode"] = False + is_adaw = isinstance(optimizer, optim.AdamW) + + if is_bnb_available() and not is_adaw: + import bitsandbytes.optim as bnb_opt + + if isinstance(optimizer, (bnb_opt.AdamW, bnb_opt.AdamW32bit)): + try: + is_adaw = optimizer.optim_bits == 32 + except AttributeError: + is_adaw = optimizer.args.optim_bits == 32 + else: + is_adaw = False + + if is_adaw: + defaults["adamw_mode"] = True + + # For DeepSpeedCPUAdagrad + if compare_versions("deepspeed", ">=", "0.5.5"): + # Check if the optimizer is PyTorch's Adagrad. + is_ada = isinstance(optimizer, optim.Adagrad) + # If not, and bitsandbytes is available, + # # check if the optimizer is the 32-bit bitsandbytes Adagrad. + if is_bnb_available() and not is_ada: + import bitsandbytes.optim as bnb_opt + + if isinstance(optimizer, (bnb_opt.Adagrad, bnb_opt.Adagrad32bit)): + try: + is_ada = optimizer.optim_bits == 32 + except AttributeError: + is_ada = optimizer.args.optim_bits == 32 + if is_ada: + from deepspeed.ops.adagrad import DeepSpeedCPUAdagrad + + optimizer_class = DeepSpeedCPUAdagrad + + # For DeepSpeedCPULion + if is_bnb_available(min_version="0.38.0") and compare_versions("deepspeed", ">=", "0.11.0"): + from bitsandbytes.optim import Lion, Lion32bit + + if isinstance(optimizer, (Lion, Lion32bit)): + try: + is_bnb_32bits = optimizer.optim_bits == 32 + except AttributeError: + is_bnb_32bits = optimizer.args.optim_bits == 32 + if is_bnb_32bits: + from deepspeed.ops.lion import DeepSpeedCPULion + + optimizer_class = DeepSpeedCPULion + + return optimizer_class(optimizer.param_groups, **defaults) + + +def get_active_deepspeed_plugin(state): + """ + Returns the currently active DeepSpeedPlugin. + + Raises: + ValueError: If DeepSpeed was not enabled and this function is called. + """ + if state.distributed_type != DistributedType.DEEPSPEED: + raise ValueError( + "Couldn't retrieve the active `DeepSpeedPlugin` as none were enabled. " + "Please make sure that either `Accelerator` is configured for `deepspeed` " + "or make sure that the desired `DeepSpeedPlugin` has been enabled (`AcceleratorState().select_deepspeed_plugin(name)`) " + "before calling this function." + ) + if not isinstance(state.deepspeed_plugins, dict): + return state.deepspeed_plugins + return next(plugin for plugin in state.deepspeed_plugins.values() if plugin.selected) + + +class HfDeepSpeedConfig: + """ + This object contains a DeepSpeed configuration dictionary and can be quickly queried for things like zero stage. + + A `weakref` of this object is stored in the module's globals to be able to access the config from areas where + things like the Trainer object is not available (e.g. `from_pretrained` and `_get_resized_embeddings`). Therefore + it's important that this object remains alive while the program is still running. + + [`Trainer`] uses the `HfTrainerDeepSpeedConfig` subclass instead. That subclass has logic to sync the configuration + with values of [`TrainingArguments`] by replacing special placeholder values: `"auto"`. Without this special logic + the DeepSpeed configuration is not modified in any way. + + Args: + config_file_or_dict (`Union[str, Dict]`): path to DeepSpeed config file or dict. + + """ + + def __init__(self, config_file_or_dict): + if isinstance(config_file_or_dict, dict): + # Don't modify user's data should they want to reuse it (e.g. in tests), because once we + # modified it, it will not be accepted here again, since `auto` values would have been overridden + config = deepcopy(config_file_or_dict) + elif os.path.exists(config_file_or_dict): + with open(config_file_or_dict, encoding="utf-8") as f: + config = json.load(f) + else: + try: + try: + # First try parsing as JSON directly + config = json.loads(config_file_or_dict) + except json.JSONDecodeError: + # If that fails, try base64 decoding + config_decoded = base64.urlsafe_b64decode(config_file_or_dict).decode("utf-8") + config = json.loads(config_decoded) + except (UnicodeDecodeError, AttributeError, ValueError): + raise ValueError( + f"Expected a string path to an existing deepspeed config, or a dictionary, or a base64 encoded string. Received: {config_file_or_dict}" + ) + + self.config = config + + self.set_stage_and_offload() + + def set_stage_and_offload(self): + # zero stage - this is done as early as possible, before model is created, to allow + # ``is_deepspeed_zero3_enabled`` query and getting to the early deepspeed config object + # during ``zero.Init()`` which needs to know the dtype, and some other hparams. + self._stage = self.get_value("zero_optimization.stage", -1) + + # offload + self._offload = False + if self.is_zero2() or self.is_zero3(): + offload_devices_valid = set(["cpu", "nvme"]) + offload_devices = set( + [ + self.get_value("zero_optimization.offload_optimizer.device"), + self.get_value("zero_optimization.offload_param.device"), + ] + ) + if len(offload_devices & offload_devices_valid) > 0: + self._offload = True + + def find_config_node(self, ds_key_long): + config = self.config + + # find the config node of interest if it exists + nodes = ds_key_long.split(".") + ds_key = nodes.pop() + for node in nodes: + config = config.get(node) + if config is None: + return None, ds_key + + return config, ds_key + + def get_value(self, ds_key_long, default=None): + """ + Returns the set value or `default` if no value is set + """ + config, ds_key = self.find_config_node(ds_key_long) + if config is None: + return default + return config.get(ds_key, default) + + def del_config_sub_tree(self, ds_key_long, must_exist=False): + """ + Deletes a sub-section of the config file if it's found. + + Unless `must_exist` is `True` the section doesn't have to exist. + """ + config = self.config + + # find the config node of interest if it exists + nodes = ds_key_long.split(".") + for node in nodes: + parent_config = config + config = config.get(node) + if config is None: + if must_exist: + raise ValueError(f"Can't find {ds_key_long} entry in the config: {self.config}") + else: + return + + # if found remove it + if parent_config is not None: + parent_config.pop(node) + + def is_true(self, ds_key_long): + """ + Returns `True`/``False` only if the value is set, always `False` otherwise. So use this method to ask the very + specific question of whether the value is set to `True` (and it's not set to `False`` or isn't set). + + """ + value = self.get_value(ds_key_long) + return False if value is None else bool(value) + + def is_false(self, ds_key_long): + """ + Returns `True`/``False` only if the value is set, always `False` otherwise. So use this method to ask the very + specific question of whether the value is set to `False` (and it's not set to `True`` or isn't set). + """ + value = self.get_value(ds_key_long) + return False if value is None else not bool(value) + + def is_zero2(self): + return self._stage == 2 + + def is_zero3(self): + return self._stage == 3 + + def is_offload(self): + return self._offload + + +class DeepSpeedEngineWrapper: + """ + Internal wrapper for deepspeed.runtime.engine.DeepSpeedEngine. This is used to follow conventional training loop. + + Args: + engine (deepspeed.runtime.engine.DeepSpeedEngine): deepspeed engine to wrap + """ + + def __init__(self, engine): + self.engine = engine + + def backward(self, loss, sync_gradients=True, **kwargs): + # Set gradient accumulation boundary based on Accelerate's sync_gradients state + # This tells DeepSpeed whether this is the final micro-batch before gradient sync + self.engine.set_gradient_accumulation_boundary(is_boundary=sync_gradients) + + # runs backpropagation and handles mixed precision + self.engine.backward(loss, **kwargs) + + # Only perform step and related operations at gradient accumulation boundaries + if sync_gradients: + # Deepspeed's `engine.step` performs the following operations: + # - gradient accumulation check + # - gradient clipping + # - optimizer step + # - zero grad + # - checking overflow + # - lr_scheduler step (only if engine.lr_scheduler is not None) + self.engine.step() + # and this plugin overrides the above calls with no-ops when Accelerate runs under + # Deepspeed, but allows normal functionality for non-Deepspeed cases thus enabling a simple + # training loop that works transparently under many training regimes. + + def get_global_grad_norm(self): + """Get the global gradient norm from DeepSpeed engine.""" + grad_norm = self.engine.get_global_grad_norm() + # Convert to scalar if it's a tensor + if hasattr(grad_norm, "item"): + return grad_norm.item() + return grad_norm + + +class DeepSpeedOptimizerWrapper(AcceleratedOptimizer): + """ + Internal wrapper around a deepspeed optimizer. + + Args: + optimizer (`torch.optim.optimizer.Optimizer`): + The optimizer to wrap. + """ + + def __init__(self, optimizer): + super().__init__(optimizer, device_placement=False, scaler=None) + self.__has_overflow__ = hasattr(self.optimizer, "overflow") + + def zero_grad(self, set_to_none=None): + pass # `accelerator.backward(loss)` is doing that automatically. Therefore, its implementation is not needed + + def step(self): + pass # `accelerator.backward(loss)` is doing that automatically. Therefore, its implementation is not needed + + @property + def step_was_skipped(self): + """Whether or not the optimizer step was done, or skipped because of gradient overflow.""" + if self.__has_overflow__: + return self.optimizer.overflow + return False + + +class DeepSpeedSchedulerWrapper(AcceleratedScheduler): + """ + Internal wrapper around a deepspeed scheduler. + + Args: + scheduler (`torch.optim.lr_scheduler.LambdaLR`): + The scheduler to wrap. + optimizers (one or a list of `torch.optim.Optimizer`): + """ + + def __init__(self, scheduler, optimizers): + super().__init__(scheduler, optimizers) + + def step(self): + pass # `accelerator.backward(loss)` is doing that automatically. Therefore, its implementation is not needed + + +class DummyOptim: + """ + Dummy optimizer presents model parameters or param groups, this is primarily used to follow conventional training + loop when optimizer config is specified in the deepspeed config file. + + Args: + lr (float): + Learning rate. + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + weight_decay (float): + Weight decay. + **kwargs (additional keyword arguments, *optional*): + Other arguments. + """ + + def __init__(self, params, lr=0.001, weight_decay=0, **kwargs): + self.params = params + self.lr = lr + self.weight_decay = weight_decay + self.kwargs = kwargs + + +class DummyScheduler: + """ + Dummy scheduler presents model parameters or param groups, this is primarily used to follow conventional training + loop when scheduler config is specified in the deepspeed config file. + + Args: + optimizer (`torch.optim.optimizer.Optimizer`): + The optimizer to wrap. + total_num_steps (int, *optional*): + Total number of steps. + warmup_num_steps (int, *optional*): + Number of steps for warmup. + lr_scheduler_callable (callable, *optional*): + A callable function that creates an LR Scheduler. It accepts only one argument `optimizer`. + **kwargs (additional keyword arguments, *optional*): + Other arguments. + """ + + def __init__(self, optimizer, total_num_steps=None, warmup_num_steps=0, lr_scheduler_callable=None, **kwargs): + self.optimizer = optimizer + self.total_num_steps = total_num_steps + self.warmup_num_steps = warmup_num_steps + self.lr_scheduler_callable = lr_scheduler_callable + self.kwargs = kwargs diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/environment.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..792ca6e5c832b24bd5ba2039164561a125ea725c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/environment.py @@ -0,0 +1,474 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import math +import os +import platform +import subprocess +import sys +from contextlib import contextmanager +from dataclasses import dataclass, field +from functools import lru_cache, wraps +from shutil import which +from typing import Optional, Union + +import torch +from packaging.version import parse + + +logger = logging.getLogger(__name__) + + +def convert_dict_to_env_variables(current_env: dict): + """ + Verifies that all keys and values in `current_env` do not contain illegal keys or values, and returns a list of + strings as the result. + + Example: + ```python + >>> from accelerate.utils.environment import verify_env + + >>> env = {"ACCELERATE_DEBUG_MODE": "1", "BAD_ENV_NAME": ">> valid_env_items = verify_env(env) + >>> print(valid_env_items) + ["ACCELERATE_DEBUG_MODE=1\n", "OTHER_ENV=2\n"] + ``` + """ + forbidden_chars = [";", "\n", "<", ">", " "] + valid_env_items = [] + for key, value in current_env.items(): + if all(char not in (key + value) for char in forbidden_chars) and len(key) >= 1 and len(value) >= 1: + valid_env_items.append(f"{key}={value}\n") + else: + logger.warning(f"WARNING: Skipping {key}={value} as it contains forbidden characters or missing values.") + return valid_env_items + + +def str_to_bool(value, to_bool: bool = False) -> Union[int, bool]: + """ + Converts a string representation of truth to `True` (1) or `False` (0). + + True values are `y`, `yes`, `t`, `true`, `on`, and `1`; False value are `n`, `no`, `f`, `false`, `off`, and `0`; + """ + value = value.lower() + if value in ("y", "yes", "t", "true", "on", "1"): + return 1 if not to_bool else True + elif value in ("n", "no", "f", "false", "off", "0"): + return 0 if not to_bool else False + else: + raise ValueError(f"invalid truth value {value}") + + +def get_int_from_env(env_keys, default): + """Returns the first positive env value found in the `env_keys` list or the default.""" + for e in env_keys: + val = int(os.environ.get(e, -1)) + if val >= 0: + return val + return default + + +def parse_flag_from_env(key, default=False): + """Returns truthy value for `key` from the env if available else the default.""" + value = os.environ.get(key, str(default)) + return str_to_bool(value) == 1 # As its name indicates `str_to_bool` actually returns an int... + + +def parse_choice_from_env(key, default="no"): + value = os.environ.get(key, str(default)) + return value + + +def are_libraries_initialized(*library_names: str) -> list[str]: + """ + Checks if any of `library_names` are imported in the environment. Will return any names that are. + """ + return [lib_name for lib_name in library_names if lib_name in sys.modules.keys()] + + +def get_current_device_type() -> tuple[str, str]: + """ + Determines the current device type and distributed type without initializing any device. + + This is particularly important when using fork-based multiprocessing, as device initialization + before forking can cause errors. + + The device detection order follows the same priority as state.py:_prepare_backend(): + MLU -> SDAA -> MUSA -> NPU -> HPU -> CUDA -> XPU + + Returns: + tuple[str, str]: A tuple of (device_type, distributed_type) + - device_type: The device string (e.g., "cuda", "npu", "xpu") + - distributed_type: The distributed type string (e.g., "MULTI_GPU", "MULTI_NPU") + + Example: + ```python + >>> device_type, distributed_type = get_current_device_type() + >>> print(device_type) # "cuda" + >>> print(distributed_type) # "MULTI_GPU" + ``` + """ + from .imports import ( + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_xpu_available, + ) + + if is_mlu_available(): + return "mlu", "MULTI_MLU" + elif is_sdaa_available(): + return "sdaa", "MULTI_SDAA" + elif is_musa_available(): + return "musa", "MULTI_MUSA" + elif is_npu_available(): + return "npu", "MULTI_NPU" + elif is_hpu_available(): + return "hpu", "MULTI_HPU" + elif torch.cuda.is_available(): + return "cuda", "MULTI_GPU" + elif is_xpu_available(): + return "xpu", "MULTI_XPU" + elif is_neuron_available(): + return "neuron", "MULTI_NEURON" + else: + # Default to CUDA even if not available (for CPU-only scenarios where CUDA code paths are still used) + return "cuda", "MULTI_GPU" + + +def _nvidia_smi(): + """ + Returns the right nvidia-smi command based on the system. + """ + if platform.system() == "Windows": + # If platform is Windows and nvidia-smi can't be found in path + # try from systemd drive with default installation path + command = which("nvidia-smi") + if command is None: + command = f"{os.environ['systemdrive']}\\Program Files\\NVIDIA Corporation\\NVSMI\\nvidia-smi.exe" + else: + command = "nvidia-smi" + return command + + +def get_gpu_info(): + """ + Gets GPU count and names using `nvidia-smi` instead of torch to not initialize CUDA. + + Largely based on the `gputil` library. + """ + # Returns as list of `n` GPUs and their names + output = subprocess.check_output( + [_nvidia_smi(), "--query-gpu=count,name", "--format=csv,noheader"], universal_newlines=True + ) + output = output.strip() + gpus = output.split(os.linesep) + # Get names from output + gpu_count = len(gpus) + gpu_names = [gpu.split(",")[1].strip() for gpu in gpus] + return gpu_names, gpu_count + + +def get_driver_version(): + """ + Returns the driver version + + In the case of multiple GPUs, will return the first. + """ + output = subprocess.check_output( + [_nvidia_smi(), "--query-gpu=driver_version", "--format=csv,noheader"], universal_newlines=True + ) + output = output.strip() + return output.split(os.linesep)[0] + + +def check_cuda_p2p_ib_support(): + """ + Checks if the devices being used have issues with P2P and IB communications, namely any consumer GPU hardware after + the 3090. + + Notably uses `nvidia-smi` instead of torch to not initialize CUDA. + """ + try: + device_names, device_count = get_gpu_info() + # As new consumer GPUs get released, add them to `unsupported_devices`` + unsupported_devices = {"RTX 40"} + if device_count > 1: + if any( + unsupported_device in device_name + for device_name in device_names + for unsupported_device in unsupported_devices + ): + # Check if they have the right driver version + acceptable_driver_version = "550.40.07" + current_driver_version = get_driver_version() + if parse(current_driver_version) < parse(acceptable_driver_version): + return False + return True + except Exception: + pass + return True + + +@lru_cache +def check_cuda_fp8_capability(): + """ + Checks if the current GPU available supports FP8. + + Notably might initialize `torch.cuda` to check. + """ + + try: + # try to get the compute capability from nvidia-smi + output = subprocess.check_output( + [_nvidia_smi(), "--query-gpu=compute_capability", "--format=csv,noheader"], universal_newlines=True + ) + output = output.strip() + # we take the first GPU's compute capability + compute_capability = tuple(map(int, output.split(os.linesep)[0].split("."))) + except Exception: + compute_capability = torch.cuda.get_device_capability() + + return compute_capability >= (8, 9) + + +@dataclass +class CPUInformation: + """ + Stores information about the CPU in a distributed environment. It contains the following attributes: + - rank: The rank of the current process. + - world_size: The total number of processes in the world. + - local_rank: The rank of the current process on the local node. + - local_world_size: The total number of processes on the local node. + """ + + rank: int = field(default=0, metadata={"help": "The rank of the current process."}) + world_size: int = field(default=1, metadata={"help": "The total number of processes in the world."}) + local_rank: int = field(default=0, metadata={"help": "The rank of the current process on the local node."}) + local_world_size: int = field(default=1, metadata={"help": "The total number of processes on the local node."}) + + +def get_cpu_distributed_information() -> CPUInformation: + """ + Returns various information about the environment in relation to CPU distributed training as a `CPUInformation` + dataclass. + """ + information = {} + information["rank"] = get_int_from_env(["RANK", "PMI_RANK", "OMPI_COMM_WORLD_RANK", "MV2_COMM_WORLD_RANK"], 0) + information["world_size"] = get_int_from_env( + ["WORLD_SIZE", "PMI_SIZE", "OMPI_COMM_WORLD_SIZE", "MV2_COMM_WORLD_SIZE"], 1 + ) + information["local_rank"] = get_int_from_env( + ["LOCAL_RANK", "MPI_LOCALRANKID", "OMPI_COMM_WORLD_LOCAL_RANK", "MV2_COMM_WORLD_LOCAL_RANK"], 0 + ) + information["local_world_size"] = get_int_from_env( + ["LOCAL_WORLD_SIZE", "MPI_LOCALNRANKS", "OMPI_COMM_WORLD_LOCAL_SIZE", "MV2_COMM_WORLD_LOCAL_SIZE"], + 1, + ) + return CPUInformation(**information) + + +def override_numa_affinity(local_process_index: int, verbose: Optional[bool] = None) -> None: + """ + Overrides whatever NUMA affinity is set for the current process. This is very taxing and requires recalculating the + affinity to set, ideally you should use `utils.environment.set_numa_affinity` instead. + + Args: + local_process_index (int): + The index of the current process on the current server. + verbose (bool, *optional*): + Whether to log out the assignment of each CPU. If `ACCELERATE_DEBUG_MODE` is enabled, will default to True. + """ + if verbose is None: + verbose = parse_flag_from_env("ACCELERATE_DEBUG_MODE", False) + if torch.cuda.is_available(): + from accelerate.utils import is_pynvml_available + + if not is_pynvml_available(): + raise ImportError( + "To set CPU affinity on CUDA GPUs the `nvidia-ml-py` package must be available. (`pip install nvidia-ml-py`)" + ) + import pynvml as nvml + + # The below code is based on https://github.com/NVIDIA/DeepLearningExamples/blob/master/TensorFlow2/LanguageModeling/BERT/gpu_affinity.py + nvml.nvmlInit() + num_elements = math.ceil(os.cpu_count() / 64) + handle = nvml.nvmlDeviceGetHandleByIndex(local_process_index) + affinity_string = "" + for j in nvml.nvmlDeviceGetCpuAffinity(handle, num_elements): + # assume nvml returns list of 64 bit ints + affinity_string = f"{j:064b}{affinity_string}" + affinity_list = [int(x) for x in affinity_string] + affinity_list.reverse() # so core 0 is the 0th element + affinity_to_set = [i for i, e in enumerate(affinity_list) if e != 0] + os.sched_setaffinity(0, affinity_to_set) + if verbose: + cpu_cores = os.sched_getaffinity(0) + logger.info(f"Assigning {len(cpu_cores)} cpu cores to process {local_process_index}: {cpu_cores}") + + +@lru_cache +def set_numa_affinity(local_process_index: int, verbose: Optional[bool] = None) -> None: + """ + Assigns the current process to a specific NUMA node. Ideally most efficient when having at least 2 cpus per node. + + This result is cached between calls. If you want to override it, please use + `accelerate.utils.environment.override_numa_afifnity`. + + Args: + local_process_index (int): + The index of the current process on the current server. + verbose (bool, *optional*): + Whether to print the new cpu cores assignment for each process. If `ACCELERATE_DEBUG_MODE` is enabled, will + default to True. + """ + override_numa_affinity(local_process_index=local_process_index, verbose=verbose) + + +@contextmanager +def clear_environment(): + """ + A context manager that will temporarily clear environment variables. + + When this context exits, the previous environment variables will be back. + + Example: + + ```python + >>> import os + >>> from accelerate.utils import clear_environment + + >>> os.environ["FOO"] = "bar" + >>> with clear_environment(): + ... print(os.environ) + ... os.environ["FOO"] = "new_bar" + ... print(os.environ["FOO"]) + {} + new_bar + + >>> print(os.environ["FOO"]) + bar + ``` + """ + _old_os_environ = os.environ.copy() + os.environ.clear() + + try: + yield + finally: + os.environ.clear() # clear any added keys, + os.environ.update(_old_os_environ) # then restore previous environment + + +@contextmanager +def patch_environment(**kwargs): + """ + A context manager that will add each keyword argument passed to `os.environ` and remove them when exiting. + + Will convert the values in `kwargs` to strings and upper-case all the keys. + + Example: + + ```python + >>> import os + >>> from accelerate.utils import patch_environment + + >>> with patch_environment(FOO="bar"): + ... print(os.environ["FOO"]) # prints "bar" + >>> print(os.environ["FOO"]) # raises KeyError + ``` + """ + existing_vars = {} + for key, value in kwargs.items(): + key = key.upper() + if key in os.environ: + existing_vars[key] = os.environ[key] + os.environ[key] = str(value) + + try: + yield + finally: + for key in kwargs: + key = key.upper() + if key in existing_vars: + # restore previous value + os.environ[key] = existing_vars[key] + else: + os.environ.pop(key, None) + + +def purge_accelerate_environment(func_or_cls): + """Decorator to clean up accelerate environment variables set by the decorated class or function. + + In some circumstances, calling certain classes or functions can result in accelerate env vars being set and not + being cleaned up afterwards. As an example, when calling: + + TrainingArguments(fp16=True, ...) + + The following env var will be set: + + ACCELERATE_MIXED_PRECISION=fp16 + + This can affect subsequent code, since the env var takes precedence over TrainingArguments(fp16=False). This is + especially relevant for unit testing, where we want to avoid the individual tests to have side effects on one + another. Decorate the unit test function or whole class with this decorator to ensure that after each test, the env + vars are cleaned up. This works for both unittest.TestCase and normal classes (pytest); it also works when + decorating the parent class. + + """ + prefix = "ACCELERATE_" + + @contextmanager + def env_var_context(): + # Store existing accelerate env vars + existing_vars = {k: v for k, v in os.environ.items() if k.startswith(prefix)} + try: + yield + finally: + # Restore original env vars or remove new ones + for key in [k for k in os.environ if k.startswith(prefix)]: + if key in existing_vars: + os.environ[key] = existing_vars[key] + else: + os.environ.pop(key, None) + + def wrap_function(func): + @wraps(func) + def wrapper(*args, **kwargs): + with env_var_context(): + return func(*args, **kwargs) + + wrapper._accelerate_is_purged_environment_wrapped = True + return wrapper + + if not isinstance(func_or_cls, type): + return wrap_function(func_or_cls) + + # Handle classes by wrapping test methods + def wrap_test_methods(test_class_instance): + for name in dir(test_class_instance): + if name.startswith("test"): + method = getattr(test_class_instance, name) + if callable(method) and not hasattr(method, "_accelerate_is_purged_environment_wrapped"): + setattr(test_class_instance, name, wrap_function(method)) + return test_class_instance + + # Handle inheritance + wrap_test_methods(func_or_cls) + func_or_cls.__init_subclass__ = classmethod(lambda cls, **kw: wrap_test_methods(cls)) + return func_or_cls diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/fsdp_utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/fsdp_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8663f2c5e9e2e3274f7e6c806c999f35bf171900 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/fsdp_utils.py @@ -0,0 +1,857 @@ +# Copyright 2023 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import copy +import functools +import os +import re +import shutil +import warnings +from collections import defaultdict +from collections.abc import Iterable +from contextlib import nullcontext +from pathlib import Path +from typing import Callable, Union + +import torch + +from ..logging import get_logger +from .constants import FSDP_MODEL_NAME, OPTIMIZER_NAME, SAFE_WEIGHTS_NAME, WEIGHTS_NAME +from .dataclasses import get_module_class_from_name +from .modeling import get_non_persistent_buffers, is_peft_model +from .other import get_module_children_bottom_up, is_compiled_module, save +from .versions import is_torch_version + + +logger = get_logger(__name__) + + +def enable_fsdp_ram_efficient_loading(): + """ + Enables RAM efficient loading of Hugging Face models for FSDP in the environment. + """ + # Sets values for `transformers.modeling_utils.is_fsdp_enabled` + if "ACCELERATE_USE_FSDP" not in os.environ: + os.environ["ACCELERATE_USE_FSDP"] = "True" + os.environ["FSDP_CPU_RAM_EFFICIENT_LOADING"] = "True" + + +def disable_fsdp_ram_efficient_loading(): + """ + Disables RAM efficient loading of Hugging Face models for FSDP in the environment. + """ + os.environ["FSDP_CPU_RAM_EFFICIENT_LOADING"] = "False" + + +def _get_model_state_dict(model, adapter_only=False, sd_options=None): + if adapter_only and is_peft_model(model): + from peft import get_peft_model_state_dict + + return get_peft_model_state_dict(model, adapter_name=model.active_adapter) + + # Invariant: `sd_options` is not None only for FSDP2 + if sd_options is not None: + from torch.distributed.checkpoint.state_dict import get_model_state_dict + + return get_model_state_dict(model, options=sd_options) + else: + return model.state_dict() + + +def _set_model_state_dict(model, state_dict, adapter_only=False, sd_options=None): + if adapter_only and is_peft_model(model): + from peft import set_peft_model_state_dict + + return set_peft_model_state_dict(model, state_dict, adapter_name=model.active_adapter) + + # Invariant: `sd_options` is not None only for FSDP2 + if sd_options is not None: + from torch.distributed.checkpoint.state_dict import set_model_state_dict + + return set_model_state_dict(model, state_dict, options=sd_options) + else: + return model.load_state_dict(state_dict) + + +def _prepare_sd_options(fsdp_plugin): + sd_options = None + + # we use this only for FSDP2, as it requires torch >= 2.6.0 and this api requires torch >= 2.2.0 + if fsdp_plugin.fsdp_version == 2: + from torch.distributed.checkpoint.state_dict import StateDictOptions + from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType + + sd_options = StateDictOptions( + full_state_dict=fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT, + cpu_offload=getattr(fsdp_plugin.state_dict_config, "offload_to_cpu", False), + broadcast_from_rank0=getattr(fsdp_plugin.state_dict_config, "rank0_only", False), + ) + + return sd_options + + +def save_fsdp_model(fsdp_plugin, accelerator, model, output_dir, model_index=0, adapter_only=False): + # Note: We import here to reduce import time from general modules, and isolate outside dependencies + import torch.distributed.checkpoint as dist_cp + from torch.distributed.checkpoint.default_planner import DefaultSavePlanner + from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP + from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType + + os.makedirs(output_dir, exist_ok=True) + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + # FSDP raises error when single GPU is used with `offload_to_cpu=True` for FULL_STATE_DICT + # so, only enable it when num_processes>1 + is_multi_process = accelerator.num_processes > 1 + fsdp_plugin.state_dict_config.offload_to_cpu = is_multi_process + fsdp_plugin.state_dict_config.rank0_only = is_multi_process + + ctx = ( + FSDP.state_dict_type( + model, fsdp_plugin.state_dict_type, fsdp_plugin.state_dict_config, fsdp_plugin.optim_state_dict_config + ) + if fsdp_plugin.fsdp_version == 1 + else nullcontext() + ) + sd_options = _prepare_sd_options(fsdp_plugin) + + with ctx: + state_dict = _get_model_state_dict(model, adapter_only=adapter_only, sd_options=sd_options) + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + weights_name = f"{FSDP_MODEL_NAME}.bin" if model_index == 0 else f"{FSDP_MODEL_NAME}_{model_index}.bin" + output_model_file = os.path.join(output_dir, weights_name) + if accelerator.process_index == 0: + logger.info(f"Saving model to {output_model_file}") + torch.save(state_dict, output_model_file) + logger.info(f"Model saved to {output_model_file}") + # Invariant: `LOCAL_STATE_DICT` is never possible with `FSDP2` + elif fsdp_plugin.state_dict_type == StateDictType.LOCAL_STATE_DICT: + weights_name = ( + f"{FSDP_MODEL_NAME}_rank{accelerator.process_index}.bin" + if model_index == 0 + else f"{FSDP_MODEL_NAME}_{model_index}_rank{accelerator.process_index}.bin" + ) + output_model_file = os.path.join(output_dir, weights_name) + logger.info(f"Saving model to {output_model_file}") + torch.save(state_dict, output_model_file) + logger.info(f"Model saved to {output_model_file}") + elif fsdp_plugin.state_dict_type == StateDictType.SHARDED_STATE_DICT: + ckpt_dir = os.path.join(output_dir, f"{FSDP_MODEL_NAME}_{model_index}") + os.makedirs(ckpt_dir, exist_ok=True) + logger.info(f"Saving model to {ckpt_dir}") + state_dict = {"model": state_dict} + + dist_cp.save( + state_dict=state_dict, + storage_writer=dist_cp.FileSystemWriter(ckpt_dir), + planner=DefaultSavePlanner(), + ) + logger.info(f"Model saved to {ckpt_dir}") + + +def load_fsdp_model(fsdp_plugin, accelerator, model, input_dir, model_index=0, adapter_only=False): + # Note: We import here to reduce import time from general modules, and isolate outside dependencies + import torch.distributed.checkpoint as dist_cp + from torch.distributed.checkpoint.default_planner import DefaultLoadPlanner + from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP + from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType + + accelerator.wait_for_everyone() + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + # FSDP raises error when single GPU is used with `offload_to_cpu=True` for FULL_STATE_DICT + # so, only enable it when num_processes>1 + is_multi_process = accelerator.num_processes > 1 + fsdp_plugin.state_dict_config.offload_to_cpu = is_multi_process + fsdp_plugin.state_dict_config.rank0_only = is_multi_process + + ctx = ( + FSDP.state_dict_type( + model, fsdp_plugin.state_dict_type, fsdp_plugin.state_dict_config, fsdp_plugin.optim_state_dict_config + ) + if fsdp_plugin.fsdp_version == 1 + else nullcontext() + ) + sd_options = _prepare_sd_options(fsdp_plugin) + with ctx: + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + if type(model) is not FSDP and accelerator.process_index != 0 and not accelerator.is_fsdp2: + if not fsdp_plugin.sync_module_states and fsdp_plugin.fsdp_version == 1: + raise ValueError( + "Set the `sync_module_states` flag to `True` so that model states are synced across processes when " + "initializing FSDP object" + ) + return + weights_name = f"{FSDP_MODEL_NAME}.bin" if model_index == 0 else f"{FSDP_MODEL_NAME}_{model_index}.bin" + input_model_file = os.path.join(input_dir, weights_name) + logger.info(f"Loading model from {input_model_file}") + # we want an empty state dict for FSDP2 as we use `broadcast_from_rank0` + load_model = not accelerator.is_fsdp2 or accelerator.is_main_process + if load_model: + state_dict = torch.load(input_model_file, weights_only=True) + else: + state_dict = {} + logger.info(f"Model loaded from {input_model_file}") + elif fsdp_plugin.state_dict_type == StateDictType.LOCAL_STATE_DICT: + weights_name = ( + f"{FSDP_MODEL_NAME}_rank{accelerator.process_index}.bin" + if model_index == 0 + else f"{FSDP_MODEL_NAME}_{model_index}_rank{accelerator.process_index}.bin" + ) + input_model_file = os.path.join(input_dir, weights_name) + logger.info(f"Loading model from {input_model_file}") + state_dict = torch.load(input_model_file, weights_only=True) + logger.info(f"Model loaded from {input_model_file}") + elif fsdp_plugin.state_dict_type == StateDictType.SHARDED_STATE_DICT: + ckpt_dir = ( + os.path.join(input_dir, f"{FSDP_MODEL_NAME}_{model_index}") + if f"{FSDP_MODEL_NAME}" not in input_dir + else input_dir + ) + logger.info(f"Loading model from {ckpt_dir}") + state_dict = {"model": _get_model_state_dict(model, adapter_only=adapter_only, sd_options=sd_options)} + dist_cp.load( + state_dict=state_dict, + storage_reader=dist_cp.FileSystemReader(ckpt_dir), + planner=DefaultLoadPlanner(), + ) + state_dict = state_dict["model"] + logger.info(f"Model loaded from {ckpt_dir}") + + load_result = _set_model_state_dict(model, state_dict, adapter_only=adapter_only, sd_options=sd_options) + return load_result + + +def save_fsdp_optimizer(fsdp_plugin, accelerator, optimizer, model, output_dir, optimizer_index=0): + # Note: We import here to reduce import time from general modules, and isolate outside dependencies + import torch.distributed.checkpoint as dist_cp + from torch.distributed.checkpoint.default_planner import DefaultSavePlanner + from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP + from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType + + os.makedirs(output_dir, exist_ok=True) + + ctx = ( + FSDP.state_dict_type( + model, fsdp_plugin.state_dict_type, fsdp_plugin.state_dict_config, fsdp_plugin.optim_state_dict_config + ) + if fsdp_plugin.fsdp_version == 1 + else nullcontext() + ) + + sd_options = _prepare_sd_options(fsdp_plugin) + + with ctx: + if fsdp_plugin.fsdp_version == 2: + from torch.distributed.checkpoint.state_dict import get_optimizer_state_dict + + optim_state = get_optimizer_state_dict(model, optimizer, options=sd_options) + else: + optim_state = FSDP.optim_state_dict(model, optimizer) + + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + if accelerator.process_index == 0: + optim_state_name = ( + f"{OPTIMIZER_NAME}.bin" if optimizer_index == 0 else f"{OPTIMIZER_NAME}_{optimizer_index}.bin" + ) + output_optimizer_file = os.path.join(output_dir, optim_state_name) + logger.info(f"Saving Optimizer state to {output_optimizer_file}") + torch.save(optim_state, output_optimizer_file) + logger.info(f"Optimizer state saved in {output_optimizer_file}") + else: + ckpt_dir = os.path.join(output_dir, f"{OPTIMIZER_NAME}_{optimizer_index}") + os.makedirs(ckpt_dir, exist_ok=True) + logger.info(f"Saving Optimizer state to {ckpt_dir}") + dist_cp.save( + state_dict={"optimizer": optim_state}, + storage_writer=dist_cp.FileSystemWriter(ckpt_dir), + planner=DefaultSavePlanner(), + ) + logger.info(f"Optimizer state saved in {ckpt_dir}") + + +def load_fsdp_optimizer(fsdp_plugin, accelerator, optimizer, model, input_dir, optimizer_index=0, adapter_only=False): + # Note: We import here to reduce import time from general modules, and isolate outside dependencies + import torch.distributed.checkpoint as dist_cp + from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP + from torch.distributed.fsdp.fully_sharded_data_parallel import StateDictType + + accelerator.wait_for_everyone() + ctx = ( + FSDP.state_dict_type( + model, fsdp_plugin.state_dict_type, fsdp_plugin.state_dict_config, fsdp_plugin.optim_state_dict_config + ) + if fsdp_plugin.fsdp_version == 1 + else nullcontext() + ) + sd_options = _prepare_sd_options(fsdp_plugin) + with ctx: + if fsdp_plugin.state_dict_type == StateDictType.FULL_STATE_DICT: + optim_state = None + if accelerator.process_index == 0 or not fsdp_plugin.optim_state_dict_config.rank0_only: + optimizer_name = ( + f"{OPTIMIZER_NAME}.bin" if optimizer_index == 0 else f"{OPTIMIZER_NAME}_{optimizer_index}.bin" + ) + input_optimizer_file = os.path.join(input_dir, optimizer_name) + logger.info(f"Loading Optimizer state from {input_optimizer_file}") + optim_state = torch.load(input_optimizer_file, weights_only=True) + logger.info(f"Optimizer state loaded from {input_optimizer_file}") + else: + ckpt_dir = ( + os.path.join(input_dir, f"{OPTIMIZER_NAME}_{optimizer_index}") + if f"{OPTIMIZER_NAME}" not in input_dir + else input_dir + ) + logger.info(f"Loading Optimizer from {ckpt_dir}") + if fsdp_plugin.fsdp_version == 2: + from torch.distributed.checkpoint.state_dict import get_optimizer_state_dict + + optim_state = get_optimizer_state_dict(model, optimizer, options=sd_options) + else: + optim_state = FSDP.optim_state_dict(model, optimizer) + optim_state = {"optimizer": optim_state} + dist_cp.load( + optim_state, + checkpoint_id=ckpt_dir, + storage_reader=dist_cp.FileSystemReader(ckpt_dir), + ) + optim_state = optim_state["optimizer"] + logger.info(f"Optimizer loaded from {ckpt_dir}") + + if fsdp_plugin.fsdp_version == 1: + flattened_osd = FSDP.optim_state_dict_to_load(model=model, optim=optimizer, optim_state_dict=optim_state) + optimizer.load_state_dict(flattened_osd) + else: + from torch.distributed.checkpoint.state_dict import set_optimizer_state_dict + + set_optimizer_state_dict(model, optimizer, optim_state, options=sd_options) + + +def _distributed_checkpoint_to_merged_weights(checkpoint_dir: str, save_path: str, safe_serialization: bool = True): + """ + Passthrough to `torch.distributed.checkpoint.format_utils.dcp_to_torch_save` + + Will save under `save_path` as either `model.safetensors` or `pytorch_model.bin`. + """ + # Note: We import here to reduce import time from general modules, and isolate outside dependencies + import torch.distributed.checkpoint as dist_cp + import torch.distributed.checkpoint.format_utils as dist_cp_format_utils + + state_dict = {} + save_path = Path(save_path) + save_path.mkdir(exist_ok=True) + dist_cp_format_utils._load_state_dict( + state_dict, + storage_reader=dist_cp.FileSystemReader(checkpoint_dir), + planner=dist_cp_format_utils._EmptyStateDictLoadPlanner(), + no_dist=True, + ) + save_path = save_path / SAFE_WEIGHTS_NAME if safe_serialization else save_path / WEIGHTS_NAME + + # To handle if state is a dict like {model: {...}} + if len(state_dict.keys()) == 1: + state_dict = state_dict[list(state_dict)[0]] + save(state_dict, save_path, safe_serialization=safe_serialization) + return save_path + + +def merge_fsdp_weights( + checkpoint_dir: str, output_path: str, safe_serialization: bool = True, remove_checkpoint_dir: bool = False +): + """ + Merge the weights from sharded FSDP model checkpoints into a single combined checkpoint. Should be used if + `SHARDED_STATE_DICT` was used for the model. Weights will be saved to `{output_path}/model.safetensors` if + `safe_serialization` else `pytorch_model.bin`. + + Note: this is a CPU-bound process. + + Args: + checkpoint_dir (`str`): + The directory containing the FSDP checkpoints (can be either the model or optimizer). + output_path (`str`): + The path to save the merged checkpoint. + safe_serialization (`bool`, *optional*, defaults to `True`): + Whether to save the merged weights with safetensors (recommended). + remove_checkpoint_dir (`bool`, *optional*, defaults to `False`): + Whether to remove the checkpoint directory after merging. + """ + checkpoint_dir = Path(checkpoint_dir) + from accelerate.state import PartialState + + if not is_torch_version(">=", "2.3.0"): + raise ValueError("`merge_fsdp_weights` requires PyTorch >= 2.3.0`") + + # Verify that the checkpoint directory exists + if not checkpoint_dir.exists(): + model_path_exists = (checkpoint_dir / "pytorch_model_fsdp_0").exists() + optimizer_path_exists = (checkpoint_dir / "optimizer_0").exists() + err = f"Tried to load from {checkpoint_dir} but couldn't find a valid metadata file." + if model_path_exists and optimizer_path_exists: + err += " However, potential model and optimizer checkpoint directories exist." + err += f"Please pass in either {checkpoint_dir}/pytorch_model_fsdp_0 or {checkpoint_dir}/optimizer_0" + err += "instead." + elif model_path_exists: + err += " However, a potential model checkpoint directory exists." + err += f"Please try passing in {checkpoint_dir}/pytorch_model_fsdp_0 instead." + elif optimizer_path_exists: + err += " However, a potential optimizer checkpoint directory exists." + err += f"Please try passing in {checkpoint_dir}/optimizer_0 instead." + raise ValueError(err) + + # To setup `save` to work + state = PartialState() + if state.is_main_process: + logger.info(f"Merging FSDP weights from {checkpoint_dir}") + save_path = _distributed_checkpoint_to_merged_weights(checkpoint_dir, output_path, safe_serialization) + logger.info(f"Successfully merged FSDP weights and saved to {save_path}") + if remove_checkpoint_dir: + logger.info(f"Removing old checkpoint directory {checkpoint_dir}") + shutil.rmtree(checkpoint_dir) + state.wait_for_everyone() + + +def ensure_weights_retied(param_init_fn, model: torch.nn.Module, device: torch.device): + _tied_names = getattr(model, "_tied_weights_keys", None) + if not _tied_names: + # if no tied names just passthrough + return param_init_fn + + # get map of parameter instances to params. + # - needed for replacement later + _tied_params = {} + for name in _tied_names: + name = name.split(".") + name, param_name = ".".join(name[:-1]), name[-1] + mod = model.get_submodule(name) + param = getattr(mod, param_name) + + _tied_params[id(param)] = None # placeholder for the param first + + # build param_init_fn for the case with tied params + def param_init_fn_tied_param(module: torch.nn.Module): + # track which params to tie + # - usually only 1, but for completeness consider > 1 + params_to_tie = defaultdict(list) + for n, param in module.named_parameters(recurse=False): + if id(param) in _tied_params: + params_to_tie[id(param)].append(n) + + # call the param init fn, which potentially re-allocates the + # parameters + module = param_init_fn(module) + + # search the parameters again and tie them up again + for id_key, _param_names in params_to_tie.items(): + for param_name in _param_names: + param = _tied_params[id_key] + if param is None: + # everything will be tied to the first time the + # param is observed + _tied_params[id_key] = getattr(module, param_name) + else: + setattr(module, param_name, param) # tie + + return module + + return param_init_fn_tied_param + + +def fsdp2_load_full_state_dict(accelerator, model: torch.nn.Module, full_sd: dict, cpu_offload: bool = False): + """ + Loads the full state dict (could be only on rank 0) into the sharded model. This is done by broadcasting the + parameters from rank 0 to all other ranks. This function modifies the model in-place. + + Args: + accelerator (`Accelerator`): The accelerator instance + model (`torch.nn.Module`): + The model to load the state dict into, expected to be on meta device or a VRAM spike can occur + full_sd (`dict`): The full state dict to load, can only be on rank 0 + cpu_offload (`bool`, defaults to `False`): + If True, move sharded parameters to CPU after distribution. Required when FSDP CPU offloading is enabled. + """ + import torch.distributed as dist + from torch.distributed.tensor import DTensor, distribute_tensor + + # Model was previously copied to meta device + meta_sharded_sd = model.state_dict() + sharded_sd = {} + + # Rank 0 distributes the full state dict to other ranks + def _infer_parameter_dtype(model, param_name, empty_param): + try: + old_param = model.get_parameter_or_buffer(param_name) + except AttributeError: + # Need this for LORA, as there some params are not *parameters* of sorts + base_param_name, local_param_name = param_name.rsplit(".", 1) + submodule = model.get_submodule(base_param_name) + old_param = getattr(submodule, local_param_name) + + is_torch_e4m3fn_available = hasattr(torch, "float8_e4m3fn") + casting_dtype = None + is_param_float8_e4m3fn = is_torch_e4m3fn_available and empty_param.dtype == torch.float8_e4m3fn + + if empty_param.dtype.is_floating_point and not is_param_float8_e4m3fn: + casting_dtype = old_param.dtype + + return old_param is not None and old_param.is_contiguous(), casting_dtype + + def _cast_and_contiguous(tensor, to_contiguous, dtype): + if dtype is not None: + tensor = tensor.to(dtype=dtype) + if to_contiguous: + tensor = tensor.contiguous() + return tensor + + if accelerator.is_main_process: + for (param_name, full_param), sharded_param in zip(full_sd.items(), meta_sharded_sd.values()): + device_mesh = sharded_param.device_mesh + full_param = full_param.detach().to(device_mesh.device_type) + if isinstance(full_param, DTensor): + # dist.broadcast() only supports torch.Tensor. + # After prepare_tp(), model parameters may become DTensor. + # To broadcast such a parameter, convert it to a local tensor first. + full_param = full_param.to_local() + dist.broadcast(full_param, src=0, group=dist.group.WORLD) + sharded_tensor = distribute_tensor(full_param, device_mesh, sharded_param.placements) + to_contiguous, casting_dtype = _infer_parameter_dtype( + model, + param_name, + full_param, + ) + sharded_tensor = _cast_and_contiguous(sharded_tensor, to_contiguous, casting_dtype) + # When CPU offloading is enabled, FSDP2's lazy_init expects parameters on CPU + if cpu_offload: + sharded_tensor = sharded_tensor.to("cpu") + sharded_sd[param_name] = sharded_tensor + # We need this else to have a matching `broadcast` for all of the ranks, else we deadlock + else: + for param_name, sharded_param in meta_sharded_sd.items(): + device_mesh = sharded_param.device_mesh + full_tensor = torch.empty(sharded_param.size(), device=device_mesh.device_type, dtype=sharded_param.dtype) + dist.broadcast(full_tensor, src=0, group=dist.group.WORLD) + sharded_tensor = distribute_tensor(full_tensor, device_mesh, sharded_param.placements) + to_contiguous, casting_dtype = _infer_parameter_dtype( + model, + param_name, + full_tensor, + ) + sharded_tensor = _cast_and_contiguous(sharded_tensor, to_contiguous, casting_dtype) + # When CPU offloading is enabled, FSDP2's lazy_init expects parameters on CPU + if cpu_offload: + sharded_tensor = sharded_tensor.to("cpu") + sharded_sd[param_name] = sharded_tensor + + # we set `assign=True` because our params are on meta device + model.load_state_dict(sharded_sd, assign=True) + return model + + +def fsdp2_switch_optimizer_parameters(optimizer: torch.optim.Optimizer, mapping: dict): + """ + Switches the parameters of the optimizer to new ones (sharded parameters in usual case). This function modifies the + optimizer in-place. + + Args: + optimizer (`torch.optim.Optimizer`): Optimizer instance which contains the original model parameters + mapping (`dict`): Mapping from the original parameter (specified by `data_ptr`) to the sharded parameter + + Raises: + KeyError: + If a parameter in the optimizer couldn't be switched to its sharded version. This should never happen and + indicates a bug. If we kept the original params instead of raising, the training wouldn't be numerically + correct and weights wouldn't get updated. + """ + from torch.distributed.tensor import DTensor + + accessor_mapping = {} + + accessor_mapping[DTensor] = "_local_tensor" + try: + for param_group in optimizer.param_groups: + param_group["params"] = [mapping[p.data_ptr] for p in param_group["params"]] + except KeyError: + # This shouldn't ever happen, but we want to fail here else training wouldn't be numerically correct + # This basically means that we're missing a mapping from the original parameter to the sharded parameter + raise KeyError( + "A parameter in the optimizer couldn't be switched to its sharded version. This breaks the training. Please raise an issue on GitHub." + ) + + +def fsdp2_apply_ac(accelerator, model: torch.nn.Module): + """ + Applies the activation checkpointing to the model. + + Args: + accelerator (`Accelerator`): The accelerator instance + model (`torch.nn.Module`): The model to apply the activation checkpointing to + + Returns: + `torch.nn.Module`: The model with the activation checkpointing applied + """ + + from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( + checkpoint_wrapper, + ) + + auto_wrap_policy_func = fsdp2_prepare_auto_wrap_policy(accelerator.state.fsdp_plugin, model) + + for layer_name, layer in get_module_children_bottom_up(model, return_fqns=True)[:-1]: + if len(layer_name.split(".")) > 1: + parent_name, child_name = layer_name.rsplit(".", 1) + else: + parent_name = None + child_name = layer_name + + parent_module = model.get_submodule(parent_name) if parent_name else model + if auto_wrap_policy_func(parent_module): + layer = checkpoint_wrapper(layer, preserve_rng_state=False) + parent_module.register_module(child_name, layer) + + return model + + +def fsdp2_prepare_model(accelerator, model: torch.nn.Module) -> torch.nn.Module: + """Prepares the model for FSDP2 in-place. Also returns the model to avoid misuse of the original model. + + Args: + accelerator (`Accelerator`): The accelerator instance + model (`torch.nn.Module`): The model to prepare + + Returns: + `torch.nn.Module`: Prepared model + """ + from torch.distributed.fsdp import FSDPModule, MixedPrecisionPolicy, fully_shard + + is_type_fsdp = isinstance(model, FSDPModule) or ( + is_compiled_module(model) and isinstance(model._orig_mod, FSDPModule) + ) + if is_type_fsdp: + return model + + fsdp2_plugin = accelerator.state.fsdp_plugin + + fsdp2_plugin.set_auto_wrap_policy(model) + + original_sd = model.state_dict() + mesh = getattr(accelerator, "torch_device_mesh", None) + + fsdp2_kwargs = { + "reshard_after_forward": fsdp2_plugin.reshard_after_forward, + "offload_policy": fsdp2_plugin.cpu_offload, + # `fully_shard` does not accept `None` in case of `MixedPrecisionPolicy` + "mp_policy": fsdp2_plugin.mixed_precision_policy or MixedPrecisionPolicy(), + "mesh": mesh[tuple(accelerator.parallelism_config.fsdp_dim_names)] if mesh is not None else None, + } + + # `ignored_params` is only supported in torch >= 2.7.0 + if is_torch_version(">=", "2.7.0") and fsdp2_plugin.ignored_modules is not None: + fsdp2_kwargs["ignored_params"] = get_parameters_from_modules( + fsdp2_plugin.ignored_modules, model, accelerator.device + ) + + model_has_params4bit = False + for name, param in model.named_parameters(): + # this is a temporary fix whereby loading models with bnb params cannot be moved from + # GPU to a meta device due with FSDP2 because torch operations don't return the original class type + # bypassing the move to meta will still cause the VRAM spike, but at least it still will load + if param.__class__.__name__ == "Params4bit": + model_has_params4bit = True + break + + if fsdp2_plugin.cpu_ram_efficient_loading and not model_has_params4bit: + # Context: `fully_shard` moves the model to GPU if it was on CPU, however it can also be on `meta` and then it stays there even after `fully_shard` + # For this reason, we need to move the model to `meta` device, as then sharding happens on `meta` device + # If we kept the model on CPU (`cpu_ram_efficient_loading` has model be on CPU on all ranks, though non-main ranks only have `torch.empty`), `fully_shard` would move it to GPU + # Afterwards, when we call `fsdp2_load_full_state_dict`, us creating the state_dict would result into briefly having two copies of model state_dict on the GPU -> VRAM spike + + # We need to keep the original non-persistent buffers, as those MAY not be in the state_dict, resulting in them staying on meta device + # Also, these buffers aren't getting sharded by default + # We get the FQNs of all non-persistent buffers, to re-register them after + non_persistent_buffer_fqns = get_non_persistent_buffers(model, recurse=True, fqns=True) + original_non_persistent_buffers = copy.deepcopy( + {k: v for k, v in model.named_buffers() if k in non_persistent_buffer_fqns} + ) + # We move the model to meta device, as then sharding happens on meta device + model = model.to(torch.device("meta")) + # We need to re-tie the weights, not exactly sure why, but if we don't do this, reference to `lm_head/embed_tokens` stay hanging -> more VRAM usage + # We assume `transformers` models have a `tie_weights` method if they support it + if hasattr(model, "tie_weights"): + model.tie_weights() + + auto_wrap_policy_func = fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model) + if auto_wrap_policy_func is not None: + # We skip the model itself, as that one is always wrapped + for module in get_module_children_bottom_up(model)[:-1]: + if auto_wrap_policy_func(module) and not isinstance(module, FSDPModule): + fully_shard(module, **fsdp2_kwargs) + + if not isinstance(model, FSDPModule): + fully_shard(model, **fsdp2_kwargs) + + if fsdp2_plugin.cpu_ram_efficient_loading: + # If `cpu_ram_efficient_loading` is enabled, only rank 0 loads the weights + # Other ranks have an empty model on `meta` device, so we need to distribute the weights properly + # When CPU offloading is enabled, parameters need to stay on CPU after distribution + from torch.distributed.fsdp import CPUOffloadPolicy + + fsdp2_load_full_state_dict( + accelerator, model, original_sd, cpu_offload=isinstance(fsdp2_plugin.cpu_offload, CPUOffloadPolicy) + ) + + if fsdp2_plugin.cpu_ram_efficient_loading and not model_has_params4bit: + # We re-register the buffers, as they may not be in the state_dict + for fqn, buffer_tensor in original_non_persistent_buffers.items(): + buffer_tensor = buffer_tensor.to(accelerator.device) + + if "." in fqn: + parent_fqn, local_buffer_name = fqn.rsplit(".", 1) + parent_module = model.get_submodule(parent_fqn) + else: + local_buffer_name = fqn + parent_module = model + + parent_module.register_buffer(local_buffer_name, buffer_tensor, persistent=False) + + # We need to tie the weights again, as call to `load_full_state_dict` breaks the tie + # Needs to be called both here and above + # removing this call makes the have slightly different loss + # removing the call above leads to extra memory usage as explained in the comment above + if hasattr(model, "tie_weights"): + model.tie_weights() + + # There is no `dtype` attribution for nn.Module + # Set it to None if it doesn't exist and do the upcast always + model_dtype = getattr(model, "dtype", None) + if accelerator.mixed_precision != "no" and (model_dtype is None or model_dtype != torch.float32): + # We upcast the trainable parameters according to `deepspeed`'s implementation + # More info about this can be found in `accelerator.py:prepare_model`s FSDP1 section + upcasted_params = [] + for name, param in model.named_parameters(): + if param.requires_grad and param.dtype != torch.float32: + upcasted_params.append(name) + param = param.to(torch.float32) + if accelerator.is_main_process and upcasted_params: + warnings.warn( + "FSDP upcast of low precision parameters to fp32 (since mixed_precision != 'no') may affect the precision of model checkpoints. " + f"This effects {len(upcasted_params)} parameters: {upcasted_params}..." + ) + return model + + +def fsdp2_prepare_auto_wrap_policy(fsdp2_plugin, model: torch.nn.Module) -> Callable[[torch.nn.Module], bool]: + """Prepares the auto wrap policy based on its type, done to mimic the behaviour of FSDP1 auto wrap policy. + + Args: + fsdp2_plugin (`FullyShardedDataParallelPlugin`): + Instance of `FullyShardedDataParallelPlugin` containing the configuration options + auto_wrap_policy_type (`str`): + Either `transformer` or `size` + model (`torch.nn.Module`): + The model to wrap + + Returns: + `Callable[[torch.nn.Module], bool]`: + The auto wrap policy function to be applied to the model + """ + from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy, transformer_auto_wrap_policy + + fn = fsdp2_plugin.auto_wrap_policy + + if isinstance(fn, functools.partial): + fn = fn.func + + if fn is transformer_auto_wrap_policy: + no_split_modules = getattr(model, "_no_split_modules", None) + if no_split_modules is None: + no_split_modules = [] + transformer_cls_names_to_wrap = list(no_split_modules) + if fsdp2_plugin.transformer_cls_names_to_wrap is not None: + transformer_cls_names_to_wrap = fsdp2_plugin.transformer_cls_names_to_wrap + transformer_cls_to_wrap = set() + + for layer_class in transformer_cls_names_to_wrap: + transformer_cls = get_module_class_from_name(model, layer_class) + if transformer_cls is None: + raise ValueError(f"Could not find the transformer layer class {layer_class} in the model.") + transformer_cls_to_wrap.add(transformer_cls) + + def policy(module: torch.nn.Module) -> bool: + if fsdp2_plugin.transformer_cls_names_to_wrap is None: + return False + return isinstance(module, tuple(transformer_cls_to_wrap)) + + elif fn is size_based_auto_wrap_policy: + + def policy(module: torch.nn.Module) -> bool: + module_num_params = sum(p.numel() for p in module.parameters()) + return module_num_params > fsdp2_plugin.min_num_params + else: + return None + + return policy + + +def get_fsdp2_grad_scaler(**kwargs): + """ + Returns a `GradScaler` for FSDP2, as the current implementation of `get_grad_scaler` doesn't accept other args. We + need this as current `get_grad_scaler` accepts only `distributed_type` as arg, which doesn't differentiate between + FSDP1 and FSDP2 + """ + from torch.amp.grad_scaler import GradScaler + + return GradScaler(**kwargs) + + +def fsdp2_canonicalize_names(named_params: dict) -> dict: + """Removes parameter name modifiers in order to map them back to their original names. + + See huggingface/accelerate#3554 for more context. + + Args: + named_params (`dict`): The named parameters dictionary to canonicalize. + + Returns: + `dict`: The canonicalized named parameters dictionary + """ + named_params = {k.replace("._checkpoint_wrapped_module", ""): v for k, v in named_params.items()} + named_params = { + k.replace("_orig_mod.", "") if k.startswith("_orig_mod.") else k: v for k, v in named_params.items() + } + named_params = {k.replace("._orig_mod", ""): v for k, v in named_params.items()} + return named_params + + +def get_parameters_from_modules( + modules: Union[Iterable[torch.nn.Module], str], model, device +) -> set[torch.nn.Parameter]: + """Converts modules to parameters where modules can be a string or list of torch.nn.Module + + Args: + modules (`Union[Iterable[torch.nn.Module], str]`): List of modules + + Returns: + `set[torch.nn.Parameter]`: List of parameters + """ + if modules is None: + return set() + parameters = [] + # code taken from accelerate while preparing kwargs for FSDP + if isinstance(modules, str): + reg = re.compile(modules) + mapped_modules = [] + for name, module in model.named_modules(): + if reg.fullmatch(name): + module.to(device) + mapped_modules.append(module) + modules = mapped_modules + for module in modules: + parameters.extend(list(module.parameters())) + return set(parameters) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/imports.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/imports.py new file mode 100644 index 0000000000000000000000000000000000000000..0704088d2096902c952867add39892c32b1f661e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/imports.py @@ -0,0 +1,536 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib +import importlib.metadata +import os +import sys +import warnings +from functools import lru_cache, wraps + +import torch +from packaging import version +from packaging.version import parse + +from .environment import parse_flag_from_env, patch_environment, str_to_bool +from .versions import compare_versions, is_torch_version + + +# Try to run Torch native job in an environment with TorchXLA installed by setting this value to 0. +USE_TORCH_XLA = parse_flag_from_env("USE_TORCH_XLA", default=True) + +_torch_xla_available = False +if USE_TORCH_XLA: + try: + import torch_xla.core.xla_model as xm # noqa: F401 + import torch_xla.runtime + + _torch_xla_available = True + except ImportError: + pass + +# Keep it for is_tpu_available. It will be removed along with is_tpu_available. +_tpu_available = _torch_xla_available + +# Cache this result has it's a C FFI call which can be pretty time-consuming +_torch_distributed_available = torch.distributed.is_available() + + +def _is_package_available(pkg_name, metadata_name=None): + # Check we're not importing a "pkg_name" directory somewhere but the actual library by trying to grab the version + package_exists = importlib.util.find_spec(pkg_name) is not None + if package_exists: + try: + # Some libraries have different names in the metadata + _ = importlib.metadata.metadata(pkg_name if metadata_name is None else metadata_name) + return True + except importlib.metadata.PackageNotFoundError: + return False + + +def is_torch_distributed_available() -> bool: + return _torch_distributed_available + + +def is_xccl_available(): + if is_torch_version(">=", "2.7.0"): + return torch.distributed.distributed_c10d.is_xccl_available() + return False + + +def is_import_timer_available(): + return _is_package_available("import_timer") + + +def is_pynvml_available(): + return _is_package_available("pynvml") or _is_package_available("pynvml", "nvidia-ml-py") + + +def is_pytest_available(): + return _is_package_available("pytest") + + +def is_msamp_available(): + return _is_package_available("msamp", "ms-amp") + + +def is_schedulefree_available(): + return _is_package_available("schedulefree") + + +def is_transformer_engine_available(): + if is_hpu_available(): + return _is_package_available("intel_transformer_engine", "intel-transformer-engine") + else: + return _is_package_available("transformer_engine", "transformer-engine") + + +def is_transformer_engine_mxfp8_available(): + if _is_package_available("transformer_engine", "transformer-engine"): + from transformer_engine.pytorch.fp8 import check_mxfp8_support + + return check_mxfp8_support()[0] + return False + + +def is_lomo_available(): + return _is_package_available("lomo_optim") + + +def is_cuda_available(): + """ + Checks if `cuda` is available via an `nvml-based` check which won't trigger the drivers and leave cuda + uninitialized. + """ + with patch_environment(PYTORCH_NVML_BASED_CUDA_CHECK="1"): + available = torch.cuda.is_available() + + return available + + +@lru_cache +def is_torch_xla_available(check_is_tpu=False, check_is_gpu=False): + """ + Check if `torch_xla` is available. To train a native pytorch job in an environment with torch xla installed, set + the USE_TORCH_XLA to false. + """ + assert not (check_is_tpu and check_is_gpu), "The check_is_tpu and check_is_gpu cannot both be true." + + if not _torch_xla_available: + return False + elif check_is_gpu: + return torch_xla.runtime.device_type() in ["GPU", "CUDA"] + elif check_is_tpu: + return torch_xla.runtime.device_type() == "TPU" + + return True + + +def is_torchao_available(): + package_exists = _is_package_available("torchao") + if package_exists: + torchao_version = version.parse(importlib.metadata.version("torchao")) + return compare_versions(torchao_version, ">=", "0.6.1") + return False + + +def is_deepspeed_available(): + return _is_package_available("deepspeed") + + +def is_pippy_available(): + return is_torch_version(">=", "2.4.0") + + +def is_bf16_available(ignore_tpu=False): + "Checks if bf16 is supported, optionally ignoring the TPU" + if is_torch_xla_available(check_is_tpu=True): + return not ignore_tpu + if is_cuda_available(): + return torch.cuda.is_bf16_supported() + if is_mlu_available(): + return torch.mlu.is_bf16_supported() + if is_xpu_available(): + return torch.xpu.is_bf16_supported() + if is_mps_available(): + return torch.backends.mps.is_macos_or_newer(14, 0) + return True + + +def is_fp16_available(): + "Checks if fp16 is supported" + if is_habana_gaudi1(): + return False + + return True + + +def is_fp8_available(): + "Checks if fp8 is supported" + return is_msamp_available() or is_transformer_engine_available() or is_torchao_available() + + +def is_4bit_bnb_available(): + package_exists = _is_package_available("bitsandbytes") + if package_exists: + bnb_version = version.parse(importlib.metadata.version("bitsandbytes")) + return compare_versions(bnb_version, ">=", "0.39.0") + return False + + +def is_8bit_bnb_available(): + package_exists = _is_package_available("bitsandbytes") + if package_exists: + bnb_version = version.parse(importlib.metadata.version("bitsandbytes")) + return compare_versions(bnb_version, ">=", "0.37.2") + return False + + +def is_bnb_available(min_version=None): + package_exists = _is_package_available("bitsandbytes") + if package_exists and min_version is not None: + bnb_version = version.parse(importlib.metadata.version("bitsandbytes")) + return compare_versions(bnb_version, ">=", min_version) + else: + return package_exists + + +def is_bitsandbytes_multi_backend_available(): + if not is_bnb_available(): + return False + import bitsandbytes as bnb + + return "multi_backend" in getattr(bnb, "features", set()) + + +def is_torchvision_available(): + return _is_package_available("torchvision") + + +def is_megatron_lm_available(): + if str_to_bool(os.environ.get("ACCELERATE_USE_MEGATRON_LM", "False")) == 1: + if importlib.util.find_spec("megatron") is not None: + try: + megatron_version = parse(importlib.metadata.version("megatron-core")) + if compare_versions(megatron_version, ">=", "0.8.0"): + return importlib.util.find_spec(".training", "megatron") + except Exception as e: + warnings.warn(f"Parse Megatron version failed. Exception:{e}") + return False + + +def is_transformers_available(): + return _is_package_available("transformers") + + +def is_datasets_available(): + return _is_package_available("datasets") + + +def is_peft_available(): + return _is_package_available("peft") + + +def is_timm_available(): + return _is_package_available("timm") + + +def is_triton_available(): + if is_xpu_available(): + return _is_package_available("triton", "pytorch-triton-xpu") + return _is_package_available("triton") + + +def is_aim_available(): + package_exists = _is_package_available("aim") + if package_exists: + aim_version = version.parse(importlib.metadata.version("aim")) + return compare_versions(aim_version, "<", "4.0.0") + return False + + +def is_tensorboard_available(): + return _is_package_available("tensorboard") or _is_package_available("tensorboardX") + + +def is_wandb_available(): + return _is_package_available("wandb") + + +def is_comet_ml_available(): + return _is_package_available("comet_ml") + + +def is_swanlab_available(): + return _is_package_available("swanlab") + + +def is_trackio_available(): + return sys.version_info >= (3, 10) and _is_package_available("trackio") + + +def is_boto3_available(): + return _is_package_available("boto3") + + +def is_rich_available(): + if _is_package_available("rich"): + return parse_flag_from_env("ACCELERATE_ENABLE_RICH", False) + return False + + +def is_sagemaker_available(): + return _is_package_available("sagemaker") + + +def is_tqdm_available(): + return _is_package_available("tqdm") + + +def is_clearml_available(): + return _is_package_available("clearml") + + +def is_pandas_available(): + return _is_package_available("pandas") + + +def is_matplotlib_available(): + return _is_package_available("matplotlib") + + +def is_mlflow_available(): + if _is_package_available("mlflow"): + return True + + if importlib.util.find_spec("mlflow") is not None: + try: + _ = importlib.metadata.metadata("mlflow-skinny") + return True + except importlib.metadata.PackageNotFoundError: + return False + return False + + +def is_mps_available(min_version="1.12"): + "Checks if MPS device is available. The minimum version required is 1.12." + # With torch 1.12, you can use torch.backends.mps + # With torch 2.0.0, you can use torch.mps + return is_torch_version(">=", min_version) and torch.backends.mps.is_available() and torch.backends.mps.is_built() + + +@lru_cache +def is_mlu_available(check_device=False): + """ + Checks if `mlu` is available via an `cndev-based` check which won't trigger the drivers and leave mlu + uninitialized. + """ + if importlib.util.find_spec("torch_mlu") is None: + return False + + import torch_mlu # noqa: F401 + + with patch_environment(PYTORCH_CNDEV_BASED_MLU_CHECK="1"): + available = torch.mlu.is_available() + + return available + + +@lru_cache +def is_musa_available(check_device=False): + "Checks if `torch_musa` is installed and potentially if a MUSA is in the environment" + if importlib.util.find_spec("torch_musa") is None: + return False + + import torch_musa # noqa: F401 + + if check_device: + try: + # Will raise a RuntimeError if no MUSA is found + _ = torch.musa.device_count() + return torch.musa.is_available() + except RuntimeError: + return False + return hasattr(torch, "musa") and torch.musa.is_available() + + +@lru_cache +def is_npu_available(check_device=False): + "Checks if `torch_npu` is installed and potentially if a NPU is in the environment" + if importlib.util.find_spec("torch_npu") is None: + return False + + # NOTE: importing torch_npu may raise error in some envs + # e.g. inside cpu-only container with torch_npu installed + try: + import torch_npu # noqa: F401 + except Exception: + return False + + if check_device: + try: + # Will raise a RuntimeError if no NPU is found + _ = torch.npu.device_count() + return torch.npu.is_available() + except RuntimeError: + return False + return hasattr(torch, "npu") and torch.npu.is_available() + + +@lru_cache +def is_sdaa_available(check_device=False): + "Checks if `torch_sdaa` is installed and potentially if a SDAA is in the environment" + if importlib.util.find_spec("torch_sdaa") is None: + return False + + import torch_sdaa # noqa: F401 + + if check_device: + try: + # Will raise a RuntimeError if no NPU is found + _ = torch.sdaa.device_count() + return torch.sdaa.is_available() + except RuntimeError: + return False + return hasattr(torch, "sdaa") and torch.sdaa.is_available() + + +@lru_cache +def is_hpu_available(init_hccl=False): + "Checks if `torch.hpu` is installed and potentially if a HPU is in the environment" + if ( + importlib.util.find_spec("habana_frameworks") is None + or importlib.util.find_spec("habana_frameworks.torch") is None + ): + return False + + import habana_frameworks.torch # noqa: F401 + + if init_hccl: + import habana_frameworks.torch.distributed.hccl as hccl # noqa: F401 + + return hasattr(torch, "hpu") and torch.hpu.is_available() + + +def is_habana_gaudi1(): + if is_hpu_available(): + import habana_frameworks.torch.utils.experimental as htexp # noqa: F401 + + if htexp._get_device_type() == htexp.synDeviceType.synDeviceGaudi: + return True + + return False + + +@lru_cache +def is_xpu_available(check_device=False): + """ + Checks if XPU acceleration is available via stock PyTorch (>=2.7) and + potentially if a XPU is in the environment + """ + + if is_torch_version("<=", "2.6"): + return False + + if check_device: + try: + # Will raise a RuntimeError if no XPU is found + _ = torch.xpu.device_count() + return torch.xpu.is_available() + except RuntimeError: + return False + return hasattr(torch, "xpu") and torch.xpu.is_available() + + +@lru_cache +def is_neuron_available(check_device=False): + if importlib.util.find_spec("torch_neuronx") is None: + return False + + if check_device: + try: + import torch_neuronx # noqa: F401 + + # Will raise a RuntimeError if no Neuron is found + _ = torch.neuron.device_count() + return torch.neuron.is_available() + except RuntimeError: + return False + + return hasattr(torch, "neuron") and torch.neuron.is_available() + + +def is_dvclive_available(): + return _is_package_available("dvclive") + + +def is_torchdata_available(): + return _is_package_available("torchdata") + + +# TODO: Remove this function once stateful_dataloader is a stable feature in torchdata. +def is_torchdata_stateful_dataloader_available(): + package_exists = _is_package_available("torchdata") + if package_exists: + torchdata_version = version.parse(importlib.metadata.version("torchdata")) + return compare_versions(torchdata_version, ">=", "0.8.0") + return False + + +def torchao_required(func): + """ + A decorator that ensures the decorated function is only called when torchao is available. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + if not is_torchao_available(): + raise ImportError( + "`torchao` is not available, please install it before calling this function via `pip install torchao`." + ) + return func(*args, **kwargs) + + return wrapper + + +# TODO: Rework this into `utils.deepspeed` and migrate the "core" chunks into `accelerate.deepspeed` +def deepspeed_required(func): + """ + A decorator that ensures the decorated function is only called when deepspeed is enabled. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + from accelerate.state import AcceleratorState + from accelerate.utils.dataclasses import DistributedType + + if AcceleratorState._shared_state != {} and AcceleratorState().distributed_type != DistributedType.DEEPSPEED: + raise ValueError( + "DeepSpeed is not enabled, please make sure that an `Accelerator` is configured for `deepspeed` " + "before calling this function." + ) + return func(*args, **kwargs) + + return wrapper + + +def is_weights_only_available(): + # Weights only with allowlist was added in 2.4.0 + # ref: https://github.com/pytorch/pytorch/pull/124331 + return is_torch_version(">=", "2.4.0") + + +def is_numpy_available(min_version="1.25.0"): + numpy_version = parse(importlib.metadata.version("numpy")) + return compare_versions(numpy_version, ">=", min_version) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/launch.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/launch.py new file mode 100644 index 0000000000000000000000000000000000000000..a1801c8c4335a445026b64bc00ff3fc4abe0414f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/launch.py @@ -0,0 +1,827 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import os +import subprocess +import sys +import warnings +from ast import literal_eval +from shutil import which +from typing import Any + +import torch + +from ..commands.config.config_args import SageMakerConfig +from ..utils import ( + DynamoBackend, + PrecisionType, + is_fp8_available, + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_torch_xla_available, + is_xpu_available, +) +from ..utils.constants import DEEPSPEED_MULTINODE_LAUNCHERS +from ..utils.other import get_free_port, is_port_in_use, merge_dicts +from ..utils.versions import compare_versions +from . import parse_flag_from_env +from .dataclasses import DistributedType, SageMakerDistributedType + + +def _filter_args(args, parser, default_args=[]): + """ + Filters out all `accelerate` specific args + """ + new_args, _ = parser.parse_known_args(default_args) + for key, value in vars(args).items(): + if key in vars(new_args).keys(): + setattr(new_args, key, value) + return new_args + + +def _get_mpirun_args(): + """ + Determines the executable and argument names for mpirun, based on the type of install. The supported MPI programs + are: OpenMPI, Intel MPI, or MVAPICH. + + Returns: Program name and arg names for hostfile, num processes, and processes per node + """ + # Find the MPI program name + mpi_apps = [x for x in ["mpirun", "mpiexec"] if which(x)] + + if len(mpi_apps) == 0: + raise OSError("mpirun or mpiexec were not found. Ensure that Intel MPI, Open MPI, or MVAPICH are installed.") + + # Call the app with the --version flag to determine which MPI app is installed + mpi_app = mpi_apps[0] + mpirun_version = subprocess.check_output([mpi_app, "--version"]) + + if b"Open MPI" in mpirun_version: + return mpi_app, "--hostfile", "-n", "--npernode", "--bind-to" + else: + # Intel MPI and MVAPICH both use the same arg names + return mpi_app, "-f", "-n", "-ppn", "" + + +def setup_fp8_env(args: argparse.Namespace, current_env: dict[str, str]): + """ + Setup the FP8 environment variables. + """ + prefix = "ACCELERATE_" + for arg in vars(args): + if arg.startswith("fp8_"): + value = getattr(args, arg) + if value is not None: + if arg == "fp8_override_linear_precision": + current_env[prefix + "FP8_OVERRIDE_FPROP"] = str(value[0]) + current_env[prefix + "FP8_OVERRIDE_DGRAD"] = str(value[1]) + current_env[prefix + "FP8_OVERRIDE_WGRAD"] = str(value[2]) + else: + current_env[f"{prefix}{arg.upper()}"] = str(getattr(args, arg)) + return current_env + + +def prepare_simple_launcher_cmd_env(args: argparse.Namespace) -> tuple[list[str], dict[str, str]]: + """ + Prepares and returns the command list and an environment with the correct simple launcher environment variables. + """ + cmd = [] + if args.no_python and args.module: + raise ValueError("--module and --no_python cannot be used together") + + num_processes = getattr(args, "num_processes", None) + num_machines = args.num_machines + if args.mpirun_hostfile is not None: + mpi_app_name, hostfile_arg, num_proc_arg, proc_per_node_arg, bind_to_arg = _get_mpirun_args() + bind_to = getattr(args, "bind-to", "socket") + nproc_per_node = str(num_processes // num_machines) if num_processes and num_machines else "1" + cmd += [ + mpi_app_name, + hostfile_arg, + args.mpirun_hostfile, + proc_per_node_arg, + nproc_per_node, + ] + if num_processes: + cmd += [num_proc_arg, str(num_processes)] + if bind_to_arg: + cmd += [bind_to_arg, bind_to] + if not args.no_python: + cmd.append(sys.executable) + if args.module: + cmd.append("-m") + cmd.append(args.training_script) + cmd.extend(args.training_script_args) + + current_env = os.environ.copy() + current_env["ACCELERATE_USE_CPU"] = str(args.cpu or args.use_cpu) + if args.debug: + current_env["ACCELERATE_DEBUG_MODE"] = "true" + if args.gpu_ids != "all" and args.gpu_ids is not None: + if is_xpu_available(): + current_env["ZE_AFFINITY_MASK"] = args.gpu_ids + elif is_mlu_available(): + current_env["MLU_VISIBLE_DEVICES"] = args.gpu_ids + elif is_sdaa_available(): + current_env["SDAA_VISIBLE_DEVICES"] = args.gpu_ids + elif is_musa_available(): + current_env["MUSA_VISIBLE_DEVICES"] = args.gpu_ids + elif is_npu_available(): + current_env["ASCEND_RT_VISIBLE_DEVICES"] = args.gpu_ids + elif is_hpu_available(): + current_env["HABANA_VISIBLE_MODULES"] = args.gpu_ids + elif is_neuron_available(): + current_env["NEURON_RT_VISIBLE_CORES"] = args.gpu_ids + else: + current_env["CUDA_VISIBLE_DEVICES"] = args.gpu_ids + if num_machines > 1: + assert args.main_process_ip is not None, ( + "When using multiple machines, you need to specify the main process IP." + ) + assert args.main_process_port is not None, ( + "When using multiple machines, you need to specify the main process port." + ) + + if (num_processes is not None and num_processes > 1) or num_machines > 1: + current_env["MASTER_ADDR"] = args.main_process_ip if args.main_process_ip is not None else "127.0.0.1" + current_env["MASTER_PORT"] = str(args.main_process_port) if args.main_process_port is not None else "29500" + if parse_flag_from_env(current_env["ACCELERATE_USE_CPU"], False): + current_env["KMP_AFFINITY"] = "granularity=fine,compact,1,0" + current_env["KMP_BLOCKTIME"] = str(1) + + try: + mixed_precision = PrecisionType(args.mixed_precision.lower()) + except ValueError: + raise ValueError( + f"Unknown mixed_precision mode: {args.mixed_precision.lower()}. Choose between {PrecisionType.list()}." + ) + + current_env["ACCELERATE_MIXED_PRECISION"] = str(mixed_precision) + if args.mixed_precision.lower() == "fp8": + if not is_fp8_available(): + raise RuntimeError( + "FP8 is not available on this machine. Please ensure that either Transformer Engine, MSAMP or torchao is installed." + ) + current_env = setup_fp8_env(args, current_env) + + try: + dynamo_backend = DynamoBackend(args.dynamo_backend.upper()) + except ValueError: + raise ValueError( + f"Unknown dynamo backend: {args.dynamo_backend.upper()}. Choose between {DynamoBackend.list()}." + ) + current_env["ACCELERATE_DYNAMO_BACKEND"] = dynamo_backend.value + current_env["ACCELERATE_DYNAMO_MODE"] = args.dynamo_mode + current_env["ACCELERATE_DYNAMO_USE_FULLGRAPH"] = str(args.dynamo_use_fullgraph) + current_env["ACCELERATE_DYNAMO_USE_DYNAMIC"] = str(args.dynamo_use_dynamic) + current_env["ACCELERATE_DYNAMO_USE_REGIONAL_COMPILATION"] = str(args.dynamo_use_regional_compilation) + + current_env["OMP_NUM_THREADS"] = str(args.num_cpu_threads_per_process) + if args.enable_cpu_affinity: + current_env["ACCELERATE_CPU_AFFINITY"] = "1" + return cmd, current_env + + +def prepare_multi_gpu_env(args: argparse.Namespace) -> dict[str, str]: + """ + Prepares and returns an environment with the correct multi-GPU environment variables. + """ + # get free port and update configurations + if args.main_process_port == 0: + args.main_process_port = get_free_port() + + elif args.main_process_port is None: + args.main_process_port = 29500 + + num_processes = args.num_processes + num_machines = args.num_machines + main_process_ip = args.main_process_ip + main_process_port = args.main_process_port + if num_machines > 1: + args.nproc_per_node = str(num_processes // num_machines) + args.nnodes = str(num_machines) + args.node_rank = int(args.machine_rank) + if getattr(args, "same_network", False): + args.master_addr = str(main_process_ip) + args.master_port = str(main_process_port) + else: + args.rdzv_endpoint = f"{main_process_ip}:{main_process_port}" + else: + args.nproc_per_node = str(num_processes) + if main_process_port is not None: + args.master_port = str(main_process_port) + + # only need to check port availability in main process, in case we have to start multiple launchers on the same machine + # for some reasons like splitting log files. + need_port_check = num_machines <= 1 or int(args.machine_rank) == 0 + if need_port_check and is_port_in_use(main_process_port): + if num_machines <= 1: + args.standalone = True + warnings.warn( + f"Port `{main_process_port}` is already in use. " + "Accelerate will attempt to launch in a standalone-like mode by finding an open port automatically for this session. " + "If this current attempt fails, or for more control in future runs, please specify a different port " + "(e.g., `--main_process_port `) or use `--main_process_port 0` for automatic selection " + "in your launch command or Accelerate config file." + ) + else: + raise ConnectionError( + f"Tried to launch distributed communication on port `{main_process_port}`, but another process is utilizing it. " + "Please specify a different port (such as using the `--main_process_port` flag or specifying a different `main_process_port` in your config file)" + " and rerun your script. To automatically use the next open port (on a single node), you can set this to `0`." + ) + + if args.module and args.no_python: + raise ValueError("--module and --no_python cannot be used together") + elif args.module: + args.module = True + elif args.no_python: + args.no_python = True + + current_env = os.environ.copy() + if args.debug: + current_env["ACCELERATE_DEBUG_MODE"] = "true" + gpu_ids = getattr(args, "gpu_ids", "all") + if gpu_ids != "all" and args.gpu_ids is not None: + if is_xpu_available(): + current_env["ZE_AFFINITY_MASK"] = gpu_ids + elif is_mlu_available(): + current_env["MLU_VISIBLE_DEVICES"] = gpu_ids + elif is_sdaa_available(): + current_env["SDAA_VISIBLE_DEVICES"] = gpu_ids + elif is_musa_available(): + current_env["MUSA_VISIBLE_DEVICES"] = gpu_ids + elif is_npu_available(): + current_env["ASCEND_RT_VISIBLE_DEVICES"] = gpu_ids + elif is_hpu_available(): + current_env["HABANA_VISIBLE_MODULES"] = gpu_ids + elif is_neuron_available(): + current_env["NEURON_RT_VISIBLE_CORES"] = gpu_ids + else: + current_env["CUDA_VISIBLE_DEVICES"] = gpu_ids + mixed_precision = args.mixed_precision.lower() + try: + mixed_precision = PrecisionType(mixed_precision) + except ValueError: + raise ValueError(f"Unknown mixed_precision mode: {mixed_precision}. Choose between {PrecisionType.list()}.") + + current_env["ACCELERATE_MIXED_PRECISION"] = str(mixed_precision) + if args.mixed_precision.lower() == "fp8": + if not is_fp8_available(): + raise RuntimeError( + "FP8 is not available on this machine. Please ensure that either Transformer Engine, MSAMP or torchao is installed." + ) + current_env = setup_fp8_env(args, current_env) + + try: + dynamo_backend = DynamoBackend(args.dynamo_backend.upper()) + except ValueError: + raise ValueError( + f"Unknown dynamo backend: {args.dynamo_backend.upper()}. Choose between {DynamoBackend.list()}." + ) + current_env["ACCELERATE_DYNAMO_BACKEND"] = dynamo_backend.value + current_env["ACCELERATE_DYNAMO_MODE"] = args.dynamo_mode + current_env["ACCELERATE_DYNAMO_USE_FULLGRAPH"] = str(args.dynamo_use_fullgraph) + current_env["ACCELERATE_DYNAMO_USE_DYNAMIC"] = str(args.dynamo_use_dynamic) + current_env["ACCELERATE_DYNAMO_USE_REGIONAL_COMPILATION"] = str(args.dynamo_use_regional_compilation) + + if args.use_fsdp: + current_env["ACCELERATE_USE_FSDP"] = "true" + if args.fsdp_cpu_ram_efficient_loading and not args.fsdp_sync_module_states: + raise ValueError("When using `--fsdp_cpu_ram_efficient_loading` set `--fsdp_sync_module_states` to `True`") + + current_env["FSDP_VERSION"] = str(args.fsdp_version) if hasattr(args, "fsdp_version") else "1" + + # For backwards compatibility, we support this in launched scripts, + # however, we do not ask users for this in `accelerate config` CLI + current_env["FSDP_SHARDING_STRATEGY"] = str(args.fsdp_sharding_strategy) + + current_env["FSDP_RESHARD_AFTER_FORWARD"] = str(args.fsdp_reshard_after_forward).lower() + current_env["FSDP_OFFLOAD_PARAMS"] = str(args.fsdp_offload_params).lower() + current_env["FSDP_MIN_NUM_PARAMS"] = str(args.fsdp_min_num_params) + if args.fsdp_auto_wrap_policy is not None: + current_env["FSDP_AUTO_WRAP_POLICY"] = str(args.fsdp_auto_wrap_policy) + if args.fsdp_transformer_layer_cls_to_wrap is not None: + current_env["FSDP_TRANSFORMER_CLS_TO_WRAP"] = str(args.fsdp_transformer_layer_cls_to_wrap) + if args.fsdp_backward_prefetch is not None: + current_env["FSDP_BACKWARD_PREFETCH"] = str(args.fsdp_backward_prefetch) + if args.fsdp_state_dict_type is not None: + current_env["FSDP_STATE_DICT_TYPE"] = str(args.fsdp_state_dict_type) + current_env["FSDP_FORWARD_PREFETCH"] = str(args.fsdp_forward_prefetch).lower() + current_env["FSDP_USE_ORIG_PARAMS"] = str(args.fsdp_use_orig_params).lower() + current_env["FSDP_CPU_RAM_EFFICIENT_LOADING"] = str(args.fsdp_cpu_ram_efficient_loading).lower() + current_env["FSDP_SYNC_MODULE_STATES"] = str(args.fsdp_sync_module_states).lower() + current_env["FSDP_ACTIVATION_CHECKPOINTING"] = str(args.fsdp_activation_checkpointing).lower() + if getattr(args, "fsdp_ignored_modules", None) is not None: + current_env["FSDP_IGNORED_MODULES"] = str(args.fsdp_ignored_modules) + + if args.use_megatron_lm: + prefix = "MEGATRON_LM_" + current_env["ACCELERATE_USE_MEGATRON_LM"] = "true" + current_env[prefix + "TP_DEGREE"] = str(args.megatron_lm_tp_degree) + current_env[prefix + "USE_CUSTOM_FSDP"] = str(args.megatron_lm_use_custom_fsdp) + if args.megatron_lm_no_load_optim is not None: + current_env[prefix + "NO_LOAD_OPTIM"] = str(args.megatron_lm_no_load_optim) + if args.megatron_lm_eod_mask_loss is not None: + current_env[prefix + "EOD_MASK_LOSS"] = str(args.megatron_lm_eod_mask_loss) + if args.megatron_lm_no_save_optim is not None: + current_env[prefix + "NO_SAVE_OPTIM"] = str(args.megatron_lm_no_save_optim) + if args.megatron_lm_optimizer_cpu_offload is not None: + current_env[prefix + "OPTIMIZER_CPU_OFFLOAD"] = str(args.megatron_lm_optimizer_cpu_offload) + if args.megatron_lm_use_precision_aware_optimizer is not None: + current_env[prefix + "USE_PRECISION_AWARE_OPTIMIZER"] = str(args.megatron_lm_use_precision_aware_optimizer) + if args.megatron_lm_overlap_cpu_optimizer_d2h_h2d is not None: + current_env[prefix + "OVERLAP_CPU_OPTIMIZER_D2H_H2D"] = str(args.megatron_lm_overlap_cpu_optimizer_d2h_h2d) + if args.megatron_lm_decoder_last_pipeline_num_layers is not None: + current_env[prefix + "DECODER_LAST_PIPELINE_NUM_LAYERS"] = str( + args.megatron_lm_decoder_last_pipeline_num_layers + ) + current_env[prefix + "PP_DEGREE"] = str(args.megatron_lm_pp_degree) + current_env[prefix + "GRADIENT_CLIPPING"] = str(args.megatron_lm_gradient_clipping) + if args.megatron_lm_num_micro_batches is not None: + current_env[prefix + "NUM_MICRO_BATCHES"] = str(args.megatron_lm_num_micro_batches) + if args.megatron_lm_sequence_parallelism is not None: + current_env[prefix + "SEQUENCE_PARALLELISM"] = str(args.megatron_lm_sequence_parallelism) + if args.megatron_lm_recompute_activations is not None: + current_env[prefix + "RECOMPUTE_ACTIVATIONS"] = str(args.megatron_lm_recompute_activations) + if args.megatron_lm_use_distributed_optimizer is not None: + current_env[prefix + "USE_DISTRIBUTED_OPTIMIZER"] = str(args.megatron_lm_use_distributed_optimizer) + if args.megatron_lm_recompute_granularity is not None: + current_env[prefix + "RECOMPUTE_GRANULARITY"] = str(args.megatron_lm_recompute_granularity) + if args.megatron_lm_recompute_method is not None: + current_env[prefix + "RECOMPUTE_METHOD"] = str(args.megatron_lm_recompute_method) + if args.megatron_lm_recompute_num_layers is not None: + current_env[prefix + "RECOMPUTE_NUM_LAYERS"] = str(args.megatron_lm_recompute_num_layers) + if args.megatron_lm_attention_backend is not None: + current_env[prefix + "ATTENTION_BACKEND"] = str(args.megatron_lm_attention_backend) + if args.megatron_lm_expert_model_parallel_size is not None: + current_env[prefix + "EXPERT_MODEL_PARALLEL_SIZE"] = str(args.megatron_lm_expert_model_parallel_size) + if args.megatron_lm_context_parallel_size is not None: + current_env[prefix + "CONTEXT_PARALLEL_SIZE"] = str(args.megatron_lm_context_parallel_size) + if args.megatron_lm_attention_dropout is not None: + current_env[prefix + "ATTENTION_DROPOUT"] = str(args.megatron_lm_attention_dropout) + if args.megatron_lm_hidden_dropout is not None: + current_env[prefix + "HIDDEN_DROPOUT"] = str(args.megatron_lm_hidden_dropout) + if args.megatron_lm_attention_softmax_in_fp32 is not None: + current_env[prefix + "ATTENTION_SOFTMAX_IN_FP32"] = str(args.megatron_lm_attention_softmax_in_fp32) + if args.megatron_lm_expert_tensor_parallel_size is not None: + current_env[prefix + "EXPERT_TENSOR_PARALLEL_SIZE"] = str(args.megatron_lm_expert_tensor_parallel_size) + if args.megatron_lm_calculate_per_token_loss is not None: + current_env[prefix + "CALCULATE_PER_TOKEN_LOSS"] = str(args.megatron_lm_calculate_per_token_loss) + if args.megatron_lm_use_rotary_position_embeddings is not None: + current_env[prefix + "USE_ROTARY_POSITION_EMBEDDINGS"] = str( + args.megatron_lm_use_rotary_position_embeddings + ) + + current_env["OMP_NUM_THREADS"] = str(args.num_cpu_threads_per_process) + if args.enable_cpu_affinity: + current_env["ACCELERATE_CPU_AFFINITY"] = "1" + + if args.use_parallelism_config: + current_env = prepare_extend_env_parallelism_config(args, current_env) + + return current_env + + +def prepare_extend_env_parallelism_config( + args: argparse.Namespace, current_env: dict +) -> tuple[list[str], dict[str, str]]: + """ + Extends `current_env` with context parallelism env vars if any have been set + """ + + prefix = "PARALLELISM_CONFIG_" + + current_env["ACCELERATE_USE_PARALLELISM_CONFIG"] = "true" + current_env[prefix + "DP_REPLICATE_SIZE"] = str(args.parallelism_config_dp_replicate_size) + current_env[prefix + "DP_SHARD_SIZE"] = str(args.parallelism_config_dp_shard_size) + current_env[prefix + "TP_SIZE"] = str(args.parallelism_config_tp_size) + current_env[prefix + "CP_SIZE"] = str(args.parallelism_config_cp_size) + current_env[prefix + "CP_BACKEND"] = str(args.parallelism_config_cp_backend) + current_env[prefix + "SP_SIZE"] = str(args.parallelism_config_sp_size) + current_env[prefix + "SP_BACKEND"] = str(args.parallelism_config_sp_backend) + if args.parallelism_config_cp_size > 1: + current_env[prefix + "CP_COMM_STRATEGY"] = str(args.parallelism_config_cp_comm_strategy) + if args.parallelism_config_sp_size > 1: + current_env[prefix + "SP_SEQ_LENGTH"] = str(args.parallelism_config_sp_seq_length) + current_env[prefix + "SP_SEQ_LENGTH_IS_VARIABLE"] = str(args.parallelism_config_sp_seq_length_is_variable) + current_env[prefix + "SP_ATTN_IMPLEMENTATION"] = str(args.parallelism_config_sp_attn_implementation) + + return current_env + + +def prepare_deepspeed_cmd_env(args: argparse.Namespace) -> tuple[list[str], dict[str, str]]: + """ + Prepares and returns the command list and an environment with the correct DeepSpeed environment variables. + """ + # get free port and update configurations + if args.main_process_port == 0: + args.main_process_port = get_free_port() + + elif args.main_process_port is None: + args.main_process_port = 29500 + + num_processes = args.num_processes + num_machines = args.num_machines + main_process_ip = args.main_process_ip + main_process_port = args.main_process_port + cmd = None + + # make sure launcher is not None + if args.deepspeed_multinode_launcher is None: + # set to default pdsh + args.deepspeed_multinode_launcher = DEEPSPEED_MULTINODE_LAUNCHERS[0] + + if num_machines > 1 and args.deepspeed_multinode_launcher != DEEPSPEED_MULTINODE_LAUNCHERS[1]: + cmd = ["deepspeed"] + cmd.extend(["--hostfile", str(args.deepspeed_hostfile)]) + if args.deepspeed_multinode_launcher == "nossh": + if compare_versions("deepspeed", "<", "0.14.5"): + raise ValueError("nossh launcher requires DeepSpeed >= 0.14.5") + cmd.extend(["--node_rank", str(args.machine_rank), "--no_ssh"]) + else: + cmd.extend(["--no_local_rank", "--launcher", str(args.deepspeed_multinode_launcher)]) + if args.deepspeed_exclusion_filter is not None: + cmd.extend( + [ + "--exclude", + str(args.deepspeed_exclusion_filter), + ] + ) + elif args.deepspeed_inclusion_filter is not None: + cmd.extend( + [ + "--include", + str(args.deepspeed_inclusion_filter), + ] + ) + else: + cmd.extend(["--num_gpus", str(args.num_processes // args.num_machines)]) + if main_process_ip: + cmd.extend(["--master_addr", str(main_process_ip)]) + cmd.extend(["--master_port", str(main_process_port)]) + if args.module and args.no_python: + raise ValueError("--module and --no_python cannot be used together") + elif args.module: + cmd.append("--module") + elif args.no_python: + cmd.append("--no_python") + cmd.append(args.training_script) + cmd.extend(args.training_script_args) + elif num_machines > 1 and args.deepspeed_multinode_launcher == DEEPSPEED_MULTINODE_LAUNCHERS[1]: + args.nproc_per_node = str(num_processes // num_machines) + args.nnodes = str(num_machines) + args.node_rank = int(args.machine_rank) + if getattr(args, "same_network", False): + args.master_addr = str(main_process_ip) + args.master_port = str(main_process_port) + else: + args.rdzv_endpoint = f"{main_process_ip}:{main_process_port}" + else: + args.nproc_per_node = str(num_processes) + if main_process_port is not None: + args.master_port = str(main_process_port) + + # only need to check port availability in main process, in case we have to start multiple launchers on the same machine + # for some reasons like splitting log files. + need_port_check = num_machines <= 1 or int(args.machine_rank) == 0 + if need_port_check and is_port_in_use(main_process_port): + if num_machines <= 1: + args.standalone = True + warnings.warn( + f"Port `{main_process_port}` is already in use. " + "Accelerate will attempt to launch in a standalone-like mode by finding an open port automatically for this session. " + "If this current attempt fails, or for more control in future runs, please specify a different port " + "(e.g., `--main_process_port `) or use `--main_process_port 0` for automatic selection " + "in your launch command or Accelerate config file." + ) + else: + raise ConnectionError( + f"Tried to launch distributed communication on port `{main_process_port}`, but another process is utilizing it. " + "Please specify a different port (such as using the `--main_process_port` flag or specifying a different `main_process_port` in your config file)" + " and rerun your script. To automatically use the next open port (on a single node), you can set this to `0`." + ) + + if args.module and args.no_python: + raise ValueError("--module and --no_python cannot be used together") + elif args.module: + args.module = True + elif args.no_python: + args.no_python = True + + current_env = os.environ.copy() + if args.debug: + current_env["ACCELERATE_DEBUG_MODE"] = "true" + gpu_ids = getattr(args, "gpu_ids", "all") + if gpu_ids != "all" and args.gpu_ids is not None: + if is_xpu_available(): + current_env["ZE_AFFINITY_MASK"] = gpu_ids + elif is_mlu_available(): + current_env["MLU_VISIBLE_DEVICES"] = gpu_ids + elif is_sdaa_available(): + current_env["SDAA_VISIBLE_DEVICES"] = gpu_ids + elif is_musa_available(): + current_env["MUSA_VISIBLE_DEVICES"] = gpu_ids + elif is_npu_available(): + current_env["ASCEND_RT_VISIBLE_DEVICES"] = gpu_ids + elif is_hpu_available(): + current_env["HABANA_VISIBLE_MODULES"] = gpu_ids + elif is_neuron_available(): + current_env["NEURON_RT_VISIBLE_CORES"] = gpu_ids + else: + current_env["CUDA_VISIBLE_DEVICES"] = gpu_ids + try: + mixed_precision = PrecisionType(args.mixed_precision.lower()) + except ValueError: + raise ValueError( + f"Unknown mixed_precision mode: {args.mixed_precision.lower()}. Choose between {PrecisionType.list()}." + ) + + current_env["PYTHONPATH"] = env_var_path_add("PYTHONPATH", os.path.abspath(".")) + current_env["ACCELERATE_MIXED_PRECISION"] = str(mixed_precision) + if args.mixed_precision.lower() == "fp8": + if not is_fp8_available(): + raise RuntimeError( + "FP8 is not available on this machine. Please ensure that either Transformer Engine, MSAMP or torchao is installed." + ) + current_env = setup_fp8_env(args, current_env) + current_env["ACCELERATE_CONFIG_DS_FIELDS"] = str(args.deepspeed_fields_from_accelerate_config).lower() + current_env["ACCELERATE_USE_DEEPSPEED"] = "true" + if args.zero_stage is not None: + current_env["ACCELERATE_DEEPSPEED_ZERO_STAGE"] = str(args.zero_stage) + if args.gradient_accumulation_steps is not None: + current_env["ACCELERATE_GRADIENT_ACCUMULATION_STEPS"] = str(args.gradient_accumulation_steps) + if args.gradient_clipping is not None: + current_env["ACCELERATE_GRADIENT_CLIPPING"] = str(args.gradient_clipping).lower() + if args.offload_optimizer_device is not None: + current_env["ACCELERATE_DEEPSPEED_OFFLOAD_OPTIMIZER_DEVICE"] = str(args.offload_optimizer_device).lower() + if args.offload_param_device is not None: + current_env["ACCELERATE_DEEPSPEED_OFFLOAD_PARAM_DEVICE"] = str(args.offload_param_device).lower() + if args.zero3_init_flag is not None: + current_env["ACCELERATE_DEEPSPEED_ZERO3_INIT"] = str(args.zero3_init_flag).lower() + if args.zero3_save_16bit_model is not None: + current_env["ACCELERATE_DEEPSPEED_ZERO3_SAVE_16BIT_MODEL"] = str(args.zero3_save_16bit_model).lower() + if args.deepspeed_config_file is not None: + current_env["ACCELERATE_DEEPSPEED_CONFIG_FILE"] = str(args.deepspeed_config_file) + if args.enable_cpu_affinity: + current_env["ACCELERATE_CPU_AFFINITY"] = "1" + if args.deepspeed_moe_layer_cls_names is not None: + current_env["ACCELERATE_DEEPSPEED_MOE_LAYER_CLS_NAMES"] = str(args.deepspeed_moe_layer_cls_names) + + if args.use_parallelism_config: + current_env = prepare_extend_env_parallelism_config(args, current_env) + + return cmd, current_env + + +def prepare_tpu( + args: argparse.Namespace, current_env: dict[str, str], pod: bool = False +) -> tuple[argparse.Namespace, dict[str, str]]: + """ + Prepares and returns an environment with the correct TPU environment variables. + """ + if args.mixed_precision == "bf16" and is_torch_xla_available(check_is_tpu=True): + if args.downcast_bf16: + current_env["XLA_DOWNCAST_BF16"] = "1" + else: + current_env["XLA_USE_BF16"] = "1" + if args.debug: + current_env["ACCELERATE_DEBUG_MODE"] = "true" + if pod: + # Take explicit args and set them up for XLA + args.vm = args.tpu_vm + args.tpu = args.tpu_name + return args, current_env + + +def _convert_nargs_to_dict(nargs: list[str]) -> dict[str, str]: + if len(nargs) < 0: + return {} + # helper function to infer type for argsparser + + def _infer_type(s): + try: + s = float(s) + + if s // 1 == s: + return int(s) + return s + except ValueError: + return s + + parser = argparse.ArgumentParser() + _, unknown = parser.parse_known_args(nargs) + for index, argument in enumerate(unknown): + if argument.startswith(("-", "--")): + action = None + if index + 1 < len(unknown): # checks if next index would be in list + if unknown[index + 1].startswith(("-", "--")): # checks if next element is an key + # raise an error if element is store_true or store_false + raise ValueError( + "SageMaker doesn’t support argparse actions for `store_true` or `store_false`. Please define explicit types" + ) + else: # raise an error if last element is store_true or store_false + raise ValueError( + "SageMaker doesn’t support argparse actions for `store_true` or `store_false`. Please define explicit types" + ) + # adds argument to parser based on action_store true + if action is None: + parser.add_argument(argument, type=_infer_type) + else: + parser.add_argument(argument, action=action) + + return { + key: (literal_eval(value) if value in ("True", "False") else value) + for key, value in parser.parse_args(nargs).__dict__.items() + } + + +def prepare_sagemager_args_inputs( + sagemaker_config: SageMakerConfig, args: argparse.Namespace +) -> tuple[argparse.Namespace, dict[str, Any]]: + # configure environment + print("Configuring Amazon SageMaker environment") + os.environ["AWS_DEFAULT_REGION"] = sagemaker_config.region + + # configure credentials + if sagemaker_config.profile is not None: + os.environ["AWS_PROFILE"] = sagemaker_config.profile + elif args.aws_access_key_id is not None and args.aws_secret_access_key is not None: + os.environ["AWS_ACCESS_KEY_ID"] = args.aws_access_key_id + os.environ["AWS_SECRET_ACCESS_KEY"] = args.aws_secret_access_key + else: + raise OSError("You need to provide an aws_access_key_id and aws_secret_access_key when not using aws_profile") + + # extract needed arguments + source_dir = os.path.dirname(args.training_script) + if not source_dir: # checks if string is empty + source_dir = "." + entry_point = os.path.basename(args.training_script) + if not entry_point.endswith(".py"): + raise ValueError(f'Your training script should be a python script and not "{entry_point}"') + + print("Converting Arguments to Hyperparameters") + hyperparameters = _convert_nargs_to_dict(args.training_script_args) + + try: + mixed_precision = PrecisionType(args.mixed_precision.lower()) + except ValueError: + raise ValueError( + f"Unknown mixed_precision mode: {args.mixed_precision.lower()}. Choose between {PrecisionType.list()}." + ) + + try: + dynamo_backend = DynamoBackend(args.dynamo_backend.upper()) + except ValueError: + raise ValueError( + f"Unknown dynamo backend: {args.dynamo_backend.upper()}. Choose between {DynamoBackend.list()}." + ) + + # Environment variables to be set for use during training job + environment = { + "ACCELERATE_USE_SAGEMAKER": "true", + "ACCELERATE_MIXED_PRECISION": str(mixed_precision), + "ACCELERATE_DYNAMO_BACKEND": dynamo_backend.value, + "ACCELERATE_DYNAMO_MODE": args.dynamo_mode, + "ACCELERATE_DYNAMO_USE_FULLGRAPH": str(args.dynamo_use_fullgraph), + "ACCELERATE_DYNAMO_USE_DYNAMIC": str(args.dynamo_use_dynamic), + "ACCELERATE_DYNAMO_USE_REGIONAL_COMPILATION": str(args.dynamo_use_regional_compilation), + "ACCELERATE_SAGEMAKER_DISTRIBUTED_TYPE": sagemaker_config.distributed_type.value, + } + if args.mixed_precision.lower() == "fp8": + if not is_fp8_available(): + raise RuntimeError( + "FP8 is not available on this machine. Please ensure that either Transformer Engine, MSAMP or torchao is installed." + ) + environment = setup_fp8_env(args, environment) + # configure distribution set up + distribution = None + if sagemaker_config.distributed_type == SageMakerDistributedType.DATA_PARALLEL: + distribution = {"smdistributed": {"dataparallel": {"enabled": True}}} + + # configure sagemaker inputs + sagemaker_inputs = None + if sagemaker_config.sagemaker_inputs_file is not None: + print(f"Loading SageMaker Inputs from {sagemaker_config.sagemaker_inputs_file} file") + sagemaker_inputs = {} + with open(sagemaker_config.sagemaker_inputs_file) as file: + for i, line in enumerate(file): + if i == 0: + continue + l = line.split("\t") + sagemaker_inputs[l[0]] = l[1].strip() + print(f"Loaded SageMaker Inputs: {sagemaker_inputs}") + + # configure sagemaker metrics + sagemaker_metrics = None + if sagemaker_config.sagemaker_metrics_file is not None: + print(f"Loading SageMaker Metrics from {sagemaker_config.sagemaker_metrics_file} file") + sagemaker_metrics = [] + with open(sagemaker_config.sagemaker_metrics_file) as file: + for i, line in enumerate(file): + if i == 0: + continue + l = line.split("\t") + metric_dict = { + "Name": l[0], + "Regex": l[1].strip(), + } + sagemaker_metrics.append(metric_dict) + print(f"Loaded SageMaker Metrics: {sagemaker_metrics}") + + # configure session + print("Creating Estimator") + args = { + "image_uri": sagemaker_config.image_uri, + "entry_point": entry_point, + "source_dir": source_dir, + "role": sagemaker_config.iam_role_name, + "transformers_version": sagemaker_config.transformers_version, + "pytorch_version": sagemaker_config.pytorch_version, + "py_version": sagemaker_config.py_version, + "base_job_name": sagemaker_config.base_job_name, + "instance_count": sagemaker_config.num_machines, + "instance_type": sagemaker_config.ec2_instance_type, + "debugger_hook_config": False, + "distribution": distribution, + "hyperparameters": hyperparameters, + "environment": environment, + "metric_definitions": sagemaker_metrics, + } + + if sagemaker_config.additional_args is not None: + args = merge_dicts(sagemaker_config.additional_args, args) + return args, sagemaker_inputs + + +def env_var_path_add(env_var_name, path_to_add): + """ + Extends a path-based environment variable's value with a new path and returns the updated value. It's up to the + caller to set it in os.environ. + """ + paths = [p for p in os.environ.get(env_var_name, "").split(":") if len(p) > 0] + paths.append(str(path_to_add)) + return ":".join(paths) + + +class PrepareForLaunch: + """ + Prepare a function that will launched in a distributed setup. + + Args: + launcher (`Callable`): + The function to launch. + distributed_type ([`~state.DistributedType`]): + The distributed type to prepare for. + debug (`bool`, *optional*, defaults to `False`): + Whether or not this is a debug launch. + """ + + def __init__(self, launcher, distributed_type="NO", debug=False): + self.launcher = launcher + self.distributed_type = DistributedType(distributed_type) + self.debug = debug + + def __call__(self, index, *args): + if self.debug: + world_size = int(os.environ.get("WORLD_SIZE")) + rdv_file = os.environ.get("ACCELERATE_DEBUG_RDV_FILE") + torch.distributed.init_process_group( + "gloo", + rank=index, + store=torch.distributed.FileStore(rdv_file, world_size), + world_size=world_size, + ) + elif self.distributed_type in ( + DistributedType.MULTI_GPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_CPU, + DistributedType.MULTI_NEURON, + ): + # Prepare the environment for torch.distributed + os.environ["LOCAL_RANK"] = str(index) + nproc = int(os.environ.get("NPROC", 1)) + node_rank = int(os.environ.get("NODE_RANK", 0)) + os.environ["RANK"] = str(nproc * node_rank + index) + + os.environ["FORK_LAUNCHED"] = str(1) + self.launcher(*args) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/megatron_lm.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/megatron_lm.py new file mode 100644 index 0000000000000000000000000000000000000000..ff8900cbad888af9dceca2adc2600a02d213d304 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/megatron_lm.py @@ -0,0 +1,1248 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import math +import os +from abc import ABC +from functools import partial + +import torch +import torch.nn.functional as F +from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss + +from ..optimizer import AcceleratedOptimizer +from ..scheduler import AcceleratedScheduler +from .imports import is_megatron_lm_available +from .operations import recursively_apply, send_to_device + + +if is_megatron_lm_available(): + from megatron.core import mpu, tensor_parallel + from megatron.core.distributed import DistributedDataParallel as LocalDDP + from megatron.core.distributed import finalize_model_grads + from megatron.core.enums import ModelType + from megatron.core.num_microbatches_calculator import get_num_microbatches + from megatron.core.optimizer import get_megatron_optimizer + from megatron.core.parallel_state import get_tensor_model_parallel_group, get_tensor_model_parallel_src_rank + from megatron.core.pipeline_parallel import get_forward_backward_func + from megatron.core.utils import get_model_config + from megatron.legacy.data.dataset_utils import build_train_valid_test_datasets + from megatron.legacy.model import BertModel, T5Model + from megatron.legacy.model.classification import Classification + from megatron.training import ( + get_args, + get_tensorboard_writer, + get_tokenizer, + print_rank_last, + ) + from megatron.training.arguments import ( + _add_data_args, + _add_validation_args, + core_transformer_config_from_args, + parse_args, + validate_args, + ) + from megatron.training.checkpointing import load_args_from_checkpoint, load_checkpoint, save_checkpoint + from megatron.training.global_vars import set_global_variables + from megatron.training.gpt_builders import gpt_builder + from megatron.training.initialize import ( + _compile_dependencies, + _init_autoresume, + _initialize_distributed, + _set_random_seed, + set_jit_fusion_options, + write_args_to_tensorboard, + ) + from megatron.training.tokenizer.tokenizer import _vocab_size_with_padding + from megatron.training.training import ( + build_train_valid_test_data_iterators, + get_optimizer_param_scheduler, + num_floating_point_operations, + setup_model_and_optimizer, + train_step, + training_log, + ) + from megatron.training.utils import ( + average_losses_across_data_parallel_group, + calc_params_l2_norm, + get_ltor_masks_and_position_ids, + ) + + +# model utilities +def model_provider_func(pre_process=True, post_process=True, add_encoder=True, add_decoder=True): + """Build the model.""" + args = get_args() + mode = "pre-training" if args.pretraining_flag else "fine-tuning" + if args.rank == 0: + print(f"Building {args.model_type_name} model in the {mode} mode.") + print( + "The Megatron LM model weights are initialized at random in `accelerator.prepare`. " + "Please use `accelerator.load_checkpoint` to load a pre-trained checkpoint matching the distributed setup." + ) + config = core_transformer_config_from_args(args) + if args.model_type_name == "bert": + if args.pretraining_flag: + num_tokentypes = 2 if args.bert_binary_head else 0 + model = BertModel( + config=config, + num_tokentypes=num_tokentypes, + add_binary_head=args.bert_binary_head, + parallel_output=True, + pre_process=pre_process, + post_process=post_process, + ) + else: + model = Classification( + config=config, + num_classes=args.num_labels, + num_tokentypes=2, + pre_process=pre_process, + post_process=post_process, + ) + elif args.model_type_name == "gpt": + # use the latest gpt builder to build the model and set use_legacy_models to False + args.use_legacy_models = False + model = gpt_builder(args, pre_process, post_process, vp_stage=None, config=None) + elif args.model_type_name == "t5": + model = T5Model( + config=config, + num_tokentypes=0, + parallel_output=True, + pre_process=pre_process, + post_process=post_process, + add_encoder=add_encoder, + add_decoder=add_decoder, + ) + else: + raise ValueError(f"Unsupported model type: {args.model_type_name}") + return model + + +def prepare_model_optimizer_scheduler(accelerator): + accelerator.print("Preparing model optimizer scheduler") + args = get_args() + if accelerator.state.megatron_lm_plugin.custom_prepare_model_function is not None: + if accelerator.state.megatron_lm_plugin.custom_model_provider_function is None: + raise ValueError( + "You must provide a `custom_model_provider_function` when using a `custom_prepare_model_function`." + ) + custom_model_provider_func = accelerator.state.megatron_lm_plugin.custom_model_provider_function + model = accelerator.state.megatron_lm_plugin.custom_prepare_model_function(custom_model_provider_func) + optimizer = prepare_optimizer(accelerator, model) + scheduler = prepare_scheduler(accelerator, optimizer, scheduler=None) + else: + model_type = ModelType.encoder_or_decoder + if args.model_type_name == "t5": + model_type = ModelType.encoder_and_decoder + model_provider_func_ = model_provider_func + if accelerator.state.megatron_lm_plugin.custom_model_provider_function is not None: + model_provider_func_ = accelerator.state.megatron_lm_plugin.custom_model_provider_function + (model, optimizer, scheduler) = setup_model_and_optimizer( + model_provider_func_, + model_type, + ) + args.model_len = len(model) + return model, optimizer, scheduler + + +# dataloader utilities +class MegatronLMDummyDataLoader: + """ + Dummy dataloader presents model parameters or param groups, this is primarily used to follow conventional training + + Args: + **dataset_kwargs: Megatron data arguments. + """ + + def __init__(self, **dataset_kwargs): + parser = argparse.ArgumentParser() + parser = _add_data_args(parser) + parser = _add_validation_args(parser) + data_args = parser.parse_known_args() + self.dataset_args = vars(data_args[0]) + self.dataset_args.update(dataset_kwargs) + self.dataset_args["megatron_dataset_flag"] = True + + def set_megatron_data_args(self): + args = get_args() + for key, value in self.dataset_args.items(): + old_value = getattr(args, key, "") + if old_value != value: + print( + f"WARNING: MegatronLMDummyDataLoader overriding arguments for {key}:{old_value} with {key}:{value}" + ) + setattr(args, key, value) + + def get_train_valid_test_datasets_provider(self, accelerator): + def train_valid_test_datasets_provider(train_val_test_num_samples): + """Build train, valid, and test datasets.""" + args = get_args() + dataset_args = { + "data_prefix": args.data_path if isinstance(args.data_path, (list, tuple)) else [args.data_path], + "splits_string": args.split, + "train_valid_test_num_samples": train_val_test_num_samples, + "seed": args.seed, + } + if args.model_type_name == "bert": + dataset_args.update( + { + "max_seq_length": args.seq_length, + "binary_head": args.bert_binary_head, + } + ) + elif args.model_type_name == "gpt": + dataset_args.update( + { + "max_seq_length": args.seq_length, + } + ) + elif args.model_type_name == "t5": + dataset_args.update( + { + "max_seq_length": args.encoder_seq_length, + "max_seq_length_dec": args.decoder_seq_length, + "dataset_type": "t5", + } + ) + else: + raise ValueError(f"Unsupported model type: {args.model_type_name}") + train_ds, valid_ds, test_ds = build_train_valid_test_datasets(**dataset_args) + return train_ds, valid_ds, test_ds + + if accelerator.state.megatron_lm_plugin.custom_megatron_datasets_provider_function is not None: + return accelerator.state.megatron_lm_plugin.custom_megatron_datasets_provider_function + try: + args = get_args() + # Use '--no-use-pep517 -e' to pip install nvidia's megatron from source + if args.model_type_name == "bert": + from pretrain_bert import train_valid_test_datasets_provider + + train_valid_test_datasets_provider.is_distributed = True + return train_valid_test_datasets_provider + elif args.model_type_name == "gpt": + from pretrain_gpt import train_valid_test_datasets_provider + + train_valid_test_datasets_provider.is_distributed = True + return train_valid_test_datasets_provider + elif args.model_type_name == "t5": + from pretrain_t5 import train_valid_test_datasets_provider + + train_valid_test_datasets_provider.is_distributed = True + return train_valid_test_datasets_provider + except ImportError: + pass + return train_valid_test_datasets_provider + + def build_train_valid_test_data_iterators(self, accelerator): + args = get_args() + + train_valid_test_dataset_provider = self.get_train_valid_test_datasets_provider(accelerator) + if args.virtual_pipeline_model_parallel_size is not None: + train_data_iterator = [] + valid_data_iterator = [] + test_data_iterator = [] + for i in range(getattr(args, "model_len", 0)): + mpu.set_virtual_pipeline_model_parallel_rank(i) + iterators = build_train_valid_test_data_iterators(train_valid_test_dataset_provider) + train_data_iterator.append(iterators[0]) + valid_data_iterator.append(iterators[1]) + test_data_iterator.append(iterators[2]) + else: + train_data_iterator, valid_data_iterator, test_data_iterator = build_train_valid_test_data_iterators( + train_valid_test_dataset_provider + ) + + return train_data_iterator, valid_data_iterator, test_data_iterator + + +def _handle_megatron_data_iterator(accelerator, data_iterator): + class DummyMegatronDataloader: + def __iter__(self): + return self + + def __next__(self): + return {} + + is_data_iterator_empty = data_iterator is None + is_src_data_iterator_empty = torch.tensor(is_data_iterator_empty, dtype=torch.bool, device=accelerator.device) + torch.distributed.broadcast( + is_src_data_iterator_empty, get_tensor_model_parallel_src_rank(), group=get_tensor_model_parallel_group() + ) + if not is_src_data_iterator_empty and is_data_iterator_empty: + return DummyMegatronDataloader() + return data_iterator + + +def prepare_data_loader(accelerator, dataloader): + accelerator.print("Preparing dataloader") + args = get_args() + if not args.megatron_dataset_flag: + from ..data_loader import _PYTORCH_DATALOADER_KWARGS, prepare_data_loader + + micro_batch_size = args.micro_batch_size * args.num_micro_batches + kwargs = {k: getattr(dataloader, k, _PYTORCH_DATALOADER_KWARGS[k]) for k in _PYTORCH_DATALOADER_KWARGS} + if kwargs["batch_size"] is None: + if isinstance(kwargs["sampler"], torch.utils.data.BatchSampler): + kwargs["sampler"].batch_size = micro_batch_size + else: + del kwargs["sampler"] + del kwargs["shuffle"] + del kwargs["batch_size"] + kwargs["batch_sampler"].batch_size = micro_batch_size + else: + del kwargs["batch_sampler"] + kwargs["batch_size"] = micro_batch_size + + dataloader = torch.utils.data.DataLoader(dataloader.dataset, **kwargs) + # split_batches: + # Megatron only needs to fetch different data between different dp groups, + # and does not need to split the data within the dp group. + return prepare_data_loader( + dataloader, + accelerator.device, + num_processes=mpu.get_data_parallel_world_size(), + process_index=mpu.get_data_parallel_rank(), + split_batches=False, + put_on_device=True, + rng_types=accelerator.rng_types.copy(), + dispatch_batches=accelerator.dispatch_batches, + ) + else: + if args.consumed_samples is not None: + ( + args.consumed_train_samples, + args.consumed_valid_samples, + args.consumed_test_samples, + ) = args.consumed_samples + else: + args.consumed_train_samples, args.consumed_valid_samples, args.consumed_test_samples = 0, 0, 0 + args.micro_batch_size = args.micro_batch_size * args.num_micro_batches + # In order to be compatible with data in transform format, + # it needs to increase the size of mbs first, + # and then split the large batch data into some mbs. + ( + train_data_iterator, + valid_data_iterator, + test_data_iterator, + ) = dataloader.build_train_valid_test_data_iterators(accelerator) + args.micro_batch_size = args.micro_batch_size // args.num_micro_batches + + train_data_iterator = _handle_megatron_data_iterator( + accelerator=accelerator, data_iterator=train_data_iterator + ) + valid_data_iterator = _handle_megatron_data_iterator( + accelerator=accelerator, data_iterator=valid_data_iterator + ) + test_data_iterator = _handle_megatron_data_iterator(accelerator=accelerator, data_iterator=test_data_iterator) + + return train_data_iterator, valid_data_iterator, test_data_iterator + + +# optimizer utilities +class MegatronLMOptimizerWrapper(AcceleratedOptimizer): + def __init__(self, optimizer): + super().__init__(optimizer, device_placement=False, scaler=None) + + def zero_grad(self, set_to_none=None): + pass # `model(**batch)` is doing that automatically. Therefore, its implementation is not needed + + def step(self): + pass # `model(**batch)` is doing that automatically. Therefore, its implementation is not needed + + @property + def step_was_skipped(self): + """Whether or not the optimizer step was done, or skipped because of gradient overflow.""" + return self.optimizer.skipped_iter + + +def prepare_optimizer(accelerator, model): + accelerator.print("Preparing optimizer") + args = get_args() + return get_megatron_optimizer(model, args.no_wd_decay_cond, args.scale_lr_cond, args.lr_mult) + + +# scheduler utilities +class MegatronLMDummyScheduler: + """ + Dummy scheduler presents model parameters or param groups, this is primarily used to follow conventional training + loop when scheduler config is specified in the deepspeed config file. + + Args: + optimizer (`torch.optim.optimizer.Optimizer`): + The optimizer to wrap. + total_num_steps (int): + Total number of steps. + warmup_num_steps (int): + Number of steps for warmup. + **kwargs (additional keyword arguments, *optional*): + Other arguments. + """ + + def __init__(self, optimizer, total_num_steps=None, warmup_num_steps=0, **kwargs): + self.optimizer = optimizer + self.total_num_steps = total_num_steps + self.warmup_num_steps = warmup_num_steps + self.kwargs = kwargs + + +class MegatronLMSchedulerWrapper(AcceleratedScheduler): + def __init__(self, scheduler, optimizers): + super().__init__(scheduler, optimizers) + + def step(self, *args, **kwargs): + return # `model(**batch)` is doing that automatically. Therefore, its implementation is not needed + + +def prepare_scheduler(accelerator, optimizer, scheduler): + accelerator.print("Preparing scheduler") + scheduler = get_optimizer_param_scheduler(optimizer) + return scheduler + + +class AbstractTrainStep(ABC): + """Abstract class for batching, forward pass and loss handler.""" + + def __init__(self, name): + super().__init__() + self.name = name + + def get_batch_func(self, accelerator, megatron_dataset_flag): + pass + + def get_forward_step_func(self): + pass + + def get_loss_func(self, accelerator): + pass + + +class BertTrainStep(AbstractTrainStep): + """ + Bert train step class. + + Args: + args (`argparse.Namespace`): Megatron-LM arguments. + """ + + def __init__(self, accelerator, args): + super().__init__("BertTrainStep") + self.get_batch = self.get_batch_func(accelerator, args.megatron_dataset_flag) + self.loss_func = self.get_loss_func(accelerator, args.pretraining_flag, args.num_labels) + self.forward_step = self.get_forward_step_func(args.pretraining_flag, args.bert_binary_head) + if not args.model_return_dict: + self.model_output_class = None + else: + from transformers.modeling_outputs import SequenceClassifierOutput + + self.model_output_class = SequenceClassifierOutput + + def get_batch_func(self, accelerator, megatron_dataset_flag): + def get_batch_megatron(data_iterator): + """Build the batch.""" + + # Items and their type. + keys = ["text", "types", "labels", "is_random", "loss_mask", "padding_mask"] + datatype = torch.int64 + + # Broadcast data. + if data_iterator is not None: + data = next(data_iterator) + else: + data = None + data_b = tensor_parallel.broadcast_data(keys, data, datatype) + + # Unpack. + tokens = data_b["text"].long() + types = data_b["types"].long() + sentence_order = data_b["is_random"].long() + loss_mask = data_b["loss_mask"].float() + lm_labels = data_b["labels"].long() + padding_mask = data_b["padding_mask"].long() + + return tokens, types, sentence_order, loss_mask, lm_labels, padding_mask + + def get_batch_transformer(data_iterator): + """Build the batch.""" + data = next(data_iterator) + data = send_to_device(data, torch.cuda.current_device()) + + # Unpack. + tokens = data["input_ids"].long() + padding_mask = data["attention_mask"].long() + if "token_type_ids" in data: + types = data["token_type_ids"].long() + else: + types = None + if "labels" in data: + lm_labels = data["labels"].long() + loss_mask = (data["labels"] != -100).to(torch.float) + else: + lm_labels = None + loss_mask = None + if "next_sentence_label" in data: + sentence_order = data["next_sentence_label"].long() + else: + sentence_order = None + + return tokens, types, sentence_order, loss_mask, lm_labels, padding_mask + + if accelerator.state.megatron_lm_plugin.custom_get_batch_function is not None: + return accelerator.state.megatron_lm_plugin.custom_get_batch_function + if megatron_dataset_flag: + try: + # Use '--no-use-pep517 -e' to pip install nvidia's megatron from source + from pretrain_bert import get_batch + + return get_batch + except ImportError: + pass + return get_batch_megatron + else: + return get_batch_transformer + + def get_loss_func(self, accelerator, pretraining_flag, num_labels): + def loss_func_pretrain(loss_mask, sentence_order, output_tensor): + lm_loss_, sop_logits = output_tensor + + lm_loss_ = lm_loss_.float() + loss_mask = loss_mask.float() + lm_loss = torch.sum(lm_loss_.view(-1) * loss_mask.reshape(-1)) / loss_mask.sum() + + if sop_logits is not None: + sop_loss = F.cross_entropy(sop_logits.view(-1, 2).float(), sentence_order.view(-1), ignore_index=-1) + sop_loss = sop_loss.float() + loss = lm_loss + sop_loss + averaged_losses = average_losses_across_data_parallel_group([lm_loss, sop_loss]) + return loss, {"lm loss": averaged_losses[0], "sop loss": averaged_losses[1]} + + else: + loss = lm_loss + averaged_losses = average_losses_across_data_parallel_group([lm_loss]) + return loss, {"lm loss": averaged_losses[0]} + + def loss_func_finetune(labels, logits): + if num_labels == 1: + # We are doing regression + loss_fct = MSELoss() + loss = loss_fct(logits.view(-1), labels.view(-1)) + elif self.num_labels > 1 and (labels.dtype in (torch.long, torch.int)): + loss_fct = CrossEntropyLoss() + loss = loss_fct(logits.view(-1, num_labels), labels.view(-1)) + else: + loss_fct = BCEWithLogitsLoss() + loss = loss_fct(logits, labels) + averaged_losses = average_losses_across_data_parallel_group([loss]) + return loss, {"loss": averaged_losses[0]} + + if accelerator.state.megatron_lm_plugin.custom_loss_function is not None: + return accelerator.state.megatron_lm_plugin.custom_loss_function + if pretraining_flag: + return loss_func_pretrain + else: + return loss_func_finetune + + def get_forward_step_func(self, pretraining_flag, bert_binary_head): + def forward_step(data_iterator, model): + """Forward step.""" + tokens, types, sentence_order, loss_mask, labels, padding_mask = self.get_batch(data_iterator) + if not bert_binary_head: + types = None + # Forward pass through the model. + if pretraining_flag: + output_tensor = model(tokens, padding_mask, tokentype_ids=types, lm_labels=labels) + return output_tensor, partial(self.loss_func, loss_mask, sentence_order) + else: + logits = model(tokens, padding_mask, tokentype_ids=types) + return logits, partial(self.loss_func, labels) + + return forward_step + + +class GPTTrainStep(AbstractTrainStep): + """ + GPT train step class. + + Args: + args (`argparse.Namespace`): Megatron-LM arguments. + """ + + def __init__(self, accelerator, args): + super().__init__("GPTTrainStep") + self.get_batch = self.get_batch_func(accelerator, args.megatron_dataset_flag) + self.loss_func = self.get_loss_func(accelerator) + self.forward_step = self.get_forward_step_func() + if args.vocab_file is not None: + tokenizer = get_tokenizer() + self.eod_token = tokenizer.eod + self.eod_token = args.eos_token_id + self.pad_token = args.eos_token_id + self.reset_position_ids = args.reset_position_ids + self.reset_attention_mask = args.reset_attention_mask + self.eod_mask_loss = args.eod_mask_loss + if not args.model_return_dict: + self.model_output_class = None + else: + from transformers.modeling_outputs import CausalLMOutputWithCrossAttentions + + self.model_output_class = CausalLMOutputWithCrossAttentions + + def get_batch_func(self, accelerator, megatron_dataset_flag): + def get_batch_megatron(data_iterator): + """Generate a batch""" + # Items and their type. + keys = ["text"] + datatype = torch.int64 + + # Broadcast data. + if data_iterator is not None: + data = next(data_iterator) + else: + data = None + data_b = tensor_parallel.broadcast_data(keys, data, datatype) + + # Unpack. + tokens_ = data_b["text"].long() + labels = tokens_[:, 1:].contiguous() + tokens = tokens_[:, :-1].contiguous() + + # Get the masks and position ids. + attention_mask, loss_mask, position_ids = get_ltor_masks_and_position_ids( + tokens, + eod_token=self.eod_token, + pad_token=self.eod_token, + reset_position_ids=self.reset_position_ids, + reset_attention_mask=self.reset_attention_mask, + eod_mask_loss=self.eod_mask_loss, + pad_mask_loss=True, + ) + return tokens, labels, loss_mask, attention_mask, position_ids + + def get_batch_transformer(data_iterator): + data = next(data_iterator) + data = {"input_ids": data["input_ids"]} + data = send_to_device(data, torch.cuda.current_device()) + + tokens_ = data["input_ids"].long() + padding = torch.zeros((tokens_.shape[0], 1), dtype=tokens_.dtype, device=tokens_.device) + self.eod_token + tokens_ = torch.concat([tokens_, padding], dim=1) + labels = tokens_[:, 1:].contiguous() + tokens = tokens_[:, :-1].contiguous() + # Get the masks and position ids. + attention_mask, loss_mask, position_ids = get_ltor_masks_and_position_ids( + tokens, + eod_token=self.eod_token, + pad_token=self.eod_token, + reset_position_ids=self.reset_position_ids, + reset_attention_mask=self.reset_attention_mask, + eod_mask_loss=self.eod_mask_loss, + pad_mask_loss=True, + ) + return tokens, labels, loss_mask, attention_mask, position_ids + + if accelerator.state.megatron_lm_plugin.custom_get_batch_function is not None: + return accelerator.state.megatron_lm_plugin.custom_get_batch_function + if megatron_dataset_flag: + try: + # Use '--no-use-pep517 -e' to pip install nvidia's megatron from source + from pretrain_gpt import get_batch + + return get_batch + except ImportError: + pass + return get_batch_megatron + else: + return get_batch_transformer + + def get_loss_func(self, accelerator): + args = get_args() + + def loss_func(loss_mask, output_tensor): + if args.return_logits: + losses, logits = output_tensor + else: + losses = output_tensor + losses = losses.float() + loss_mask = loss_mask.view(-1).float() + if args.context_parallel_size > 1: + loss = torch.cat([torch.sum(losses.view(-1) * loss_mask).view(1), loss_mask.sum().view(1)]) + torch.distributed.all_reduce(loss, group=mpu.get_context_parallel_group()) + loss = loss[0] / loss[1] + else: + loss = torch.sum(losses.view(-1) * loss_mask) / loss_mask.sum() + + # Check individual rank losses are not NaN prior to DP all-reduce. + if args.check_for_nan_in_loss_and_grad: + global_rank = torch.distributed.get_rank() + assert not loss.isnan(), ( + f"Rank {global_rank}: found NaN in local forward loss calculation. " + f"Device: {torch.cuda.current_device()}, node: {os.uname()[1]}" + ) + + # Reduce loss for logging. + averaged_loss = average_losses_across_data_parallel_group([loss]) + + output_dict = {"lm loss": averaged_loss[0]} + if args.return_logits: + output_dict.update({"logits": logits}) + return loss, output_dict + + if accelerator.state.megatron_lm_plugin.custom_loss_function is not None: + return accelerator.state.megatron_lm_plugin.custom_loss_function + return loss_func + + def get_forward_step_func(self): + def forward_step(data_iterator, model): + """Forward step.""" + # Get the batch. + tokens, labels, loss_mask, attention_mask, position_ids = self.get_batch(data_iterator) + output_tensor = model(tokens, position_ids, attention_mask, labels=labels) + + return output_tensor, partial(self.loss_func, loss_mask) + + return forward_step + + +class T5TrainStep(AbstractTrainStep): + """ + T5 train step class. + + Args: + args (`argparse.Namespace`): Megatron-LM arguments. + """ + + def __init__(self, accelerator, args): + super().__init__("T5TrainStep") + self.get_batch = self.get_batch_func(accelerator, args.megatron_dataset_flag) + self.loss_func = self.get_loss_func(accelerator) + self.forward_step = self.get_forward_step_func() + if not args.model_return_dict: + self.model_output_class = None + else: + from transformers.modeling_outputs import Seq2SeqLMOutput + + self.model_output_class = Seq2SeqLMOutput + + @staticmethod + def attn_mask_postprocess(attention_mask): + # We create a 3D attention mask from a 2D tensor mask. + # [b, 1, s] + attention_mask_b1s = attention_mask.unsqueeze(1) + # [b, s, 1] + attention_mask_bs1 = attention_mask.unsqueeze(2) + # [b, s, s] + attention_mask_bss = attention_mask_b1s * attention_mask_bs1 + # Convert attention mask to binary: + extended_attention_mask = attention_mask_bss < 0.5 + return extended_attention_mask + + @staticmethod + def get_decoder_mask(seq_length, device): + attention_mask = torch.tril(torch.ones((1, seq_length, seq_length), device=device)) + attention_mask = attention_mask < 0.5 + return attention_mask + + @staticmethod + def get_enc_dec_mask(attention_mask, dec_seq_length, device): + batch_size, _ = attention_mask.shape + # We create a 3D attention mask from a 2D tensor mask. + # [b, 1, s] + attention_mask_b1s = attention_mask.unsqueeze(1) + # [b, s, 1] + attention_mask_bs1 = torch.ones((batch_size, dec_seq_length, 1), device=device) + attention_mask_bss = attention_mask_bs1 * attention_mask_b1s + extended_attention_mask = attention_mask_bss < 0.5 + return extended_attention_mask + + def get_batch_func(self, accelerator, megatron_dataset_flag): + def get_batch_megatron(data_iterator): + """Build the batch.""" + + keys = ["text_enc", "text_dec", "labels", "loss_mask", "enc_mask", "dec_mask", "enc_dec_mask"] + datatype = torch.int64 + + # Broadcast data. + if data_iterator is not None: + data = next(data_iterator) + else: + data = None + data_b = tensor_parallel.broadcast_data(keys, data, datatype) + + # Unpack. + tokens_enc = data_b["text_enc"].long() + tokens_dec = data_b["text_dec"].long() + labels = data_b["labels"].long() + loss_mask = data_b["loss_mask"].float() + + enc_mask = data_b["enc_mask"] < 0.5 + dec_mask = data_b["dec_mask"] < 0.5 + enc_dec_mask = data_b["enc_dec_mask"] < 0.5 + + return tokens_enc, tokens_dec, loss_mask, labels, enc_mask, dec_mask, enc_dec_mask + + def get_batch_transformer(data_iterator): + """Build the batch.""" + data = next(data_iterator) + data = send_to_device(data, torch.cuda.current_device()) + + tokens_enc = data["input_ids"].long() + labels = data["labels"].long() + loss_mask = (labels != -100).to(torch.float) + if "decoder_input_ids" in data: + tokens_dec = data["decoder_input_ids"].long() + else: + tokens_dec = labels.new_zeros(labels.shape, device=labels.device, dtype=torch.long) + tokens_dec[..., 1:] = labels[..., :-1].clone() + tokens_dec[..., 0] = 0 + tokens_dec.masked_fill_(tokens_dec == -100, 0) + enc_mask = T5TrainStep.attn_mask_postprocess(data["attention_mask"].long()) + dec_mask = T5TrainStep.get_decoder_mask(tokens_dec.shape[1], tokens_dec.device) + enc_dec_mask = T5TrainStep.get_enc_dec_mask( + data["attention_mask"].long(), tokens_dec.shape[1], tokens_dec.device + ) + + return tokens_enc, tokens_dec, loss_mask, labels, enc_mask, dec_mask, enc_dec_mask + + if accelerator.state.megatron_lm_plugin.custom_get_batch_function is not None: + return accelerator.state.megatron_lm_plugin.custom_get_batch_function + if megatron_dataset_flag: + try: + # Use '--no-use-pep517 -e' to pip install nvidia's megatron from source + from pretrain_t5 import get_batch + + return get_batch + except ImportError: + pass + return get_batch_megatron + else: + return get_batch_transformer + + def get_loss_func(self, accelerator): + def loss_func(loss_mask, output_tensor): + lm_loss_ = output_tensor.float() + lm_loss = torch.sum(lm_loss_.view(-1) * loss_mask.reshape(-1)) / loss_mask.sum() + + loss = lm_loss + averaged_losses = average_losses_across_data_parallel_group([lm_loss]) + + return loss, {"lm loss": averaged_losses[0]} + + if accelerator.state.megatron_lm_plugin.custom_loss_function is not None: + return accelerator.state.megatron_lm_plugin.custom_loss_function + return loss_func + + def get_forward_step_func(self): + def forward_step(data_iterator, model): + """Forward step.""" + # Get the batch. + tokens_enc, tokens_dec, loss_mask, lm_labels, enc_mask, dec_mask, enc_dec_mask = self.get_batch( + data_iterator + ) + # Forward model lm_labels + output_tensor = model( + tokens_enc, tokens_dec, enc_mask, dec_mask, enc_dec_mask, tokentype_ids=None, lm_labels=lm_labels + ) + + return output_tensor, partial(self.loss_func, loss_mask) + + return forward_step + + +def finish_mpu_init(): + # torch.distributed initialization + args = get_args() + # Pytorch distributed. + _initialize_distributed(None, None, None) + + # Random seeds for reproducibility. + if args.rank == 0: + print(f"> setting random seeds to {args.seed} ...") + _set_random_seed(args.seed, args.data_parallel_random_init) + + +# initialize megatron setup +def initialize(accelerator, extra_args_provider=None, args_defaults=None): + if args_defaults is None: + args_defaults = {} + accelerator.print("Initializing Megatron-LM") + assert torch.cuda.is_available(), "Megatron requires CUDA." + + # Parse arguments + args = parse_args(extra_args_provider, ignore_unknown_args=True) + + # Set defaults + for key, value in args_defaults.items(): + if getattr(args, key, None) is not None: + if args.rank == 0: + print( + f"WARNING: overriding default arguments for {key}:{getattr(args, key)} with {key}:{value}", + flush=True, + ) + setattr(args, key, value) + + if args.use_checkpoint_args or args_defaults.get("use_checkpoint_args", False): + assert args.load is not None, "--use-checkpoints-args requires --load argument" + load_args_from_checkpoint(args) + + validate_args(args) + + # set global args, build tokenizer, and set adlr-autoresume, + # tensorboard-writer, and timers. + set_global_variables(args, build_tokenizer=False) + + # Megatron's MPU is the master. Complete initialization right away. + finish_mpu_init() + + # Autoresume. + _init_autoresume() + + # Compile dependencies. + _compile_dependencies() + + # Set pytorch JIT layer fusion options and warmup JIT functions. + set_jit_fusion_options() + args = get_args() + if getattr(args, "padded_vocab_size", None) is None: + args.padded_vocab_size = _vocab_size_with_padding(args.orig_vocab_size, args) + if args.model_type_name == "bert" and args.pretraining_flag and args.num_labels == 2: + args.bert_binary_head = True + else: + args.bert_binary_head = False + args.iteration = 0 + + +class MegatronEngine(torch.nn.Module): + """ + Megatron-LM model wrapper + + Args: + accelerator (:class:`~accelerate.Accelerator`): The accelerator object to use. + model: Megatron-LM model + optimizer: Megatron-LM optimizer + lr_scheduler: Megatron-LM lr scheduler + """ + + def __init__(self, accelerator, model, optimizer, scheduler): + super().__init__() + self.module = model + self.base_model = model[0] + self.optimizer = optimizer + self.scheduler = scheduler + args = get_args() + if accelerator.state.megatron_lm_plugin.custom_train_step_class is not None: + self.train_step_handler = accelerator.state.megatron_lm_plugin.custom_train_step_class( + args, **accelerator.state.megatron_lm_plugin.custom_train_step_kwargs + ) + elif args.model_type_name == "bert": + self.train_step_handler = BertTrainStep(accelerator, args) + elif args.model_type_name == "gpt": + self.train_step_handler = GPTTrainStep(accelerator, args) + elif args.model_type_name == "t5": + self.train_step_handler = T5TrainStep(accelerator, args) + else: + raise ValueError(f"Unsupported model type: {args.model_type_name}") + self.optimizer.skipped_iter = False + + # Tracking loss. + self.total_loss_dict = {} + self.eval_total_loss_dict = {} + self.iteration = 0 + self.report_memory_flag = True + self.num_floating_point_operations_so_far = 0 + self.module_config = None + if args.tensorboard_dir is not None: + write_args_to_tensorboard() + + def get_module_config(self): + args = get_args() + config = get_model_config(self.module[0]) + # Setup some training config params + config.grad_scale_func = self.optimizer.scale_loss + if isinstance(self.module[0], LocalDDP) and args.overlap_grad_reduce: + assert config.no_sync_func is None, ( + "When overlap_grad_reduce is True, config.no_sync_func must be None; " + "a custom no_sync_func is not supported when overlapping grad-reduce" + ) + config.no_sync_func = [model_chunk.no_sync for model_chunk in self.module] + if len(self.module) == 1: + config.no_sync_func = config.no_sync_func[0] + if args.delay_grad_reduce: + config.grad_sync_func = [model_chunk.start_grad_sync for model_chunk in self.module] + if len(self.module) == 1: + config.grad_sync_func = config.grad_sync_func[0] + if args.overlap_param_gather and args.delay_param_gather: + config.param_sync_func = [ + lambda x: self.optimizer.finish_param_sync(model_index, x) for model_index in range(len(self.module)) + ] + if len(self.module) == 1: + config.param_sync_func = config.param_sync_func[0] + config.finalize_model_grads_func = finalize_model_grads + return config + + def train(self): + for model_module in self.module: + model_module.train() + + if self.module_config is None: + self.module_config = self.get_module_config() + + self.log_eval_results() + + def eval(self): + for model_module in self.module: + model_module.eval() + + if self.module_config is None: + self.module_config = self.get_module_config() + + def get_batch_data_iterator(self, batch_data): + args = get_args() + data_chunks = [] + if len(batch_data) > 0: + if args.num_micro_batches > 1: + for i in range(0, args.num_micro_batches): + data_chunks.append( + { + k: v[i * args.micro_batch_size : (i + 1) * args.micro_batch_size] + for k, v in batch_data.items() + } + ) + else: + data_chunks = [batch_data] + + if len(self.module) > 1: + batch_data_iterator = ( + [iter(data_chunks) for _ in range(len(self.module))] + if len(batch_data) > 0 + else [None] * len(self.module) + ) + else: + batch_data_iterator = iter(data_chunks) if len(batch_data) > 0 else None + return batch_data_iterator + + def train_step(self, **batch_data): + """ + Training step for Megatron-LM + + Args: + batch_data (:obj:`dict`): The batch data to train on. + """ + + batch_data_iterator = self.get_batch_data_iterator(batch_data) + + loss_reduced, skipped_iter, _, _, _, grad_norm, num_zeros_in_grad = train_step( + forward_step_func=self.train_step_handler.forward_step, + data_iterator=batch_data_iterator, + model=self.module, + optimizer=self.optimizer, + opt_param_scheduler=self.scheduler, + config=self.module_config, + forward_backward_func=get_forward_backward_func(), + ) + + self.optimizer.skipped_iter = skipped_iter == 1 + + return loss_reduced, skipped_iter, grad_norm, num_zeros_in_grad + + def eval_step(self, **batch_data): + """ + Evaluation step for Megatron-LM + + Args: + batch_data (:obj:`dict`): The batch data to evaluate on. + """ + + args = get_args() + batch_data_iterator = self.get_batch_data_iterator(batch_data) + forward_backward_func = get_forward_backward_func() + loss_dicts = forward_backward_func( + forward_step_func=self.train_step_handler.forward_step, + data_iterator=batch_data_iterator, + model=self.module, + num_microbatches=get_num_microbatches(), + seq_length=args.seq_length, + micro_batch_size=args.micro_batch_size, + forward_only=True, + ) + # Empty unused memory + if args.empty_unused_memory_level >= 1: + torch.cuda.empty_cache() + + args.consumed_valid_samples += ( + mpu.get_data_parallel_world_size() * args.micro_batch_size * get_num_microbatches() + ) + + if mpu.is_pipeline_last_stage(ignore_virtual=True): + # Average loss across microbatches. + loss_reduced = {} + for key in loss_dicts[0]: + losses_reduced_for_key = [x[key] for x in loss_dicts] + if len(losses_reduced_for_key[0].shape) == 0: + loss_reduced[key] = sum(losses_reduced_for_key) / len(losses_reduced_for_key) + else: + loss_reduced[key] = torch.concat(losses_reduced_for_key) + return loss_reduced + return {} + + def forward(self, **batch_data): + # During training, we use train_step() + # model(**batch_data) performs following operations by delegating it to `self.train_step`: + # 1. Prepare **batch_data for Tendor, Pipeline and Model Parallelism + # 2. Set grad to zero. + # 3. forward pass and backward pass using Pipeline Parallelism + # 4. Empty unused memory. + # 5. Reduce gradients. + # 6. Update parameters. + # 7. Gather params when using Distributed Optimizer (Data Parallelism). + # 8. Update learning rate if scheduler is specified. + # 9. Empty unused memory. + # 10. Average loss across microbatches and across DP ranks. + # + # During evaluation, we use eval_step() + args = get_args() + if self.module[0].training: + loss_dict, skipped_iter, grad_norm, num_zeros_in_grad = self.train_step(**batch_data) + self.iteration += 1 + batch_size = mpu.get_data_parallel_world_size() * args.micro_batch_size * get_num_microbatches() + args.consumed_train_samples += batch_size + self.num_floating_point_operations_so_far += num_floating_point_operations(args, batch_size) + if args.tensorboard_dir is not None: + # Logging. + loss_scale = self.optimizer.get_loss_scale().item() + params_norm = None + if args.log_params_norm: + params_norm = calc_params_l2_norm(self.model) + self.report_memory_flag = training_log( + loss_dict, + self.total_loss_dict, + self.optimizer.param_groups[0]["lr"], + self.iteration, + loss_scale, + self.report_memory_flag, + skipped_iter, + grad_norm, + params_norm, + num_zeros_in_grad, + ) + else: + loss_dict = self.eval_step(**batch_data) + if args.tensorboard_dir is not None: + for key in loss_dict: + self.eval_total_loss_dict[key] = ( + self.eval_total_loss_dict.get(key, torch.cuda.FloatTensor([0.0])) + loss_dict[key] + ) + self.eval_total_loss_dict[key + "_num_iters"] = self.eval_total_loss_dict.get( + key + "_num_iters", torch.cuda.FloatTensor([0.0]) + ) + torch.cuda.FloatTensor([1.0]) + + loss = torch.tensor(0.0, device=torch.cuda.current_device()) + for key in loss_dict: + if len(loss_dict[key].shape) == 0: + loss += loss_dict[key] + + logits = None + if "logits" in loss_dict: + logits = loss_dict["logits"] + if self.train_step_handler.model_output_class is not None: + return self.train_step_handler.model_output_class(loss=loss, logits=logits) + return loss + + def log_eval_results(self): + args = get_args() + if args.tensorboard_dir is None or self.iteration == 0: + return + args = get_args() + writer = get_tensorboard_writer() + string = f"validation loss at iteration {self.iteration} | " + for key in self.eval_total_loss_dict: + if key.endswith("_num_iters"): + continue + value = self.eval_total_loss_dict[key] / self.eval_total_loss_dict[key + "_num_iters"] + string += f"{key} value: {value} | " + ppl = math.exp(min(20, value.item())) + if args.pretraining_flag: + string += f"{key} PPL: {ppl} | " + if writer: + writer.add_scalar(f"{key} validation", value.item(), self.iteration) + if args.pretraining_flag: + writer.add_scalar(f"{key} validation ppl", ppl, self.iteration) + + length = len(string) + 1 + print_rank_last("-" * length) + print_rank_last(string) + print_rank_last("-" * length) + self.eval_total_loss_dict = {} + + def save_checkpoint(self, output_dir): + self.log_eval_results() + args = get_args() + args.save = output_dir + torch.distributed.barrier() + save_checkpoint( + self.iteration, + self.module, + self.optimizer, + self.scheduler, + num_floating_point_operations_so_far=self.num_floating_point_operations_so_far, + ) + torch.distributed.barrier() + + def load_checkpoint(self, input_dir): + args = get_args() + args.load = input_dir + args.consumed_train_samples = 0 + args.consumed_valid_samples = 0 + torch.distributed.barrier() + iteration, num_floating_point_operations_so_far = load_checkpoint(self.module, self.optimizer, self.scheduler) + torch.distributed.barrier() + self.iteration = iteration + self.num_floating_point_operations_so_far = num_floating_point_operations_so_far + if args.fp16 and self.iteration == 0: + self.optimizer.reload_model_params() + + +# other utilities +def avg_losses_across_data_parallel_group(losses): + """ + Average losses across data parallel group. + + Args: + losses (List[Tensor]): List of losses to average across data parallel group. + """ + + return average_losses_across_data_parallel_group(losses) + + +def gather_across_data_parallel_groups(tensor): + """ + Recursively gather tensor in a nested list/tuple/dictionary of tensors from data parallel ranks. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to gather across data parallel ranks. + + """ + + def _gpu_gather_one(tensor): + if tensor.ndim == 0: + tensor = tensor.clone()[None] + output_tensors = [ + torch.empty_like(tensor) + for _ in range(torch.distributed.get_world_size(group=mpu.get_data_parallel_group())) + ] + torch.distributed.all_gather(output_tensors, tensor, group=mpu.get_data_parallel_group()) + return torch.cat(output_tensors, dim=0) + + return recursively_apply(_gpu_gather_one, tensor, error_on_other_type=True) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/memory.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..f12d96fd188b70236e00302ed1916f510f0113db --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/memory.py @@ -0,0 +1,184 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +A collection of utilities for ensuring that training can always occur. Heavily influenced by the +[toma](https://github.com/BlackHC/toma) library. +""" + +import functools +import gc +import inspect +from typing import Optional + +import torch + +from .imports import ( + is_cuda_available, + is_hpu_available, + is_mlu_available, + is_mps_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_xpu_available, +) + + +def clear_device_cache(garbage_collection=False): + """ + Clears the device cache by calling `torch.{backend}.empty_cache`. Can also run `gc.collect()`, but do note that + this is a *considerable* slowdown and should be used sparingly. + """ + if garbage_collection: + gc.collect() + + if is_xpu_available(): + torch.xpu.empty_cache() + elif is_mlu_available(): + torch.mlu.empty_cache() + elif is_sdaa_available(): + torch.sdaa.empty_cache() + elif is_musa_available(): + torch.musa.empty_cache() + elif is_npu_available(): + torch.npu.empty_cache() + elif is_mps_available(min_version="2.0"): + torch.mps.empty_cache() + elif is_cuda_available(): + torch.cuda.empty_cache() + elif is_hpu_available(): + # torch.hpu.empty_cache() # not available on hpu as it reserves all device memory for the current process + pass + elif is_neuron_available(): + # Not sure it actually does something, but adding for consistency with other backends + torch.neuron.empty_cache() + + +def release_memory(*objects): + """ + Releases memory from `objects` by setting them to `None` and calls `gc.collect()` and `torch.cuda.empty_cache()`. + Returned objects should be reassigned to the same variables. + + Args: + objects (`Iterable`): + An iterable of objects + Returns: + A list of `None` objects to replace `objects` + + Example: + + ```python + >>> import torch + >>> from accelerate.utils import release_memory + + >>> a = torch.ones(1000, 1000).cuda() + >>> b = torch.ones(1000, 1000).cuda() + >>> a, b = release_memory(a, b) + ``` + """ + if not isinstance(objects, list): + objects = list(objects) + for i in range(len(objects)): + objects[i] = None + clear_device_cache(garbage_collection=True) + return objects + + +def should_reduce_batch_size(exception: Exception) -> bool: + """ + Checks if `exception` relates to CUDA out-of-memory, XPU out-of-memory, CUDNN not supported, or CPU out-of-memory + + Args: + exception (`Exception`): + An exception + """ + _statements = [ + " out of memory.", # OOM for CUDA, HIP, XPU + "cuDNN error: CUDNN_STATUS_NOT_SUPPORTED.", # CUDNN SNAFU + "DefaultCPUAllocator: can't allocate memory", # CPU OOM + "FATAL ERROR :: MODULE:PT_DEVMEM Allocation failed", # HPU OOM + ] + if isinstance(exception, RuntimeError) and len(exception.args) == 1: + return any(err in exception.args[0] for err in _statements) + return False + + +def find_executable_batch_size( + function: Optional[callable] = None, + starting_batch_size: int = 128, + reduce_batch_size_fn: Optional[callable] = None, +): + """ + A basic decorator that will try to execute `function`. If it fails from exceptions related to out-of-memory or + CUDNN, the batch size is multiplied by 0.9 and passed to `function` + + `function` must take in a `batch_size` parameter as its first argument. + + Args: + function (`callable`, *optional*): + A function to wrap + starting_batch_size (`int`, *optional*): + The batch size to try and fit into memory + + Example: + + ```python + >>> from accelerate.utils import find_executable_batch_size + + + >>> @find_executable_batch_size(starting_batch_size=128) + ... def train(batch_size, model, optimizer): + ... ... + + + >>> train(model, optimizer) + ``` + """ + if function is None: + return functools.partial(find_executable_batch_size, starting_batch_size=starting_batch_size) + + batch_size = starting_batch_size + if reduce_batch_size_fn is None: + + def reduce_batch_size_fn(): + nonlocal batch_size + batch_size = int(batch_size * 0.9) + return batch_size + + def decorator(*args, **kwargs): + nonlocal batch_size + clear_device_cache(garbage_collection=True) + params = list(inspect.signature(function).parameters.keys()) + # Guard against user error + if len(params) < (len(args) + 1): + arg_str = ", ".join([f"{arg}={value}" for arg, value in zip(params[1:], args[1:])]) + raise TypeError( + f"Batch size was passed into `{function.__name__}` as the first argument when called." + f"Remove this as the decorator already does so: `{function.__name__}({arg_str})`" + ) + while True: + if batch_size == 0: + raise RuntimeError("No executable batch size found, reached zero.") + try: + return function(batch_size, *args, **kwargs) + except Exception as e: + if should_reduce_batch_size(e): + clear_device_cache(garbage_collection=True) + batch_size = reduce_batch_size_fn() + else: + raise + + return decorator diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/modeling.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/modeling.py new file mode 100644 index 0000000000000000000000000000000000000000..7bfe1ec8e6738d4be8ac4f95dbf359cca13be02b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/modeling.py @@ -0,0 +1,2187 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import contextlib +import gc +import inspect +import json +import logging +import os +import re +import shutil +import tempfile +import warnings +from collections import OrderedDict, defaultdict +from typing import Optional, Union + +import torch +from torch import distributed as dist +from torch import nn + +from ..state import AcceleratorState +from .constants import SAFE_WEIGHTS_NAME, WEIGHTS_NAME +from .dataclasses import AutocastKwargs, CustomDtype, DistributedType +from .imports import ( + is_hpu_available, + is_mlu_available, + is_mps_available, + is_musa_available, + is_npu_available, + is_peft_available, + is_sdaa_available, + is_torch_xla_available, + is_xpu_available, +) +from .memory import clear_device_cache +from .offload import load_offloaded_weight, offload_weight, save_offload_index +from .tqdm import is_tqdm_available, tqdm +from .versions import is_torch_version + + +if is_npu_available(check_device=False): + import torch_npu # noqa: F401 + +if is_mlu_available(check_device=False): + import torch_mlu # noqa: F401 + +if is_sdaa_available(check_device=False): + import torch_sdaa # noqa: F401 + +if is_musa_available(check_device=False): + import torch_musa # noqa: F401 + +from safetensors import safe_open +from safetensors.torch import load_file as safe_load_file + + +WEIGHTS_INDEX_NAME = "pytorch_model.bin.index.json" + +logger = logging.getLogger(__name__) + + +def is_peft_model(model): + from .other import extract_model_from_parallel + + if is_peft_available(): + from peft import PeftModel + + return is_peft_available() and isinstance(extract_model_from_parallel(model), PeftModel) + + +def check_device_same(first_device, second_device): + """ + Utility method to check if two `torch` devices are similar. When dealing torch accelerator devices(e.g. cuda, xpu), + torch throws `False` for `torch.device("cuda") == torch.device("cuda:0")` whereas they should be the same + + Args: + first_device (`torch.device`): + First device to check + second_device (`torch.device`): + Second device to check + """ + if first_device.type != second_device.type: + return False + + if first_device.type != "cpu" and first_device.index is None: + # In case the first_device is an torch accelerator device(e.g. cuda, xpu) and have + # the index attribute set to `None`, default it to `0` + first_device = torch.device(first_device.type, index=0) + + if second_device.type != "cpu" and second_device.index is None: + # In case the second_device is an torch accelerator device(e.g. cuda, xpu) and have + # the index attribute set to `None`, default it to `0` + second_device = torch.device(second_device.type, index=0) + + return first_device == second_device + + +def convert_file_size_to_int(size: Union[int, str]): + """ + Converts a size expressed as a string with digits an unit (like `"5MB"`) to an integer (in bytes). + + Args: + size (`int` or `str`): The size to convert. Will be directly returned if an `int`. + + Example: + + ```py + >>> convert_file_size_to_int("1MiB") + 1048576 + ``` + """ + mem_size = -1 + err_msg = ( + f"`size` {size} is not in a valid format. Use an integer for bytes, or a string with an unit (like '5.0GB')." + ) + try: + if isinstance(size, int): + mem_size = size + elif size.upper().endswith("GIB"): + mem_size = int(float(size[:-3]) * (2**30)) + elif size.upper().endswith("MIB"): + mem_size = int(float(size[:-3]) * (2**20)) + elif size.upper().endswith("KIB"): + mem_size = int(float(size[:-3]) * (2**10)) + elif size.upper().endswith("GB"): + int_size = int(float(size[:-2]) * (10**9)) + mem_size = int_size // 8 if size.endswith("b") else int_size + elif size.upper().endswith("MB"): + int_size = int(float(size[:-2]) * (10**6)) + mem_size = int_size // 8 if size.endswith("b") else int_size + elif size.upper().endswith("KB"): + int_size = int(float(size[:-2]) * (10**3)) + mem_size = int_size // 8 if size.endswith("b") else int_size + except ValueError: + raise ValueError(err_msg) + + if mem_size < 0: + raise ValueError(err_msg) + return mem_size + + +def dtype_byte_size(dtype: torch.dtype): + """ + Returns the size (in bytes) occupied by one parameter of type `dtype`. + + Example: + + ```py + >>> dtype_byte_size(torch.float32) + 4 + ``` + """ + if dtype == torch.bool: + return 1 / 8 + elif dtype == CustomDtype.INT2: + return 1 / 4 + elif dtype == CustomDtype.INT4: + return 1 / 2 + elif dtype == CustomDtype.FP8: + return 1 + elif is_torch_version(">=", "2.1.0") and dtype in [torch.float8_e4m3fn, torch.float8_e5m2]: + return 1 + bit_search = re.search(r"[^\d](\d+)$", str(dtype)) + if bit_search is None: + raise ValueError(f"`dtype` is not a valid dtype: {dtype}.") + bit_size = int(bit_search.groups()[0]) + return bit_size // 8 + + +def id_tensor_storage(tensor: torch.Tensor) -> tuple[torch.device, int, int]: + """ + Unique identifier to a tensor storage. Multiple different tensors can share the same underlying storage. For + example, "meta" tensors all share the same storage, and thus their identifier will all be equal. This identifier is + guaranteed to be unique and constant for this tensor's storage during its lifetime. Two tensor storages with + non-overlapping lifetimes may have the same id. + """ + _SIZE = { + torch.int64: 8, + torch.float32: 4, + torch.int32: 4, + torch.bfloat16: 2, + torch.float16: 2, + torch.int16: 2, + torch.uint8: 1, + torch.int8: 1, + torch.bool: 1, + torch.float64: 8, + } + try: + storage_ptr = tensor.untyped_storage().data_ptr() + storage_size = tensor.untyped_storage().nbytes() + except Exception: + try: + # Fallback for torch==1.10 + storage_ptr = tensor.storage().data_ptr() + storage_size = tensor.storage().size() * _SIZE[tensor.dtype] + except NotImplementedError: + # Fallback for meta storage + storage_ptr = 0 + # On torch >=2.0 this is the tensor size + storage_size = tensor.nelement() * _SIZE[tensor.dtype] + + return tensor.device, storage_ptr, storage_size + + +def set_module_tensor_to_device( + module: nn.Module, + tensor_name: str, + device: Union[int, str, torch.device], + value: Optional[torch.Tensor] = None, + dtype: Optional[Union[str, torch.dtype]] = None, + fp16_statistics: Optional[torch.HalfTensor] = None, + tied_params_map: Optional[dict[int, dict[torch.device, torch.Tensor]]] = None, + non_blocking: bool = False, + clear_cache: bool = True, +): + """ + A helper function to set a given tensor (parameter of buffer) of a module on a specific device (note that doing + `param.to(device)` creates a new tensor not linked to the parameter, which is why we need this function). + + Args: + module (`torch.nn.Module`): + The module in which the tensor we want to move lives. + tensor_name (`str`): + The full name of the parameter/buffer. + device (`int`, `str` or `torch.device`): + The device on which to set the tensor. + value (`torch.Tensor`, *optional*): + The value of the tensor (useful when going from the meta device to any other device). + dtype (`torch.dtype`, *optional*): + If passed along the value of the parameter will be cast to this `dtype`. Otherwise, `value` will be cast to + the dtype of the existing parameter in the model. + fp16_statistics (`torch.HalfTensor`, *optional*): + The list of fp16 statistics to set on the module, used for 8 bit model serialization. + tied_params_map (Dict[int, Dict[torch.device, torch.Tensor]], *optional*, defaults to `None`): + A map of current data pointers to dictionaries of devices to already dispatched tied weights. For a given + execution device, this parameter is useful to reuse the first available pointer of a shared weight on the + device for all others, instead of duplicating memory. + non_blocking (`bool`, *optional*, defaults to `False`): + If `True`, the device transfer will be asynchronous with respect to the host, if possible. + clear_cache (`bool`, *optional*, defaults to `True`): + Whether or not to clear the device cache after setting the tensor on the device. + """ + # Recurse if needed + if "." in tensor_name: + splits = tensor_name.split(".") + for split in splits[:-1]: + new_module = getattr(module, split) + if new_module is None: + raise ValueError(f"{module} has no attribute {split}.") + module = new_module + tensor_name = splits[-1] + + if tensor_name not in module._parameters and tensor_name not in module._buffers: + raise ValueError(f"{module} does not have a parameter or a buffer named {tensor_name}.") + is_buffer = tensor_name in module._buffers + old_value = getattr(module, tensor_name) + + # Treat the case where old_value (or a custom `value`, typically offloaded to RAM/disk) belongs to a tied group, and one of the weight + # in the tied group has already been dispatched to the device, by avoiding reallocating memory on the device and just copying the pointer. + if ( + value is not None + and tied_params_map is not None + and value.data_ptr() in tied_params_map + and device in tied_params_map[value.data_ptr()] + ): + module._parameters[tensor_name] = tied_params_map[value.data_ptr()][device] + return + elif ( + tied_params_map is not None + and old_value.data_ptr() in tied_params_map + and device in tied_params_map[old_value.data_ptr()] + ): + module._parameters[tensor_name] = tied_params_map[old_value.data_ptr()][device] + return + + if old_value.device == torch.device("meta") and device not in ["meta", torch.device("meta")] and value is None: + raise ValueError(f"{tensor_name} is on the meta device, we need a `value` to put in on {device}.") + + param = module._parameters[tensor_name] if tensor_name in module._parameters else None + param_cls = type(param) + + if value is not None: + # We can expect mismatches when using bnb 4bit since Params4bit will reshape and pack the weights. + # In other cases, we want to make sure we're not loading checkpoints that do not match the config. + if old_value.shape != value.shape and param_cls.__name__ != "Params4bit": + raise ValueError( + f'Trying to set a tensor of shape {value.shape} in "{tensor_name}" (which has shape {old_value.shape}), this looks incorrect.' + ) + + if dtype is None: + # For compatibility with PyTorch load_state_dict which converts state dict dtype to existing dtype in model + value = value.to(old_value.dtype, non_blocking=non_blocking) + elif not str(value.dtype).startswith(("torch.uint", "torch.int", "torch.bool")): + value = value.to(dtype, non_blocking=non_blocking) + + device_quantization = None + with torch.no_grad(): + # leave it on cpu first before moving them to device + # # fix the case where the device is meta, we don't want to put it on cpu because there is no data =0 + if ( + param is not None + and param.device.type not in ("cuda", "xpu") + and torch.device(device).type in ("cuda", "xpu") + and param_cls.__name__ in ["Int8Params", "FP4Params", "Params4bit"] + ): + device_quantization = device + device = "cpu" + # `torch.Tensor.to()` is not supported by `torch_npu` (see this [issue](https://github.com/Ascend/pytorch/issues/16)). + if isinstance(device, int): + if is_npu_available(): + device = f"npu:{device}" + elif is_mlu_available(): + device = f"mlu:{device}" + elif is_sdaa_available(): + device = f"sdaa:{device}" + elif is_musa_available(): + device = f"musa:{device}" + elif is_hpu_available(): + device = "hpu" + if "xpu" in str(device) and not is_xpu_available(): + raise ValueError(f'{device} is not available, you should use device="cpu" instead') + if value is None: + new_value = old_value.to(device, non_blocking=non_blocking) + if dtype is not None and device in ["meta", torch.device("meta")]: + if not str(old_value.dtype).startswith(("torch.uint", "torch.int", "torch.bool")): + new_value = new_value.to(dtype, non_blocking=non_blocking) + + if not is_buffer: + module._parameters[tensor_name] = param_cls(new_value, requires_grad=old_value.requires_grad) + elif isinstance(value, torch.Tensor): + new_value = value.to(device, non_blocking=non_blocking) + else: + new_value = torch.tensor(value, device=device) + if device_quantization is not None: + device = device_quantization + if is_buffer: + module._buffers[tensor_name] = new_value + elif value is not None or not check_device_same(torch.device(device), module._parameters[tensor_name].device): + param_cls = type(module._parameters[tensor_name]) + kwargs = module._parameters[tensor_name].__dict__ + if param_cls.__name__ in ["Int8Params", "FP4Params", "Params4bit"]: + if param_cls.__name__ == "Int8Params" and new_value.dtype == torch.float32: + # downcast to fp16 if any - needed for 8bit serialization + new_value = new_value.to(torch.float16, non_blocking=non_blocking) + # quantize module that are going to stay on the cpu so that we offload quantized weights + if device == "cpu" and param_cls.__name__ == "Int8Params": + new_value = param_cls(new_value, requires_grad=old_value.requires_grad, **kwargs).to(0).to("cpu") + new_value.CB = new_value.CB.to("cpu") + new_value.SCB = new_value.SCB.to("cpu") + else: + new_value = param_cls(new_value, requires_grad=old_value.requires_grad, **kwargs).to( + device, non_blocking=non_blocking + ) + elif param_cls.__name__ in ["QTensor", "QBitsTensor"]: + new_value = torch.nn.Parameter(new_value, requires_grad=old_value.requires_grad).to( + device, non_blocking=non_blocking + ) + elif param_cls.__name__ in ["AffineQuantizedTensor"]: + new_value = new_value.to(device, non_blocking=non_blocking) + else: + new_value = param_cls(new_value, requires_grad=old_value.requires_grad).to( + device, non_blocking=non_blocking + ) + + module._parameters[tensor_name] = new_value + if fp16_statistics is not None: + module._parameters[tensor_name].SCB = fp16_statistics.to(device, non_blocking=non_blocking) + del fp16_statistics + # as we put the weight to meta, it doesn't have SCB attr anymore. make sure that it is not a meta weight + if ( + module.__class__.__name__ == "Linear8bitLt" + and getattr(module.weight, "SCB", None) is None + and str(module.weight.device) != "meta" + ): + # quantize only if necessary + device_index = torch.device(device).index if torch.device(device).type in ["cuda", "xpu"] else None + if not getattr(module.weight, "SCB", None) and device_index is not None: + if module.bias is not None and module.bias.device.type != "meta": + # if a bias exists, we need to wait until the bias is set on the correct device + module = module.to(device_index) + elif module.bias is None: + # if no bias exists, we can quantize right away + module = module.to(device_index) + elif ( + module.__class__.__name__ == "Linear4bit" + and getattr(module.weight, "quant_state", None) is None + and str(module.weight.device) != "meta" + ): + # quantize only if necessary + device_index = torch.device(device).index if torch.device(device).type in ["cuda", "xpu"] else None + if not getattr(module.weight, "quant_state", None) and device_index is not None: + module.weight = module.weight.to(device_index) + + # clean pre and post forward hook + if clear_cache and device not in ("cpu", "meta"): + clear_device_cache() + + # When handling tied weights, we update tied_params_map to keep track of the tied weights that have already been allocated on the device in + # order to avoid duplicating memory, see above. + if ( + tied_params_map is not None + and old_value.data_ptr() in tied_params_map + and device not in tied_params_map[old_value.data_ptr()] + ): + tied_params_map[old_value.data_ptr()][device] = new_value + elif ( + value is not None + and tied_params_map is not None + and value.data_ptr() in tied_params_map + and device not in tied_params_map[value.data_ptr()] + ): + tied_params_map[value.data_ptr()][device] = new_value + + +def named_module_tensors( + module: nn.Module, include_buffers: bool = True, recurse: bool = False, remove_non_persistent: bool = False +): + """ + A helper function that gathers all the tensors (parameters + buffers) of a given module. If `include_buffers=True` + it's the same as doing `module.named_parameters(recurse=recurse) + module.named_buffers(recurse=recurse)`. + + Args: + module (`torch.nn.Module`): + The module we want the tensors on. + include_buffer (`bool`, *optional*, defaults to `True`): + Whether or not to include the buffers in the result. + recurse (`bool`, *optional`, defaults to `False`): + Whether or not to go look in every submodule or just return the direct parameters and buffers. + remove_non_persistent (`bool`, *optional*, defaults to `False`): + Whether or not to remove the non persistent buffer from the buffers. Useful only when include_buffers = + True + """ + yield from module.named_parameters(recurse=recurse) + + if include_buffers: + non_persistent_buffers = set() + if remove_non_persistent: + non_persistent_buffers = get_non_persistent_buffers(module, recurse=recurse) + for named_buffer in module.named_buffers(recurse=recurse): + name, _ = named_buffer + if name not in non_persistent_buffers: + yield named_buffer + + +def get_non_persistent_buffers(module: nn.Module, recurse: bool = False, fqns: bool = False): + """ + Gather all non persistent buffers of a given modules into a set + + Args: + module (`nn.Module`): + The module we want the non persistent buffers on. + recurse (`bool`, *optional*, defaults to `False`): + Whether or not to go look in every submodule or just return the direct non persistent buffers. + fqns (`bool`, *optional*, defaults to `False`): + Whether or not to return the fully-qualified names of the non persistent buffers. + """ + + non_persistent_buffers_set = module._non_persistent_buffers_set + if recurse: + for n, m in module.named_modules(): + if fqns: + non_persistent_buffers_set |= {n + "." + b for b in m._non_persistent_buffers_set} + else: + non_persistent_buffers_set |= m._non_persistent_buffers_set + + return non_persistent_buffers_set + + +def check_tied_parameters_in_config(model: nn.Module): + """ + Check if there is any indication in the given model that some weights should be tied. + + Args: + model (`torch.nn.Module`): The model to inspect + + Returns: + bool: True if the model needs to have tied weights + """ + + # based on model.tie_weights() method + has_tied_word_embedding = False + has_tied_encoder_decoder = False + has_tied_module = False + + if "PreTrainedModel" in [c.__name__ for c in inspect.getmro(model.__class__)]: + has_tied_word_embedding = False + model_decoder_config = None + if hasattr(model, "config"): + model_decoder_config = ( + model.config.get_text_config(decoder=True) + if hasattr(model.config, "get_text_config") + else model.config + ) + has_tied_word_embedding = ( + model_decoder_config is not None + and getattr(model_decoder_config, "tie_word_embeddings", False) + and model.get_output_embeddings() + ) + + has_tied_encoder_decoder = ( + hasattr(model, "config") + and getattr(model.config, "is_encoder_decoder", False) + and getattr(model.config, "tie_encoder_decoder", False) + ) + has_tied_module = any(hasattr(module, "_tie_weights") for module in model.modules()) + return any([has_tied_word_embedding, has_tied_encoder_decoder, has_tied_module]) + + +def _get_param_device(param, device_map): + if param in device_map: + return device_map[param] + parent_param = ".".join(param.split(".")[:-1]) + if parent_param == param: + raise ValueError(f"The `device_map` does not contain the module {param}.") + else: + return _get_param_device(parent_param, device_map) + + +def check_tied_parameters_on_same_device(tied_params, device_map): + """ + Check if tied parameters are on the same device + + Args: + tied_params (`List[List[str]]`): + A list of lists of parameter names being all tied together. + + device_map (`Dict[str, Union[int, str, torch.device]]`): + A map that specifies where each submodule should go. + + """ + for tie_param in tied_params: + tie_param_devices = {} + for param in tie_param: + tie_param_devices[param] = _get_param_device(param, device_map) + if len(set(tie_param_devices.values())) > 1: + logger.warning( + f"Tied parameters are on different devices: {tie_param_devices}. " + "Please modify your custom device map or set `device_map='auto'`. " + ) + + +def find_tied_parameters(model: torch.nn.Module, **kwargs) -> list[list[str]]: + """ + Find the tied parameters in a given model. + + + + The signature accepts keyword arguments, but they are for the recursive part of this function and you should ignore + them. + + + + Args: + model (`torch.nn.Module`): The model to inspect. + + Returns: + List[List[str]]: A list of lists of parameter names being all tied together. + + Example: + + ```py + >>> from collections import OrderedDict + >>> import torch.nn as nn + + >>> model = nn.Sequential(OrderedDict([("linear1", nn.Linear(4, 4)), ("linear2", nn.Linear(4, 4))])) + >>> model.linear2.weight = model.linear1.weight + >>> find_tied_parameters(model) + [['linear1.weight', 'linear2.weight']] + ``` + """ + + # get ALL model parameters and their names + all_named_parameters = {name: param for name, param in model.named_parameters(remove_duplicate=False)} + + # get ONLY unique named parameters, + # if parameter is tied and have multiple names, it will be included only once + no_duplicate_named_parameters = {name: param for name, param in model.named_parameters(remove_duplicate=True)} + + # the difference of the two sets will give us the tied parameters + tied_param_names = set(all_named_parameters.keys()) - set(no_duplicate_named_parameters.keys()) + + # 'tied_param_names' contains the names of parameters that are tied in the model, but we do not know + # which names refer to the same parameter. To identify this, we need to group them together. + tied_param_groups = {} + for tied_param_name in tied_param_names: + tied_param = all_named_parameters[tied_param_name] + for param_name, param in no_duplicate_named_parameters.items(): + # compare if parameters are the same, if so, group their names together + if param is tied_param: + if param_name not in tied_param_groups: + tied_param_groups[param_name] = [] + tied_param_groups[param_name].append(tied_param_name) + + return [sorted([weight] + list(set(tied))) for weight, tied in tied_param_groups.items()] + + +def retie_parameters(model, tied_params): + """ + Reties tied parameters in a given model if the link was broken (for instance when adding hooks). + + Args: + model (`torch.nn.Module`): + The model in which to retie parameters. + tied_params (`List[List[str]]`): + A mapping parameter name to tied parameter name as obtained by `find_tied_parameters`. + """ + for tied_group in tied_params: + param_to_tie = None + # two loops : the first one to set param_to_tie , the second one to change the values of tied_group + for param_name in tied_group: + module = model + splits = param_name.split(".") + for split in splits[:-1]: + module = getattr(module, split) + param = getattr(module, splits[-1]) + if param_to_tie is None and param.device != torch.device("meta"): + param_to_tie = param + break + if param_to_tie is not None: + for param_name in tied_group: + module = model + splits = param_name.split(".") + for split in splits[:-1]: + module = getattr(module, split) + setattr(module, splits[-1], param_to_tie) + + +def _get_proper_dtype(dtype: Union[str, torch.device]) -> torch.dtype: + """ + Just does torch.dtype(dtype) if necessary. + """ + if isinstance(dtype, str): + # We accept "torch.float16" or just "float16" + dtype = dtype.replace("torch.", "") + dtype = getattr(torch, dtype) + return dtype + + +def compute_module_sizes( + model: nn.Module, + dtype: Optional[Union[str, torch.device]] = None, + special_dtypes: Optional[dict[str, Union[str, torch.device]]] = None, + buffers_only: bool = False, +): + """ + Compute the size of each submodule of a given model. + """ + if dtype is not None: + dtype = _get_proper_dtype(dtype) + dtype_size = dtype_byte_size(dtype) + if special_dtypes is not None: + special_dtypes = {key: _get_proper_dtype(dtyp) for key, dtyp in special_dtypes.items()} + special_dtypes_size = {key: dtype_byte_size(dtyp) for key, dtyp in special_dtypes.items()} + module_sizes = defaultdict(int) + + module_list = [] + + if not buffers_only: + module_list = named_module_tensors(model, recurse=True) + else: + module_list = model.named_buffers(recurse=True) + + for name, tensor in module_list: + if special_dtypes is not None and name in special_dtypes: + size = tensor.numel() * special_dtypes_size[name] + elif dtype is None: + size = tensor.numel() * dtype_byte_size(tensor.dtype) + elif str(tensor.dtype).startswith(("torch.uint", "torch.int", "torch.bool")): + # According to the code in set_module_tensor_to_device, these types won't be converted + # so use their original size here + size = tensor.numel() * dtype_byte_size(tensor.dtype) + else: + size = tensor.numel() * min(dtype_size, dtype_byte_size(tensor.dtype)) + name_parts = name.split(".") + for idx in range(len(name_parts) + 1): + module_sizes[".".join(name_parts[:idx])] += size + + return module_sizes + + +def compute_module_total_buffer_size( + model: nn.Module, + dtype: Optional[Union[str, torch.device]] = None, + special_dtypes: Optional[dict[str, Union[str, torch.device]]] = None, +): + """ + Compute the total size of buffers in each submodule of a given model. + """ + module_sizes = compute_module_sizes(model, dtype=dtype, special_dtypes=special_dtypes, buffers_only=True) + return module_sizes.get("", 0) + + +def get_max_layer_size( + modules: list[tuple[str, torch.nn.Module]], module_sizes: dict[str, int], no_split_module_classes: list[str] +): + """ + Utility function that will scan a list of named modules and return the maximum size used by one full layer. The + definition of a layer being: + - a module with no direct children (just parameters and buffers) + - a module whose class name is in the list `no_split_module_classes` + + Args: + modules (`List[Tuple[str, torch.nn.Module]]`): + The list of named modules where we want to determine the maximum layer size. + module_sizes (`Dict[str, int]`): + A dictionary mapping each layer name to its size (as generated by `compute_module_sizes`). + no_split_module_classes (`List[str]`): + A list of class names for layers we don't want to be split. + + Returns: + `Tuple[int, List[str]]`: The maximum size of a layer with the list of layer names realizing that maximum size. + """ + max_size = 0 + layer_names = [] + modules_to_treat = modules.copy() + while len(modules_to_treat) > 0: + module_name, module = modules_to_treat.pop(0) + modules_children = list(module.named_children()) if isinstance(module, torch.nn.Module) else [] + if len(modules_children) == 0 or module.__class__.__name__ in no_split_module_classes: + # No splitting this one so we compare to the max_size + size = module_sizes[module_name] + if size > max_size: + max_size = size + layer_names = [module_name] + elif size == max_size: + layer_names.append(module_name) + else: + modules_to_treat = [(f"{module_name}.{n}", v) for n, v in modules_children] + modules_to_treat + return max_size, layer_names + + +def get_max_memory(max_memory: Optional[dict[Union[int, str], Union[int, str]]] = None): + """ + Get the maximum memory available if nothing is passed, converts string to int otherwise. + """ + import psutil + + if max_memory is None: + max_memory = {} + # Make sure device is initialized on each device to have the right memory info. + if is_npu_available(): + for i in range(torch.npu.device_count()): + try: + _ = torch.tensor(0, device=torch.device("npu", i)) + max_memory[i] = torch.npu.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + elif is_mlu_available(): + for i in range(torch.mlu.device_count()): + try: + _ = torch.tensor(0, device=torch.device("mlu", i)) + max_memory[i] = torch.mlu.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + elif is_sdaa_available(): + for i in range(torch.sdaa.device_count()): + try: + _ = torch.tensor(0, device=torch.device("sdaa", i)) + max_memory[i] = torch.sdaa.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + elif is_musa_available(): + for i in range(torch.musa.device_count()): + try: + _ = torch.tensor(0, device=torch.device("musa", i)) + max_memory[i] = torch.musa.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + elif is_xpu_available(): + for i in range(torch.xpu.device_count()): + try: + _ = torch.tensor(0, device=torch.device("xpu", i)) + max_memory[i] = torch.xpu.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + elif is_hpu_available(): + for i in range(torch.hpu.device_count()): + try: + _ = torch.tensor(0, device=torch.device("hpu", i)) + max_memory[i] = torch.hpu.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + else: + for i in range(torch.cuda.device_count()): + try: + _ = torch.tensor([0], device=i) + max_memory[i] = torch.cuda.mem_get_info(i)[0] + except Exception: + logger.info(f"Device {i} seems unavailable, Proceeding to check subsequent devices.") + continue + # allocate everything in the mps device as the RAM is shared + if is_mps_available(): + max_memory["mps"] = psutil.virtual_memory().available + else: + max_memory["cpu"] = psutil.virtual_memory().available + return max_memory + + for key in max_memory: + if isinstance(max_memory[key], str): + max_memory[key] = convert_file_size_to_int(max_memory[key]) + + # Need to sort the device by type to make sure that we allocate the gpu first. + # As gpu/npu/xpu are represented by int, we need to sort them first. + gpu_devices = [k for k in max_memory.keys() if isinstance(k, int)] + gpu_devices.sort() + # check if gpu/npu/xpu devices are available and if not, throw a warning + if is_npu_available(): + num_devices = torch.npu.device_count() + elif is_mlu_available(): + num_devices = torch.mlu.device_count() + elif is_sdaa_available(): + num_devices = torch.sdaa.device_count() + elif is_musa_available(): + num_devices = torch.musa.device_count() + elif is_xpu_available(): + num_devices = torch.xpu.device_count() + elif is_hpu_available(): + num_devices = torch.hpu.device_count() + else: + num_devices = torch.cuda.device_count() + for device in gpu_devices: + if device >= num_devices or device < 0: + logger.warning(f"Device {device} is not available, available devices are {list(range(num_devices))}") + # Add the other devices in the preset order if they are available + all_devices = gpu_devices + [k for k in ["mps", "cpu", "disk"] if k in max_memory.keys()] + # Raise an error if a device is not recognized + for k in max_memory.keys(): + if k not in all_devices: + raise ValueError( + f"Device {k} is not recognized, available devices are integers(for GPU/XPU), 'mps', 'cpu' and 'disk'" + ) + max_memory = {k: max_memory[k] for k in all_devices} + + return max_memory + + +def clean_device_map(device_map: dict[str, Union[int, str, torch.device]], module_name: str = ""): + """ + Cleans a device_map by grouping all submodules that go on the same device together. + """ + # Get the value of the current module and if there is only one split across several keys, regroup it. + prefix = "" if module_name == "" else f"{module_name}." + values = [v for k, v in device_map.items() if k.startswith(prefix)] + if len(set(values)) == 1 and len(values) > 1: + for k in [k for k in device_map if k.startswith(prefix)]: + del device_map[k] + device_map[module_name] = values[0] + + # Recurse over the children + children_modules = [k for k in device_map.keys() if k.startswith(prefix) and len(k) > len(module_name)] + idx = len(module_name.split(".")) + 1 if len(module_name) > 0 else 1 + children_modules = set(".".join(k.split(".")[:idx]) for k in children_modules) + for child in children_modules: + clean_device_map(device_map, module_name=child) + + return device_map + + +def load_offloaded_weights(model, index, offload_folder): + """ + Loads the weights from the offload folder into the model. + + Args: + model (`torch.nn.Module`): + The model to load the weights into. + index (`dict`): + A dictionary containing the parameter name and its metadata for each parameter that was offloaded from the + model. + offload_folder (`str`): + The folder where the offloaded weights are stored. + """ + if index is None or len(index) == 0: + # Nothing to do + return + for param_name, metadata in index.items(): + if "SCB" in param_name: + continue + fp16_statistics = None + if "weight" in param_name and param_name.replace("weight", "SCB") in index.keys(): + weight_name = param_name.replace("weight", "SCB") + fp16_statistics = load_offloaded_weight( + os.path.join(offload_folder, f"{weight_name}.dat"), index[weight_name] + ) + tensor_file = os.path.join(offload_folder, f"{param_name}.dat") + weight = load_offloaded_weight(tensor_file, metadata) + set_module_tensor_to_device(model, param_name, "cpu", value=weight, fp16_statistics=fp16_statistics) + + +def get_module_leaves(module_sizes): + module_children = {} + for module in module_sizes: + if module == "" or "." not in module: + continue + parent = module.rsplit(".", 1)[0] + module_children[parent] = module_children.get(parent, 0) + 1 + leaves = [module for module in module_sizes if module_children.get(module, 0) == 0 and module != ""] + return leaves + + +def get_balanced_memory( + model: nn.Module, + max_memory: Optional[dict[Union[int, str], Union[int, str]]] = None, + no_split_module_classes: Optional[list[str]] = None, + dtype: Optional[Union[str, torch.dtype]] = None, + special_dtypes: Optional[dict[str, Union[str, torch.device]]] = None, + low_zero: bool = False, +): + """ + Compute a `max_memory` dictionary for [`infer_auto_device_map`] that will balance the use of each available GPU. + + + + All computation is done analyzing sizes and dtypes of the model parameters. As a result, the model can be on the + meta device (as it would if initialized within the `init_empty_weights` context manager). + + + + Args: + model (`torch.nn.Module`): + The model to analyze. + max_memory (`Dict`, *optional*): + A dictionary device identifier to maximum memory. Will default to the maximum memory available if unset. + Example: `max_memory={0: "1GB"}`. + no_split_module_classes (`List[str]`, *optional*): + A list of layer class names that should never be split across device (for instance any layer that has a + residual connection). + dtype (`str` or `torch.dtype`, *optional*): + If provided, the weights will be converted to that type when loaded. + special_dtypes (`Dict[str, Union[str, torch.device]]`, *optional*): + If provided, special dtypes to consider for some specific weights (will override dtype used as default for + all weights). + low_zero (`bool`, *optional*): + Minimizes the number of weights on GPU 0, which is convenient when it's used for other operations (like the + Transformers generate function). + """ + # Get default / clean up max_memory + user_not_set_max_memory = max_memory is None + max_memory = get_max_memory(max_memory) + + if is_npu_available(): + expected_device_type = "npu" + elif is_mlu_available(): + expected_device_type = "mlu" + elif is_sdaa_available(): + expected_device_type = "sdaa" + elif is_musa_available(): + expected_device_type = "musa" + elif is_xpu_available(): + expected_device_type = "xpu" + elif is_hpu_available(): + expected_device_type = "hpu" + elif is_mps_available(): + expected_device_type = "mps" + else: + expected_device_type = "cuda" + num_devices = len([d for d in max_memory if torch.device(d).type == expected_device_type and max_memory[d] > 0]) + + if num_devices == 0: + return max_memory + + if num_devices == 1: + # We cannot do low_zero on just one GPU, but we will still reserve some memory for the buffer + low_zero = False + # If user just asked us to handle memory usage, we should avoid OOM + if user_not_set_max_memory: + for key in max_memory.keys(): + if isinstance(key, int): + max_memory[key] *= 0.9 # 90% is a good compromise + logger.info( + f"We will use 90% of the memory on device {key} for storing the model, and 10% for the buffer to avoid OOM. " + "You can set `max_memory` in to a higher value to use more memory (at your own risk)." + ) + break # only one device + + module_sizes = compute_module_sizes(model, dtype=dtype, special_dtypes=special_dtypes) + per_gpu = module_sizes[""] // (num_devices - 1 if low_zero else num_devices) + + # We can't just set the memory to model_size // num_devices as it will end being too small: each GPU will get + # slightly less layers and some layers will end up offload at the end. So this function computes a buffer size to + # add which is the biggest of: + # - the size of no split block (if applicable) + # - the mean of the layer sizes + if no_split_module_classes is None: + no_split_module_classes = [] + elif not isinstance(no_split_module_classes, (list, tuple)): + no_split_module_classes = [no_split_module_classes] + + # Identify the size of the no_split_block modules + if len(no_split_module_classes) > 0: + no_split_children = {} + for name, size in module_sizes.items(): + if name == "": + continue + submodule = model + for submodule_name in name.split("."): + submodule = getattr(submodule, submodule_name) + class_name = submodule.__class__.__name__ + if class_name in no_split_module_classes and class_name not in no_split_children: + no_split_children[class_name] = size + + if set(no_split_children.keys()) == set(no_split_module_classes): + break + buffer = max(no_split_children.values()) if len(no_split_children) > 0 else 0 + else: + buffer = 0 + + # Compute mean of final modules. In the first dict of module sizes, leaves are the parameters + leaves = get_module_leaves(module_sizes) + leaves_set = set(leaves) # Convert to set for O(1) membership testing + module_sizes = {n: v for n, v in module_sizes.items() if n not in leaves_set} + # Once removed, leaves are the final modules. + leaves = get_module_leaves(module_sizes) + mean_leaves = int(sum([module_sizes[n] for n in leaves]) / max(len(leaves), 1)) + buffer = int(1.25 * max(buffer, mean_leaves)) + per_gpu += buffer + + # Sorted list of GPUs id (we may have some gpu ids not included in the our max_memory list - let's ignore them) + gpus_idx_list = list( + sorted( + device_id for device_id, device_mem in max_memory.items() if isinstance(device_id, int) and device_mem > 0 + ) + ) + # The last device is left with max_memory just in case the buffer is not enough. + for idx in gpus_idx_list[:-1]: + max_memory[idx] = min(max_memory[0] if low_zero and idx == 0 else per_gpu, max_memory[idx]) + + if low_zero: + min_zero = max(0, module_sizes[""] - sum([max_memory[i] for i in range(1, num_devices)])) + max_memory[0] = min(min_zero, max_memory[0]) + + return max_memory + + +def calculate_maximum_sizes(model: torch.nn.Module): + "Computes the total size of the model and its largest layer" + sizes = compute_module_sizes(model) + # `transformers` models store this information for us + no_split_modules = getattr(model, "_no_split_modules", None) + if no_split_modules is None: + no_split_modules = [] + + modules_to_treat = ( + list(model.named_parameters(recurse=False)) + + list(model.named_children()) + + list(model.named_buffers(recurse=False)) + ) + largest_layer = get_max_layer_size(modules_to_treat, sizes, no_split_modules) + total_size = sizes[""] + return total_size, largest_layer + + +def _init_infer_auto_device_map( + model: nn.Module, + max_memory: Optional[dict[Union[int, str], Union[int, str]]] = None, + no_split_module_classes: Optional[list[str]] = None, + dtype: Optional[Union[str, torch.dtype]] = None, + special_dtypes: Optional[dict[str, Union[str, torch.device]]] = None, +) -> tuple[ + list[Union[int, str]], + dict[Union[int, str], Union[int, str]], + list[Union[int, str]], + list[int], + dict[str, int], + list[list[str]], + list[str], + list[tuple[str, nn.Module]], +]: + """ + Initialize variables required for computing the device map for model allocation. + """ + max_memory = get_max_memory(max_memory) + if no_split_module_classes is None: + no_split_module_classes = [] + elif not isinstance(no_split_module_classes, (list, tuple)): + no_split_module_classes = [no_split_module_classes] + + devices = list(max_memory.keys()) + if "disk" not in devices: + devices.append("disk") + gpus = [device for device in devices if device not in ["cpu", "disk"]] + + # Devices that need to keep space for a potential offloaded layer. + if "mps" in gpus: + main_devices = ["mps"] + elif len(gpus) > 0: + main_devices = [gpus[0], "cpu"] + else: + main_devices = ["cpu"] + + module_sizes = compute_module_sizes(model, dtype=dtype, special_dtypes=special_dtypes) + tied_parameters = find_tied_parameters(model) + if check_tied_parameters_in_config(model) and len(tied_parameters) == 0: + logger.warning( + "The model weights are not tied. Please use the `tie_weights` method before using the `infer_auto_device` function." + ) + + # Direct submodules and parameters + modules_to_treat = ( + list(model.named_parameters(recurse=False)) + + list(model.named_children()) + + list(model.named_buffers(recurse=False)) + ) + + return ( + devices, + max_memory, + main_devices, + gpus, + module_sizes, + tied_parameters, + no_split_module_classes, + modules_to_treat, + ) + + +def get_module_size_with_ties( + tied_params, + module_size, + module_sizes, + modules_to_treat, +) -> tuple[int, list[str], list[nn.Module]]: + """ + Calculate the total size of a module, including its tied parameters. + + Args: + tied_params (`List[str]`): The list of tied parameters. + module_size (`int`): The size of the module without tied parameters. + module_sizes (`Dict[str, int]`): A dictionary mapping each layer name to its size. + modules_to_treat (`List[Tuple[str, nn.Module]]`): The list of named modules to treat. + + Returns: + `Tuple[int, List[str], List[nn.Module]]`: The total size of the module, the names of the tied modules, and the + tied modules. + """ + if len(tied_params) < 1: + return module_size, [], [] + tied_module_names = [] + tied_modules = [] + + for tied_param in tied_params: + tied_module_index = [i for i, (n, _) in enumerate(modules_to_treat) if tied_param.startswith(n + ".")][0] + tied_module_names.append(modules_to_treat[tied_module_index][0]) + tied_modules.append(modules_to_treat[tied_module_index][1]) + + module_size_with_ties = module_size + for tied_param, tied_module_name in zip(tied_params, tied_module_names): + module_size_with_ties += module_sizes[tied_module_name] - module_sizes[tied_param] + + return module_size_with_ties, tied_module_names, tied_modules + + +def fallback_allocate( + modules: list[tuple[str, nn.Module]], + module_sizes: dict[str, int], + size_limit: Union[int, str], + no_split_module_classes: Optional[list[str]] = None, + tied_parameters: Optional[list[list[str]]] = None, +) -> tuple[Optional[str], Optional[nn.Module], list[tuple[str, nn.Module]]]: + """ + Find a module that fits in the size limit using BFS and return it with its name and the remaining modules. + + Args: + modules (`List[Tuple[str, nn.Module]]`): + The list of named modules to search in. + module_sizes (`Dict[str, int]`): + A dictionary mapping each layer name to its size (as generated by `compute_module_sizes`). + size_limit (`Union[int, str]`): + The maximum size a module can have. + no_split_module_classes (`Optional[List[str]]`, *optional*): + A list of class names for layers we don't want to be split. + tied_parameters (`Optional[List[List[str]]`, *optional*): + A list of lists of parameter names being all tied together. + + Returns: + `Tuple[Optional[str], Optional[nn.Module], List[Tuple[str, nn.Module]]]`: A tuple containing: + - The name of the module that fits within the size limit. + - The module itself. + - The list of remaining modules after the found module is removed. + """ + try: + size_limit = convert_file_size_to_int(size_limit) + except ValueError: + return None, None, modules + + if no_split_module_classes is None: + no_split_module_classes = [] + + if tied_parameters is None: + tied_parameters = [] + + modules_to_search = modules.copy() + module_found = False + + while modules_to_search: + name, module = modules_to_search.pop(0) + + tied_param_groups = [ + tied_group + for tied_group in tied_parameters + if any(name + "." in k + "." for k in tied_group) and not all(name + "." in k + "." for k in tied_group) + ] + + tied_params = sum( + [[p for p in tied_group if name + "." not in p + "."] for tied_group in tied_param_groups], [] + ) + + module_size_with_ties, _, _ = get_module_size_with_ties( + tied_params, module_sizes[name], module_sizes, modules_to_search + ) + + # If the module fits in the size limit, we found it. + if module_size_with_ties <= size_limit: + module_found = True + break + + # The module is too big, we need to split it if possible. + modules_children = ( + [] + if isinstance(module, nn.Parameter) or isinstance(module, torch.Tensor) + else list(module.named_children()) + ) + + # Split fails, move to the next module + if len(modules_children) == 0 or module.__class__.__name__ in no_split_module_classes: + continue + + # split is possible, add the children to the list of modules to search + modules_children = list(module.named_parameters(recurse=False)) + modules_children + modules_to_search = [(f"{name}.{n}", v) for n, v in modules_children] + modules_to_search + + if not module_found: + return None, None, modules + + # Prepare the module list for removal of the found module + current_names = [n for n, _ in modules] + dot_idx = [i for i, c in enumerate(name) if c == "."] + + for dot_index in dot_idx: + parent_name = name[:dot_index] + if parent_name in current_names: + parent_module_idx = current_names.index(parent_name) + _, parent_module = modules[parent_module_idx] + module_children = list(parent_module.named_parameters(recurse=False)) + list( + parent_module.named_children() + ) + modules = ( + modules[:parent_module_idx] + + [(f"{parent_name}.{n}", v) for n, v in module_children] + + modules[parent_module_idx + 1 :] + ) + current_names = [n for n, _ in modules] + + # Now the target module should be directly in the list + target_idx = current_names.index(name) + name, module = modules.pop(target_idx) + + return name, module, modules + + +def infer_auto_device_map( + model: nn.Module, + max_memory: Optional[dict[Union[int, str], Union[int, str]]] = None, + no_split_module_classes: Optional[list[str]] = None, + dtype: Optional[Union[str, torch.dtype]] = None, + special_dtypes: Optional[dict[str, Union[str, torch.dtype]]] = None, + verbose: bool = False, + clean_result: bool = True, + offload_buffers: bool = False, + fallback_allocation: bool = False, +): + """ + Compute a device map for a given model giving priority to GPUs, then offload on CPU and finally offload to disk, + such that: + - we don't exceed the memory available of any of the GPU. + - if offload to the CPU is needed, there is always room left on GPU 0 to put back the layer offloaded on CPU that + has the largest size. + - if offload to the CPU is needed,we don't exceed the RAM available on the CPU. + - if offload to the disk is needed, there is always room left on the CPU to put back the layer offloaded on disk + that has the largest size. + + + + All computation is done analyzing sizes and dtypes of the model parameters. As a result, the model can be on the + meta device (as it would if initialized within the `init_empty_weights` context manager). + + + + Args: + model (`torch.nn.Module`): + The model to analyze. + max_memory (`Dict`, *optional*): + A dictionary device identifier to maximum memory. Will default to the maximum memory available if unset. + Example: `max_memory={0: "1GB"}`. + no_split_module_classes (`List[str]`, *optional*): + A list of layer class names that should never be split across device (for instance any layer that has a + residual connection). + dtype (`str` or `torch.dtype`, *optional*): + If provided, the weights will be converted to that type when loaded. + special_dtypes (`Dict[str, Union[str, torch.device]]`, *optional*): + If provided, special dtypes to consider for some specific weights (will override dtype used as default for + all weights). + verbose (`bool`, *optional*, defaults to `False`): + Whether or not to provide debugging statements as the function builds the device_map. + clean_result (`bool`, *optional*, defaults to `True`): + Clean the resulting device_map by grouping all submodules that go on the same device together. + offload_buffers (`bool`, *optional*, defaults to `False`): + In the layers that are offloaded on the CPU or the hard drive, whether or not to offload the buffers as + well as the parameters. + fallback_allocation (`bool`, *optional*, defaults to `False`): + When regular allocation fails, try to allocate a module that fits in the size limit using BFS. + """ + + # Initialize the variables + ( + devices, + max_memory, + main_devices, + gpus, + module_sizes, + tied_parameters, + no_split_module_classes, + modules_to_treat, + ) = _init_infer_auto_device_map(model, max_memory, no_split_module_classes, dtype, special_dtypes) + + device_map = OrderedDict() + current_device = 0 + device_memory_used = {device: 0 for device in devices} + device_buffer_sizes = {} + device_minimum_assignment_memory = {} + + # Initialize maximum largest layer, to know which space to keep in memory + max_layer_size, max_layer_names = get_max_layer_size(modules_to_treat, module_sizes, no_split_module_classes) + + # Ready ? This is going to be a bit messy. + while len(modules_to_treat) > 0: + name, module = modules_to_treat.pop(0) + if verbose: + print(f"\nTreating module {name}.") + # Max size in the remaining layers may have changed since we took one, so we maybe update it. + max_layer_names = [n for n in max_layer_names if n != name and not n.startswith(name + ".")] + if len(max_layer_names) == 0: + max_layer_size, max_layer_names = get_max_layer_size( + [(n, m) for n, m in modules_to_treat if isinstance(m, torch.nn.Module)], + module_sizes, + no_split_module_classes, + ) + # Assess size needed + module_size = module_sizes[name] + + # We keep relevant tied parameters only: one of the tied parameters in the group is inside the current module + # and the other is not. + # Note: If we are currently processing the name `compute.weight`, an other parameter named + # e.g. `compute.weight_submodule.parameter` + # needs to be considered outside the current module, hence the check with additional dots. + tied_param_groups = [ + tied_group + for tied_group in tied_parameters + if any(name + "." in k + "." for k in tied_group) and not all(name + "." in k + "." for k in tied_group) + ] + + if verbose and len(tied_param_groups) > 0: + print(f" Found the relevant tied param groups {tied_param_groups}") + + # Then we keep track of all the parameters that are tied to the current module, but not in the current module + tied_params = sum( + [[p for p in tied_group if name + "." not in p + "."] for tied_group in tied_param_groups], [] + ) + + if verbose and len(tied_params) > 0: + print(f" So those parameters need to be taken into account {tied_params}") + + device = devices[current_device] + current_max_size = max_memory[device] if device != "disk" else None + current_memory_reserved = 0 + # Reduce max size available by the largest layer. + if devices[current_device] in main_devices: + current_max_size = current_max_size - max_layer_size + current_memory_reserved = max_layer_size + + module_size_with_ties, tied_module_names, tied_modules = get_module_size_with_ties( + tied_params, module_size, module_sizes, modules_to_treat + ) + + # The module and its tied modules fit on the current device. + if current_max_size is None or device_memory_used[device] + module_size_with_ties <= current_max_size: + if verbose: + output = f"Putting {name}" + + if tied_module_names: + output += f" and {tied_module_names}" + else: + output += f" (size={module_size})" + + if current_max_size is not None: + output += f" (available={current_max_size - device_memory_used[device]})" + + output += f" on {device}." + print(output) + + device_memory_used[device] += module_size_with_ties + + # Assign the primary module to the device. + device_map[name] = device + + # Assign tied modules if any. + for tied_module_name in tied_module_names: + if tied_module_name in [m[0] for m in modules_to_treat]: + # Find the index of the tied module in the list + tied_module_index = next(i for i, (n, _) in enumerate(modules_to_treat) if n == tied_module_name) + # Remove the tied module from the list to prevent reprocessing + modules_to_treat.pop(tied_module_index) + + # Assign the tied module to the device + device_map[tied_module_name] = device + + # Buffer Handling + if not offload_buffers and isinstance(module, nn.Module): + # Compute the total buffer size for the module + current_buffer_size = compute_module_total_buffer_size( + module, dtype=dtype, special_dtypes=special_dtypes + ) + # Update the buffer size on the device + device_buffer_sizes[device] = device_buffer_sizes.get(device, 0) + current_buffer_size + + continue + + # The current module itself fits, so we try to split the tied modules. + if len(tied_params) > 0 and device_memory_used[device] + module_size <= current_max_size: + # can we split one of the tied modules to make it smaller or do we need to go on the next device? + if verbose: + print( + f"Not enough space on {devices[current_device]} to put {name} and {tied_module_names} (space " + f"available {current_max_size - device_memory_used[device]}, needed size {module_size_with_ties})." + ) + split_happened = False + for tied_module_name, tied_module in zip(tied_module_names, tied_modules): + tied_module_children = list(tied_module.named_children()) + if len(tied_module_children) == 0 or tied_module.__class__.__name__ in no_split_module_classes: + # can't break this one. + continue + + if verbose: + print(f"Splitting {tied_module_name}.") + tied_module_children = list(tied_module.named_parameters(recurse=False)) + tied_module_children + tied_module_children = [(f"{tied_module_name}.{n}", v) for n, v in tied_module_children] + tied_module_index = [i for i, (n, _) in enumerate(modules_to_treat) if n == tied_module_name][0] + + modules_to_treat = ( + [(name, module)] + + modules_to_treat[:tied_module_index] + + tied_module_children + + modules_to_treat[tied_module_index + 1 :] + ) + # Update the max layer size. + max_layer_size, max_layer_names = get_max_layer_size( + [(n, m) for n, m in modules_to_treat if isinstance(m, torch.nn.Module)], + module_sizes, + no_split_module_classes, + ) + split_happened = True + break + + if split_happened: + continue + + # If the tied module is not split, we go to the next device + if verbose: + print("None of the tied module can be split, going to the next device.") + + # The current module itself doesn't fit, so we have to split it or go to the next device. + if device_memory_used[device] + module_size >= current_max_size: + # Split or not split? + modules_children = ( + [] + if isinstance(module, nn.Parameter) or isinstance(module, torch.Tensor) + else list(module.named_children()) + ) + if verbose: + print( + f"Not enough space on {devices[current_device]} to put {name} (space available " + f"{current_max_size - device_memory_used[device]}, module size {module_size})." + ) + if len(modules_children) == 0 or module.__class__.__name__ in no_split_module_classes: + # -> no split, we go to the next device + if verbose: + print("This module cannot be split, going to the next device.") + + else: + # -> split, we replace the module studied by its children + parameters + if verbose: + print(f"Splitting {name}.") + modules_children = list(module.named_parameters(recurse=False)) + modules_children + modules_to_treat = [(f"{name}.{n}", v) for n, v in modules_children] + modules_to_treat + # Update the max layer size. + max_layer_size, max_layer_names = get_max_layer_size( + [(n, m) for n, m in modules_to_treat if isinstance(m, torch.nn.Module)], + module_sizes, + no_split_module_classes, + ) + continue + + # If no module is assigned to the current device, we attempt to allocate a fallback module + # if fallback_allocation is enabled. + if device_memory_used[device] == 0 and fallback_allocation and device != "disk": + # We try to allocate a module that fits in the size limit using BFS. + # Recompute the current max size as we need to consider the current module as well. + current_max_size = max_memory[device] - max(max_layer_size, module_size_with_ties) + + fallback_module_name, fallback_module, remaining_modules = fallback_allocate( + modules_to_treat, + module_sizes, + current_max_size - device_memory_used[device], + no_split_module_classes, + tied_parameters, + ) + # use the next iteration to put the fallback module on the next device to avoid code duplication + if fallback_module is not None: + modules_to_treat = [(fallback_module_name, fallback_module)] + [(name, module)] + remaining_modules + continue + + if device_memory_used[device] == 0: + device_minimum_assignment_memory[device] = module_size_with_ties + current_memory_reserved + + # Neither the current module nor any tied modules can be split, so we move to the next device. + device_memory_used[device] = device_memory_used[device] + current_memory_reserved + current_device += 1 + modules_to_treat = [(name, module)] + modules_to_treat + + device_memory_used = {device: mem for device, mem in device_memory_used.items() if mem > 0} + + if clean_result: + device_map = clean_device_map(device_map) + + non_gpu_buffer_size = device_buffer_sizes.get("cpu", 0) + device_buffer_sizes.get("disk", 0) + if non_gpu_buffer_size > 0 and not offload_buffers: + is_buffer_fit_any_gpu = False + for gpu_device, gpu_max_memory in max_memory.items(): + if gpu_device == "cpu" or gpu_device == "disk": + continue + + if not is_buffer_fit_any_gpu: + gpu_memory_used = device_memory_used.get(gpu_device, 0) + + if gpu_max_memory >= non_gpu_buffer_size + gpu_memory_used: + is_buffer_fit_any_gpu = True + + if len(gpus) > 0 and not is_buffer_fit_any_gpu: + warnings.warn( + f"Current model requires {non_gpu_buffer_size} bytes of buffer for offloaded layers, which seems does " + f"not fit any GPU's remaining memory. If you are experiencing a OOM later, please consider using " + f"offload_buffers=True." + ) + + if device_minimum_assignment_memory: + devices_info = "\n".join( + f" - {device}: {mem} bytes required" for device, mem in device_minimum_assignment_memory.items() + ) + logger.info( + f"Based on the current allocation process, no modules could be assigned to the following devices due to " + f"insufficient memory:\n" + f"{devices_info}\n" + f"These minimum requirements are specific to this allocation attempt and may vary. Consider increasing " + f"the available memory for these devices to at least the specified minimum, or adjusting the model config." + ) + return device_map + + +def check_device_map(model: nn.Module, device_map: dict[str, Union[int, str, torch.device]]): + """ + Checks a device map covers everything in a given model. + + Args: + model (`torch.nn.Module`): The model to check the device map against. + device_map (`Dict[str, Union[int, str, torch.device]]`): The device map to check. + """ + all_module_names = dict(model.named_modules()) + invalid_keys = [k for k in device_map if k != "" and k not in all_module_names] + + if invalid_keys: + warnings.warn( + f"The following device_map keys do not match any submodules in the model: {invalid_keys}", UserWarning + ) + + all_model_tensors = [name for name, _ in model.state_dict().items()] + for module_name in device_map.keys(): + if module_name == "": + all_model_tensors.clear() + break + else: + all_model_tensors = [ + name + for name in all_model_tensors + if not name == module_name and not name.startswith(module_name + ".") + ] + if len(all_model_tensors) > 0: + non_covered_params = ", ".join(all_model_tensors) + raise ValueError( + f"The device_map provided does not give any device for the following parameters: {non_covered_params}" + ) + + +def load_state_dict(checkpoint_file, device_map=None): + """ + Load a checkpoint from a given file. If the checkpoint is in the safetensors format and a device map is passed, the + weights can be fast-loaded directly on the GPU. + + Args: + checkpoint_file (`str`): The path to the checkpoint to load. + device_map (`Dict[str, Union[int, str, torch.device]]`, *optional*): + A map that specifies where each submodule should go. It doesn't need to be refined to each parameter/buffer + name, once a given module name is inside, every submodule of it will be sent to the same device. + """ + if checkpoint_file.endswith(".safetensors"): + with safe_open(checkpoint_file, framework="pt") as f: + metadata = f.metadata() + weight_names = f.keys() + + if metadata is None: + logger.warning( + f"The safetensors archive passed at {checkpoint_file} does not contain metadata. " + "Make sure to save your model with the `save_pretrained` method. Defaulting to 'pt' metadata." + ) + metadata = {"format": "pt"} + + if metadata.get("format") not in ["pt", "tf", "flax"]: + raise OSError( + f"The safetensors archive passed at {checkpoint_file} does not contain the valid metadata. Make sure " + "you save your model with the `save_pretrained` method." + ) + elif metadata["format"] != "pt": + raise ValueError(f"The checkpoint passed was saved with {metadata['format']}, we need a the pt format.") + if device_map is None: + return safe_load_file(checkpoint_file) + else: + # if we only have one device we can load everything directly + if len(set(device_map.values())) == 1: + device = list(device_map.values())[0] + target_device = device + if isinstance(device, int): + if is_npu_available(): + target_device = f"npu:{device}" + elif is_hpu_available(): + target_device = "hpu" + + return safe_load_file(checkpoint_file, device=target_device) + + devices = list(set(device_map.values()) - {"disk"}) + # cpu device should always exist as fallback option + if "cpu" not in devices: + devices.append("cpu") + + # For each device, get the weights that go there + device_weights = {device: [] for device in devices} + for module_name, device in device_map.items(): + if device in devices: + device_weights[device].extend( + [k for k in weight_names if k == module_name or k.startswith(module_name + ".")] + ) + + # all weights that haven't defined a device should be loaded on CPU + device_weights["cpu"].extend([k for k in weight_names if k not in sum(device_weights.values(), [])]) + tensors = {} + if is_tqdm_available(): + progress_bar = tqdm( + main_process_only=False, + total=sum([len(device_weights[device]) for device in devices]), + unit="w", + smoothing=0, + leave=False, + ) + else: + progress_bar = None + for device in devices: + target_device = device + if isinstance(device, int): + if is_npu_available(): + target_device = f"npu:{device}" + elif is_hpu_available(): + target_device = "hpu" + + with safe_open(checkpoint_file, framework="pt", device=target_device) as f: + for key in device_weights[device]: + if progress_bar is not None: + progress_bar.set_postfix(dev=device, refresh=False) + progress_bar.set_description(key) + tensors[key] = f.get_tensor(key) + if progress_bar is not None: + progress_bar.update() + if progress_bar is not None: + progress_bar.close() + + return tensors + else: + return torch.load(checkpoint_file, map_location=torch.device("cpu"), weights_only=True) + + +def get_state_dict_offloaded_model(model: nn.Module): + """ + Returns the state dictionary for an offloaded model via iterative onloading + + Args: + model (`torch.nn.Module`): + The offloaded model we want to save + """ + + state_dict = {} + placeholders = set() + for name, module in model.named_modules(): + if name == "": + continue + + try: + with align_module_device(module, "cpu"): + module_state_dict = module.state_dict() + except MemoryError: + raise MemoryError("Offloaded module must fit in CPU memory to call save_model!") from None + + for key in module_state_dict: + # ignore placeholder parameters that are still on the meta device + if module_state_dict[key].device == torch.device("meta"): + placeholders.add(name + f".{key}") + continue + params = module_state_dict[key] + state_dict[name + f".{key}"] = params.to("cpu") # move buffers to cpu + for key in placeholders.copy(): + if key in state_dict: + placeholders.remove(key) + if placeholders: + logger.warning(f"The following tensors were not saved because they were still on meta device: {placeholders}") + + return state_dict + + +def get_state_dict_from_offload( + module: nn.Module, + module_name: str, + state_dict: dict[str, Union[str, torch.tensor]], + device_to_put_offload: Union[int, str, torch.device] = "cpu", +): + """ + Retrieve the state dictionary (with parameters) from an offloaded module and load into a specified device (defaults + to cpu). + + Args: + module: (`torch.nn.Module`): + The module we want to retrieve a state dictionary from + module_name: (`str`): + The name of the module of interest + state_dict (`Dict[str, Union[int, str, torch.device]]`): + Dictionary of {module names: parameters} + device_to_put_offload (`Union[int, str, torch.device]`): + Device to load offloaded parameters into, defaults to the cpu. + """ + + root = module_name[: module_name.rfind(".")] # module name without .weight or .bias + + # do not move parameters if the module is not offloaded + if not has_offloaded_params(module): + device_to_put_offload = None + + # assign the device to which the offloaded parameters will be sent + with align_module_device(module, device_to_put_offload): + for m_key, params in module.state_dict().items(): + if (root + f".{m_key}") in state_dict: + state_dict[root + f".{m_key}"] = params + + return state_dict + + +def load_checkpoint_in_model( + model: nn.Module, + checkpoint: Union[str, os.PathLike], + device_map: Optional[dict[str, Union[int, str, torch.device]]] = None, + offload_folder: Optional[Union[str, os.PathLike]] = None, + dtype: Optional[Union[str, torch.dtype]] = None, + offload_state_dict: bool = False, + offload_buffers: bool = False, + keep_in_fp32_modules: Optional[list[str]] = None, + offload_8bit_bnb: bool = False, + strict: bool = False, + full_state_dict: bool = True, + broadcast_from_rank0: bool = False, +): + """ + Loads a (potentially sharded) checkpoint inside a model, potentially sending weights to a given device as they are + loaded. + + + + Once loaded across devices, you still need to call [`dispatch_model`] on your model to make it able to run. To + group the checkpoint loading and dispatch in one single call, use [`load_checkpoint_and_dispatch`]. + + + + Args: + model (`torch.nn.Module`): + The model in which we want to load a checkpoint. + checkpoint (`str` or `os.PathLike`): + The folder checkpoint to load. It can be: + - a path to a file containing a whole model state dict + - a path to a `.json` file containing the index to a sharded checkpoint + - a path to a folder containing a unique `.index.json` file and the shards of a checkpoint. + - a path to a folder containing a unique pytorch_model.bin or a model.safetensors file. + device_map (`Dict[str, Union[int, str, torch.device]]`, *optional*): + A map that specifies where each submodule should go. It doesn't need to be refined to each parameter/buffer + name, once a given module name is inside, every submodule of it will be sent to the same device. + offload_folder (`str` or `os.PathLike`, *optional*): + If the `device_map` contains any value `"disk"`, the folder where we will offload weights. + dtype (`str` or `torch.dtype`, *optional*): + If provided, the weights will be converted to that type when loaded. + offload_state_dict (`bool`, *optional*, defaults to `False`): + If `True`, will temporarily offload the CPU state dict on the hard drive to avoid getting out of CPU RAM if + the weight of the CPU state dict + the biggest shard does not fit. + offload_buffers (`bool`, *optional*, defaults to `False`): + Whether or not to include the buffers in the weights offloaded to disk. + keep_in_fp32_modules(`List[str]`, *optional*): + A list of the modules that we keep in `torch.float32` dtype. + offload_8bit_bnb (`bool`, *optional*): + Whether or not to enable offload of 8-bit modules on cpu/disk. + strict (`bool`, *optional*, defaults to `False`): + Whether to strictly enforce that the keys in the checkpoint state_dict match the keys of the model's + state_dict. + full_state_dict (`bool`, *optional*, defaults to `True`): if this is set to `True`, all the tensors in the + loaded state_dict will be gathered. No ShardedTensor and DTensor will be in the loaded state_dict. + broadcast_from_rank0 (`False`, *optional*, defaults to `False`): when the option is `True`, a distributed + `ProcessGroup` must be initialized. rank0 should receive a full state_dict and will broadcast the tensors + in the state_dict one by one to other ranks. Other ranks will receive the tensors and shard (if applicable) + according to the local shards in the model. + + """ + if offload_8bit_bnb: + from .bnb import quantize_and_offload_8bit + + tied_params = find_tied_parameters(model) + + if check_tied_parameters_in_config(model) and len(tied_params) == 0: + logger.warning( + "The model weights are not tied. Please use the `tie_weights` method before using the `infer_auto_device` function." + ) + if device_map is not None: + check_tied_parameters_on_same_device(tied_params, device_map) + + if offload_folder is None and device_map is not None and "disk" in device_map.values(): + raise ValueError( + "At least one of the model submodule will be offloaded to disk, please pass along an `offload_folder`." + ) + elif offload_folder is not None and device_map is not None and "disk" in device_map.values(): + os.makedirs(offload_folder, exist_ok=True) + + if isinstance(dtype, str): + # We accept "torch.float16" or just "float16" + dtype = dtype.replace("torch.", "") + dtype = getattr(torch, dtype) + + checkpoint_files = None + index_filename = None + if os.path.isfile(checkpoint): + if str(checkpoint).endswith(".json"): + index_filename = checkpoint + else: + checkpoint_files = [checkpoint] + elif os.path.isdir(checkpoint): + # check if the whole state dict is present + potential_state_bin = [f for f in os.listdir(checkpoint) if f == WEIGHTS_NAME] + potential_state_safetensor = [f for f in os.listdir(checkpoint) if f == SAFE_WEIGHTS_NAME] + if len(potential_state_bin) == 1: + checkpoint_files = [os.path.join(checkpoint, potential_state_bin[0])] + elif len(potential_state_safetensor) == 1: + checkpoint_files = [os.path.join(checkpoint, potential_state_safetensor[0])] + else: + # otherwise check for sharded checkpoints + potential_index = [f for f in os.listdir(checkpoint) if f.endswith(".index.json")] + if len(potential_index) == 0: + raise ValueError( + f"{checkpoint} is not a folder containing a `.index.json` file or a {WEIGHTS_NAME} or a {SAFE_WEIGHTS_NAME} file" + ) + elif len(potential_index) == 1: + index_filename = os.path.join(checkpoint, potential_index[0]) + else: + raise ValueError( + f"{checkpoint} containing more than one `.index.json` file, delete the irrelevant ones." + ) + else: + raise ValueError( + "`checkpoint` should be the path to a file containing a whole state dict, or the index of a sharded " + f"checkpoint, or a folder containing a sharded checkpoint or the whole state dict, but got {checkpoint}." + ) + + if index_filename is not None: + checkpoint_folder = os.path.split(index_filename)[0] + with open(index_filename) as f: + index = json.loads(f.read()) + + if "weight_map" in index: + index = index["weight_map"] + checkpoint_files = sorted(list(set(index.values()))) + checkpoint_files = [os.path.join(checkpoint_folder, f) for f in checkpoint_files] + + # Logic for missing/unexepected keys goes here. + + offload_index = {} + if offload_state_dict: + state_dict_folder = tempfile.mkdtemp() + state_dict_index = {} + + unexpected_keys = set() + model_keys = set(model.state_dict().keys()) + buffer_names = [name for name, _ in model.named_buffers()] + model_devices = {t.device for t in model.state_dict().values() if isinstance(t, torch.Tensor)} + model_physical_devices = model_devices - {torch.device("meta")} + for checkpoint_file in checkpoint_files: + if device_map is None: + # exception for multi-device loading was made for the meta device in torch v2.7.0 + # https://github.com/pytorch/pytorch/blob/v2.6.0/torch/distributed/checkpoint/state_dict.py#L557-L563 + # https://github.com/pytorch/pytorch/blob/v2.7.0-rc2/torch/distributed/checkpoint/state_dict.py#L575-L587 + if is_torch_version(">=", "2.2.0") and ( + (is_torch_version(">=", "2.7.0") and len(model_physical_devices) <= 1) or len(model_devices) <= 1 + ): + from torch.distributed.checkpoint.state_dict import StateDictOptions, set_model_state_dict + + broadcast_from_rank0 &= is_torch_version(">=", "2.4.0") + loaded_checkpoint = ( + load_state_dict(checkpoint_file, device_map=device_map) + if not broadcast_from_rank0 or dist.get_rank() == 0 + else {} + ) + set_model_state_dict( + model, + loaded_checkpoint, + options=StateDictOptions( + full_state_dict=full_state_dict, + strict=strict, + **({"broadcast_from_rank0": broadcast_from_rank0} if is_torch_version(">=", "2.4.0") else {}), + ), + ) + else: + loaded_checkpoint = load_state_dict(checkpoint_file, device_map=device_map) + model.load_state_dict(loaded_checkpoint, strict=strict) + + unexpected_keys.update(set(loaded_checkpoint.keys()) - model_keys) + else: + loaded_checkpoint = load_state_dict(checkpoint_file, device_map=device_map) + + for param_name, param in loaded_checkpoint.items(): + # skip SCB parameter (for 8-bit serialization) + if "SCB" in param_name: + continue + + if param_name not in model_keys: + unexpected_keys.add(param_name) + if not strict: + continue # Skip loading this parameter. + + module_name = param_name + + while len(module_name) > 0 and module_name not in device_map: + module_name = ".".join(module_name.split(".")[:-1]) + if module_name == "" and "" not in device_map: + # TODO: group all errors and raise at the end. + raise ValueError(f"{param_name} doesn't have any device set.") + param_device = device_map[module_name] + new_dtype = dtype + if dtype is not None and torch.is_floating_point(param): + if keep_in_fp32_modules is not None and dtype == torch.float16: + proceed = False + for key in keep_in_fp32_modules: + if ((key in param_name) and (key + "." in param_name)) or key == param_name: + proceed = True + break + if proceed: + new_dtype = torch.float32 + + if "weight" in param_name and param_name.replace("weight", "SCB") in loaded_checkpoint.keys(): + if param.dtype == torch.int8: + fp16_statistics = loaded_checkpoint[param_name.replace("weight", "SCB")] + else: + fp16_statistics = None + + if param_device == "disk": + if offload_buffers or param_name not in buffer_names: + if new_dtype is None: + new_dtype = param.dtype + if offload_8bit_bnb: + quantize_and_offload_8bit( + model, param, param_name, new_dtype, offload_folder, offload_index, fp16_statistics + ) + continue + else: + set_module_tensor_to_device(model, param_name, "meta", dtype=new_dtype) + offload_weight(param, param_name, offload_folder, index=offload_index) + elif param_device == "cpu" and offload_state_dict: + if new_dtype is None: + new_dtype = param.dtype + if offload_8bit_bnb: + quantize_and_offload_8bit( + model, param, param_name, new_dtype, state_dict_folder, state_dict_index, fp16_statistics + ) + else: + set_module_tensor_to_device(model, param_name, "meta", dtype=new_dtype) + offload_weight(param, param_name, state_dict_folder, index=state_dict_index) + else: + set_module_tensor_to_device( + model, + param_name, + param_device, + value=param, + dtype=new_dtype, + fp16_statistics=fp16_statistics, + ) + + # Force Python to clean up. + del loaded_checkpoint + gc.collect() + + if not strict and len(unexpected_keys) > 0: + logger.warning( + f"Some weights of the model checkpoint at {checkpoint} were not used when" + f" initializing {model.__class__.__name__}: {unexpected_keys}. This may or may not be an issue - make sure that the checkpoint does not have unnecessary parameters, or that the model definition correctly corresponds to the checkpoint." + ) + + save_offload_index(offload_index, offload_folder) + + # Load back offloaded state dict on CPU + if offload_state_dict: + load_offloaded_weights(model, state_dict_index, state_dict_folder) + shutil.rmtree(state_dict_folder) + + retie_parameters(model, tied_params) + + +def get_mixed_precision_context_manager(native_amp: bool = False, autocast_kwargs: AutocastKwargs = None): + """ + Return a context manager for autocasting mixed precision + + Args: + native_amp (`bool`, *optional*, defaults to False): + Whether mixed precision is actually enabled. + cache_enabled (`bool`, *optional*, defaults to True): + Whether the weight cache inside autocast should be enabled. + """ + state = AcceleratorState() + if autocast_kwargs is None: + autocast_kwargs = {} + else: + autocast_kwargs = autocast_kwargs.to_kwargs() + if native_amp: + device_type = ( + "cuda" + if (state.distributed_type == DistributedType.XLA and is_torch_xla_available(check_is_gpu=True)) + else state.device.type + ) + if state.mixed_precision == "fp16": + return torch.autocast(device_type=device_type, dtype=torch.float16, **autocast_kwargs) + elif state.mixed_precision in ["bf16", "fp8"] and state.distributed_type in [ + DistributedType.NO, + DistributedType.MULTI_CPU, + DistributedType.MULTI_GPU, + DistributedType.MULTI_MLU, + DistributedType.MULTI_SDAA, + DistributedType.MULTI_MUSA, + DistributedType.MULTI_NPU, + DistributedType.MULTI_XPU, + DistributedType.MULTI_HPU, + DistributedType.MULTI_NEURON, + DistributedType.FSDP, + DistributedType.XLA, + ]: + return torch.autocast(device_type=device_type, dtype=torch.bfloat16, **autocast_kwargs) + else: + return torch.autocast(device_type=device_type, **autocast_kwargs) + else: + return contextlib.nullcontext() + + +def get_grad_scaler(distributed_type: DistributedType = None, **kwargs): + """ + A generic helper which will initialize the correct `GradScaler` implementation based on the environment and return + it. + + Args: + distributed_type (`DistributedType`, *optional*, defaults to None): + The type of distributed environment. + kwargs: + Additional arguments for the utilized `GradScaler` constructor. + """ + if distributed_type == DistributedType.FSDP: + from torch.distributed.fsdp.sharded_grad_scaler import ShardedGradScaler + + return ShardedGradScaler(**kwargs) + if is_torch_xla_available(check_is_gpu=True): + import torch_xla.amp as xamp + + return xamp.GradScaler(**kwargs) + elif is_mlu_available(): + return torch.mlu.amp.GradScaler(**kwargs) + elif is_sdaa_available(): + return torch.sdaa.amp.GradScaler(**kwargs) + elif is_musa_available(): + return torch.musa.amp.GradScaler(**kwargs) + elif is_npu_available(): + return torch.npu.amp.GradScaler(**kwargs) + elif is_hpu_available(): + return torch.amp.GradScaler("hpu", **kwargs) + elif is_xpu_available(): + return torch.amp.GradScaler("xpu", **kwargs) + elif is_mps_available(): + if not is_torch_version(">=", "2.8.0"): + raise ValueError("Grad Scaler with MPS device requires a Pytorch >= 2.8.0") + return torch.amp.GradScaler("mps", **kwargs) + else: + if is_torch_version(">=", "2.3"): + return torch.amp.GradScaler("cuda", **kwargs) + else: + return torch.cuda.amp.GradScaler(**kwargs) + + +def has_offloaded_params(module: torch.nn.Module) -> bool: + """ + Checks if a module has offloaded parameters by checking if the given module has a AlignDevicesHook attached with + offloading enabled + + Args: + module (`torch.nn.Module`): The module to check for an offload hook. + + Returns: + bool: `True` if the module has an offload hook and offloading is enabled, `False` otherwise. + """ + from ..hooks import AlignDevicesHook # avoid circular import + + return hasattr(module, "_hf_hook") and isinstance(module._hf_hook, AlignDevicesHook) and module._hf_hook.offload + + +@contextlib.contextmanager +def align_module_device(module: torch.nn.Module, execution_device: Optional[torch.device] = None): + """ + Context manager that moves a module's parameters to the specified execution device. + + Args: + module (`torch.nn.Module`): + Module with parameters to align. + execution_device (`torch.device`, *optional*): + If provided, overrides the module's execution device within the context. Otherwise, use hook execution + device or pass + """ + if has_offloaded_params(module): + if execution_device is not None: + original_device = module._hf_hook.execution_device + module._hf_hook.execution_device = execution_device + + try: + module._hf_hook.pre_forward(module) + yield + finally: + module._hf_hook.post_forward(module, None) + if execution_device is not None: + module._hf_hook.execution_device = original_device + + elif execution_device is not None: + devices = {name: param.device for name, param in module.named_parameters(recurse=False)} + try: + for name in devices: + set_module_tensor_to_device(module, name, execution_device) + yield + finally: + for name, device in devices.items(): + set_module_tensor_to_device(module, name, device) + + else: + yield diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/offload.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/offload.py new file mode 100644 index 0000000000000000000000000000000000000000..da8cfaf3ebf7f2965f408e2b952103ac26868647 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/offload.py @@ -0,0 +1,213 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from collections.abc import Mapping +from typing import Optional, Union + +import numpy as np +import torch +from safetensors import safe_open + + +def offload_weight(weight, weight_name, offload_folder, index=None): + dtype = None + # Check the string instead of the dtype to be compatible with versions of PyTorch that don't have bfloat16. + if str(weight.dtype) == "torch.bfloat16": + # Need to reinterpret the underlined data as int16 since NumPy does not handle bfloat16s. + weight = weight.view(torch.int16) + dtype = "bfloat16" + array = weight.cpu().numpy() + tensor_file = os.path.join(offload_folder, f"{weight_name}.dat") + if index is not None: + if dtype is None: + dtype = str(array.dtype) + index[weight_name] = {"dtype": dtype, "shape": list(array.shape)} + if array.ndim == 0: + array = array[None] + file_array = np.memmap(tensor_file, dtype=array.dtype, mode="w+", shape=array.shape) + file_array[:] = array[:] + file_array.flush() + return index + + +def load_offloaded_weight(weight_file, weight_info): + shape = tuple(weight_info["shape"]) + if shape == (): + # NumPy memory-mapped arrays can't have 0 dims so it was saved as 1d tensor + shape = (1,) + + dtype = weight_info["dtype"] + if dtype == "bfloat16": + # NumPy does not support bfloat16 so this was saved as a int16 + dtype = "int16" + + weight = np.memmap(weight_file, dtype=dtype, shape=shape, mode="r") + + if len(weight_info["shape"]) == 0: + weight = weight[0] + weight = torch.tensor(weight) + if weight_info["dtype"] == "bfloat16": + weight = weight.view(torch.bfloat16) + + return weight + + +def save_offload_index(index, offload_folder): + if index is None or len(index) == 0: + # Nothing to save + return + + offload_index_file = os.path.join(offload_folder, "index.json") + if os.path.isfile(offload_index_file): + with open(offload_index_file, encoding="utf-8") as f: + current_index = json.load(f) + else: + current_index = {} + current_index.update(index) + + with open(offload_index_file, "w", encoding="utf-8") as f: + json.dump(current_index, f, indent=2) + + +def offload_state_dict(save_dir: Union[str, os.PathLike], state_dict: dict[str, torch.Tensor]): + """ + Offload a state dict in a given folder. + + Args: + save_dir (`str` or `os.PathLike`): + The directory in which to offload the state dict. + state_dict (`Dict[str, torch.Tensor]`): + The dictionary of tensors to offload. + """ + os.makedirs(save_dir, exist_ok=True) + index = {} + for name, parameter in state_dict.items(): + index = offload_weight(parameter, name, save_dir, index=index) + + # Update index + save_offload_index(index, save_dir) + + +class PrefixedDataset(Mapping): + """ + Will access keys in a given dataset by adding a prefix. + + Args: + dataset (`Mapping`): Any map with string keys. + prefix (`str`): A prefix to add when trying to access any element in the underlying dataset. + """ + + def __init__(self, dataset: Mapping, prefix: str): + self.dataset = dataset + self.prefix = prefix + + def __getitem__(self, key): + return self.dataset[f"{self.prefix}{key}"] + + def __iter__(self): + return iter([key for key in self.dataset if key.startswith(self.prefix)]) + + def __len__(self): + return len(self.dataset) + + +class OffloadedWeightsLoader(Mapping): + """ + A collection that loads weights stored in a given state dict or memory-mapped on disk. + + Args: + state_dict (`Dict[str, torch.Tensor]`, *optional*): + A dictionary parameter name to tensor. + save_folder (`str` or `os.PathLike`, *optional*): + The directory in which the weights are stored (by `offload_state_dict` for instance). + index (`Dict`, *optional*): + A dictionary from weight name to their information (`dtype`/ `shape` or safetensors filename). Will default + to the index saved in `save_folder`. + """ + + def __init__( + self, + state_dict: Optional[dict[str, torch.Tensor]] = None, + save_folder: Optional[Union[str, os.PathLike]] = None, + index: Optional[Mapping] = None, + device=None, + ): + if state_dict is None and save_folder is None and index is None: + raise ValueError("Need either a `state_dict`, a `save_folder` or an `index` containing offloaded weights.") + + self.state_dict = {} if state_dict is None else state_dict + self.save_folder = save_folder + if index is None and save_folder is not None: + with open(os.path.join(save_folder, "index.json")) as f: + index = json.load(f) + self.index = {} if index is None else index + self.all_keys = list(self.state_dict.keys()) + self.all_keys.extend([key for key in self.index if key not in self.all_keys]) + self.device = device + + def __getitem__(self, key: str): + # State dict gets priority + if key in self.state_dict: + return self.state_dict[key] + weight_info = self.index[key] + if weight_info.get("safetensors_file") is not None: + device = "cpu" if self.device is None else self.device + tensor = None + try: + with safe_open(weight_info["safetensors_file"], framework="pt", device=device) as f: + tensor = f.get_tensor(weight_info.get("weight_name", key)) + except TypeError: + # if failed to get_tensor on the device, such as bf16 on mps, try to load it on CPU first + with safe_open(weight_info["safetensors_file"], framework="pt", device="cpu") as f: + tensor = f.get_tensor(weight_info.get("weight_name", key)) + + if "dtype" in weight_info: + tensor = tensor.to(getattr(torch, weight_info["dtype"])) + + if tensor.device != torch.device(device): + tensor = tensor.to(device) + return tensor + + weight_file = os.path.join(self.save_folder, f"{key}.dat") + return load_offloaded_weight(weight_file, weight_info) + + def __iter__(self): + return iter(self.all_keys) + + def __len__(self): + return len(self.all_keys) + + +def extract_submodules_state_dict(state_dict: dict[str, torch.Tensor], submodule_names: list[str]): + """ + Extract the sub state-dict corresponding to a list of given submodules. + + Args: + state_dict (`Dict[str, torch.Tensor]`): The state dict to extract from. + submodule_names (`List[str]`): The list of submodule names we want to extract. + """ + result = {} + for module_name in submodule_names: + # We want to catch module_name parameter (module_name.xxx) or potentially module_name, but not any of the + # submodules that could being like module_name (transformers.h.1 and transformers.h.10 for instance) + result.update( + { + key: param + for key, param in state_dict.items() + if key == module_name or key.startswith(module_name + ".") + } + ) + return result diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/operations.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/operations.py new file mode 100644 index 0000000000000000000000000000000000000000..ceec9b457fe35cf146ffe3c65c73a4b2a8fb202a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/operations.py @@ -0,0 +1,871 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +A set of basic tensor ops compatible with tpu, gpu, and multigpu +""" + +import pickle +import warnings +from collections.abc import Mapping +from contextlib import contextmanager, nullcontext +from functools import update_wrapper, wraps +from typing import Any + +import torch + +from ..state import AcceleratorState, PartialState +from .constants import TORCH_DISTRIBUTED_OPERATION_TYPES +from .dataclasses import DistributedType, TensorInformation +from .imports import ( + is_npu_available, + is_torch_distributed_available, + is_torch_xla_available, +) +from .versions import is_torch_version + + +if is_torch_xla_available(): + import torch_xla.core.xla_model as xm + +if is_torch_distributed_available(): + from torch.distributed import ReduceOp + + +def is_torch_tensor(tensor): + return isinstance(tensor, torch.Tensor) + + +def is_torch_xpu_tensor(tensor): + return isinstance( + tensor, + torch.xpu.FloatTensor, + torch.xpu.ByteTensor, + torch.xpu.IntTensor, + torch.xpu.LongTensor, + torch.xpu.HalfTensor, + torch.xpu.DoubleTensor, + torch.xpu.BFloat16Tensor, + ) + + +def is_tensor_information(tensor_info): + return isinstance(tensor_info, TensorInformation) + + +def is_namedtuple(data): + """ + Checks if `data` is a `namedtuple` or not. Can have false positives, but only if a user is trying to mimic a + `namedtuple` perfectly. + """ + return isinstance(data, tuple) and hasattr(data, "_asdict") and hasattr(data, "_fields") + + +def honor_type(obj, generator): + """ + Cast a generator to the same type as obj (list, tuple, or namedtuple) + """ + # Some objects may not be able to instantiate from a generator directly + if is_namedtuple(obj): + return type(obj)(*list(generator)) + else: + return type(obj)(generator) + + +def recursively_apply(func, data, *args, test_type=is_torch_tensor, error_on_other_type=False, **kwargs): + """ + Recursively apply a function on a data structure that is a nested list/tuple/dictionary of a given base type. + + Args: + func (`callable`): + The function to recursively apply. + data (nested list/tuple/dictionary of `main_type`): + The data on which to apply `func` + *args: + Positional arguments that will be passed to `func` when applied on the unpacked data. + main_type (`type`, *optional*, defaults to `torch.Tensor`): + The base type of the objects to which apply `func`. + error_on_other_type (`bool`, *optional*, defaults to `False`): + Whether to return an error or not if after unpacking `data`, we get on an object that is not of type + `main_type`. If `False`, the function will leave objects of types different than `main_type` unchanged. + **kwargs (additional keyword arguments, *optional*): + Keyword arguments that will be passed to `func` when applied on the unpacked data. + + Returns: + The same data structure as `data` with `func` applied to every object of type `main_type`. + """ + if isinstance(data, (tuple, list)): + return honor_type( + data, + ( + recursively_apply( + func, o, *args, test_type=test_type, error_on_other_type=error_on_other_type, **kwargs + ) + for o in data + ), + ) + elif isinstance(data, Mapping): + return type(data)( + { + k: recursively_apply( + func, v, *args, test_type=test_type, error_on_other_type=error_on_other_type, **kwargs + ) + for k, v in data.items() + } + ) + elif test_type(data): + return func(data, *args, **kwargs) + elif error_on_other_type: + raise TypeError( + f"Unsupported types ({type(data)}) passed to `{func.__name__}`. Only nested list/tuple/dicts of " + f"objects that are valid for `{test_type.__name__}` should be passed." + ) + return data + + +def send_to_device(tensor, device, non_blocking=False, skip_keys=None): + """ + Recursively sends the elements in a nested list/tuple/dictionary of tensors to a given device. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to send to a given device. + device (`torch.device`): + The device to send the data to. + + Returns: + The same data structure as `tensor` with all tensors sent to the proper device. + """ + if is_torch_tensor(tensor) or hasattr(tensor, "to"): + # `torch.Tensor.to("npu")` could not find context when called for the first time (see this [issue](https://gitee.com/ascend/pytorch/issues/I8KECW?from=project-issue)). + if device == "npu": + device = "npu:0" + try: + return tensor.to(device, non_blocking=non_blocking) + except TypeError: # .to() doesn't accept non_blocking as kwarg + return tensor.to(device) + except AssertionError as error: + # `torch.Tensor.to()` is not supported by `torch_npu` (see this [issue](https://github.com/Ascend/pytorch/issues/16)). + # This call is inside the try-block since is_npu_available is not supported by torch.compile. + if is_npu_available(): + if isinstance(device, int): + device = f"npu:{device}" + else: + raise error + try: + return tensor.to(device, non_blocking=non_blocking) + except TypeError: # .to() doesn't accept non_blocking as kwarg + return tensor.to(device) + elif isinstance(tensor, (tuple, list)): + return honor_type( + tensor, (send_to_device(t, device, non_blocking=non_blocking, skip_keys=skip_keys) for t in tensor) + ) + elif isinstance(tensor, Mapping): + if isinstance(skip_keys, str): + skip_keys = [skip_keys] + elif skip_keys is None: + skip_keys = [] + return type(tensor)( + { + k: t if k in skip_keys else send_to_device(t, device, non_blocking=non_blocking, skip_keys=skip_keys) + for k, t in tensor.items() + } + ) + else: + return tensor + + +def get_data_structure(data): + """ + Recursively gathers the information needed to rebuild a nested list/tuple/dictionary of tensors. + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): + The data to send to analyze. + + Returns: + The same data structure as `data` with [`~utils.TensorInformation`] instead of tensors. + """ + + def _get_data_structure(tensor): + return TensorInformation(shape=tensor.shape, dtype=tensor.dtype) + + return recursively_apply(_get_data_structure, data) + + +def get_shape(data): + """ + Recursively gathers the shape of a nested list/tuple/dictionary of tensors as a list. + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): + The data to send to analyze. + + Returns: + The same data structure as `data` with lists of tensor shapes instead of tensors. + """ + + def _get_shape(tensor): + return list(tensor.shape) + + return recursively_apply(_get_shape, data) + + +def initialize_tensors(data_structure): + """ + Recursively initializes tensors from a nested list/tuple/dictionary of [`~utils.TensorInformation`]. + + Returns: + The same data structure as `data` with tensors instead of [`~utils.TensorInformation`]. + """ + + def _initialize_tensor(tensor_info): + return torch.empty(*tensor_info.shape, dtype=tensor_info.dtype) + + return recursively_apply(_initialize_tensor, data_structure, test_type=is_tensor_information) + + +def find_batch_size(data): + """ + Recursively finds the batch size in a nested list/tuple/dictionary of lists of tensors. + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): The data from which to find the batch size. + + Returns: + `int`: The batch size. + """ + if isinstance(data, (tuple, list, Mapping)) and (len(data) == 0): + raise ValueError(f"Cannot find the batch size from empty {type(data)}.") + + if isinstance(data, (tuple, list)): + return find_batch_size(data[0]) + elif isinstance(data, Mapping): + for k in data.keys(): + return find_batch_size(data[k]) + elif not isinstance(data, torch.Tensor): + raise TypeError(f"Can only find the batch size of tensors but got {type(data)}.") + return data.shape[0] + + +def ignorant_find_batch_size(data): + """ + Same as [`utils.operations.find_batch_size`] except will ignore if `ValueError` and `TypeErrors` are raised + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): The data from which to find the batch size. + + Returns: + `int`: The batch size. + """ + try: + return find_batch_size(data) + except (ValueError, TypeError): + pass + return None + + +def listify(data): + """ + Recursively finds tensors in a nested list/tuple/dictionary and converts them to a list of numbers. + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): The data from which to convert to regular numbers. + + Returns: + The same data structure as `data` with lists of numbers instead of `torch.Tensor`. + """ + + def _convert_to_list(tensor): + tensor = tensor.detach().cpu() + if tensor.dtype == torch.bfloat16: + # As of Numpy 1.21.4, NumPy does not support bfloat16 (see + # https://github.com/numpy/numpy/blob/a47ecdea856986cd60eabbd53265c2ca5916ad5d/doc/source/user/basics.types.rst ). + # Until Numpy adds bfloat16, we must convert float32. + tensor = tensor.to(torch.float32) + return tensor.tolist() + + return recursively_apply(_convert_to_list, data) + + +def _tpu_gather(tensor): + def _tpu_gather_one(tensor): + if tensor.ndim == 0: + tensor = tensor.clone()[None] + + # Can only gather contiguous tensors + if not tensor.is_contiguous(): + tensor = tensor.contiguous() + return xm.all_gather(tensor) + + res = recursively_apply(_tpu_gather_one, tensor, error_on_other_type=True) + xm.mark_step() + return res + + +def _gpu_gather(tensor): + state = PartialState() + gather_op = torch.distributed.all_gather_into_tensor + + # NOTE: need manually synchronize to workaourd a INT64 collectives bug in oneCCL before torch 2.9.0 + if state.device.type == "xpu" and is_torch_version("<=", "2.8"): + torch.xpu.synchronize() + + def _gpu_gather_one(tensor): + if tensor.ndim == 0: + tensor = tensor.clone()[None] + + # Can only gather contiguous tensors + if not tensor.is_contiguous(): + tensor = tensor.contiguous() + + if state.backend is not None and state.backend != "gloo": + # We use `empty` as `all_gather_into_tensor` slightly + # differs from `all_gather` for better efficiency, + # and we rely on the number of items in the tensor + # rather than its direct shape + output_tensors = torch.empty( + state.num_processes * tensor.numel(), + dtype=tensor.dtype, + device=state.device, + ) + gather_op(output_tensors, tensor) + return output_tensors.view(-1, *tensor.size()[1:]) + else: + # a backend of `None` is always CPU + # also gloo does not support `all_gather_into_tensor`, + # which will result in a larger memory overhead for the op + output_tensors = [torch.empty_like(tensor) for _ in range(state.num_processes)] + torch.distributed.all_gather(output_tensors, tensor) + return torch.cat(output_tensors, dim=0) + + return recursively_apply(_gpu_gather_one, tensor, error_on_other_type=True) + + +class DistributedOperationException(Exception): + """ + An exception class for distributed operations. Raised if the operation cannot be performed due to the shape of the + tensors. + """ + + pass + + +def verify_operation(function): + """ + Verifies that `tensor` is the same shape across all processes. Only ran if `PartialState().debug` is `True`. + """ + + @wraps(function) + def wrapper(*args, **kwargs): + if PartialState().distributed_type == DistributedType.NO or not PartialState().debug: + return function(*args, **kwargs) + operation = f"{function.__module__}.{function.__name__}" + if "tensor" in kwargs: + tensor = kwargs["tensor"] + else: + tensor = args[0] + if PartialState().device.type != find_device(tensor).type: + raise DistributedOperationException( + f"One or more of the tensors passed to {operation} were not on the {tensor.device.type} while the `Accelerator` is configured for {PartialState().device.type}. " + f"Please move it to the {PartialState().device.type} before calling {operation}." + ) + shapes = get_shape(tensor) + output = gather_object([shapes]) + if output[0] is not None: + are_same = output.count(output[0]) == len(output) + if not are_same: + process_shape_str = "\n - ".join([f"Process {i}: {shape}" for i, shape in enumerate(output)]) + raise DistributedOperationException( + f"Cannot apply desired operation due to shape mismatches. " + "All shapes across devices must be valid." + f"\n\nOperation: `{operation}`\nInput shapes:\n - {process_shape_str}" + ) + return function(*args, **kwargs) + + return wrapper + + +def chained_operation(function): + """ + Checks that `verify_operation` failed and if so reports a more helpful error chaining the existing + `DistributedOperationException`. + """ + + @wraps(function) + def wrapper(*args, **kwargs): + try: + return function(*args, **kwargs) + except DistributedOperationException as e: + operation = f"{function.__module__}.{function.__name__}" + raise DistributedOperationException( + f"Error found while calling `{operation}`. Please see the earlier error for more details." + ) from e + + return wrapper + + +@verify_operation +def gather(tensor): + """ + Recursively gather tensor in a nested list/tuple/dictionary of tensors from all devices. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to gather. + + Returns: + The same data structure as `tensor` with all tensors sent to the proper device. + """ + if PartialState().distributed_type == DistributedType.XLA: + return _tpu_gather(tensor) + elif PartialState().distributed_type in TORCH_DISTRIBUTED_OPERATION_TYPES: + return _gpu_gather(tensor) + else: + return tensor + + +def _gpu_gather_object(object: Any): + output_objects = [None for _ in range(PartialState().num_processes)] + torch.distributed.all_gather_object(output_objects, object) + # all_gather_object returns a list of lists, so we need to flatten it + return [x for y in output_objects for x in y] + + +def gather_object(object: Any): + """ + Recursively gather object in a nested list/tuple/dictionary of objects from all devices. + + Args: + object (nested list/tuple/dictionary of picklable object): + The data to gather. + + Returns: + The same data structure as `object` with all the objects sent to every device. + """ + if PartialState().distributed_type == DistributedType.XLA: + raise NotImplementedError("gather objects in TPU is not supported") + elif PartialState().distributed_type in TORCH_DISTRIBUTED_OPERATION_TYPES: + return _gpu_gather_object(object) + else: + return object + + +def _gpu_broadcast(data, src=0): + def _gpu_broadcast_one(tensor, src=0): + torch.distributed.broadcast(tensor, src=src) + return tensor + + return recursively_apply(_gpu_broadcast_one, data, error_on_other_type=True, src=src) + + +def _tpu_broadcast(tensor, src=0, name="broadcast tensor"): + if isinstance(tensor, (list, tuple)): + return honor_type(tensor, (_tpu_broadcast(t, name=f"{name}_{i}") for i, t in enumerate(tensor))) + elif isinstance(tensor, Mapping): + return type(tensor)({k: _tpu_broadcast(v, name=f"{name}_{k}") for k, v in tensor.items()}) + return xm.mesh_reduce(name, tensor, lambda x: x[src]) + + +TENSOR_TYPE_TO_INT = { + torch.float: 1, + torch.double: 2, + torch.half: 3, + torch.bfloat16: 4, + torch.uint8: 5, + torch.int8: 6, + torch.int16: 7, + torch.int32: 8, + torch.int64: 9, + torch.bool: 10, +} + +TENSOR_INT_TO_DTYPE = {v: k for k, v in TENSOR_TYPE_TO_INT.items()} + + +def gather_tensor_shape(tensor): + """ + Grabs the shape of `tensor` only available on one process and returns a tensor of its shape + """ + # Allocate 80 bytes to store the shape + max_tensor_dimension = 2**20 + state = PartialState() + base_tensor = torch.empty(max_tensor_dimension, dtype=torch.int, device=state.device) + + # Since PyTorch can't just send a tensor to another GPU without + # knowing its size, we store the size of the tensor with data + # in an allocation + if tensor is not None: + shape = tensor.shape + tensor_dtype = TENSOR_TYPE_TO_INT[tensor.dtype] + base_tensor[: len(shape) + 1] = torch.tensor(list(shape) + [tensor_dtype], dtype=int) + # Perform a reduction to copy the size data onto all GPUs + base_tensor = reduce(base_tensor, reduction="sum") + base_tensor = base_tensor[base_tensor.nonzero()] + # The last non-zero data contains the coded dtype the source tensor is + dtype = int(base_tensor[-1:][0]) + base_tensor = base_tensor[:-1] + return base_tensor, dtype + + +def copy_tensor_to_devices(tensor=None) -> torch.Tensor: + """ + Copies a tensor that only exists on a single device and broadcasts it to other devices. Differs from `broadcast` as + each worker doesn't need to know its shape when used (and tensor can be `None`) + + Args: + tensor (`torch.tensor`): + The tensor that should be sent to all devices. Must only have it be defined on a single device, the rest + should be `None`. + """ + state = PartialState() + shape, dtype = gather_tensor_shape(tensor) + if tensor is None: + tensor = torch.zeros(shape, dtype=TENSOR_INT_TO_DTYPE[dtype]).to(state.device) + return reduce(tensor, reduction="sum") + + +@verify_operation +def broadcast(tensor, from_process: int = 0): + """ + Recursively broadcast tensor in a nested list/tuple/dictionary of tensors to all devices. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to gather. + from_process (`int`, *optional*, defaults to 0): + The process from which to send the data + + Returns: + The same data structure as `tensor` with all tensors broadcasted to the proper device. + """ + if PartialState().distributed_type == DistributedType.XLA: + return _tpu_broadcast(tensor, src=from_process, name="accelerate.utils.broadcast") + elif PartialState().distributed_type in TORCH_DISTRIBUTED_OPERATION_TYPES: + return _gpu_broadcast(tensor, src=from_process) + else: + return tensor + + +def broadcast_object_list(object_list, from_process: int = 0): + """ + Broadcast a list of picklable objects from one process to the others. + + Args: + object_list (list of picklable objects): + The list of objects to broadcast. This list will be modified inplace. + from_process (`int`, *optional*, defaults to 0): + The process from which to send the data. + + Returns: + The same list containing the objects from process 0. + """ + if PartialState().distributed_type == DistributedType.XLA: + for i, obj in enumerate(object_list): + object_list[i] = xm.mesh_reduce("accelerate.utils.broadcast_object_list", obj, lambda x: x[from_process]) + elif PartialState().distributed_type in TORCH_DISTRIBUTED_OPERATION_TYPES: + torch.distributed.broadcast_object_list(object_list, src=from_process) + return object_list + + +def slice_tensors(data, tensor_slice, process_index=None, num_processes=None): + """ + Recursively takes a slice in a nested list/tuple/dictionary of tensors. + + Args: + data (nested list/tuple/dictionary of `torch.Tensor`): + The data to slice. + tensor_slice (`slice`): + The slice to take. + + Returns: + The same data structure as `data` with all the tensors slices. + """ + + def _slice_tensor(tensor, tensor_slice): + return tensor[tensor_slice] + + return recursively_apply(_slice_tensor, data, tensor_slice) + + +def concatenate(data, dim=0): + """ + Recursively concatenate the tensors in a nested list/tuple/dictionary of lists of tensors with the same shape. + If there is only a single batch of data, it is returned as-is. + + Args: + data (nested list/tuple/dictionary of lists of tensors `torch.Tensor`): + The data to concatenate. + dim (`int`, *optional*, defaults to 0): + The dimension on which to concatenate. + + Returns: + The same data structure as `data` with all the tensors concatenated. + """ + if isinstance(data[0], (tuple, list)): + return honor_type(data[0], (concatenate([d[i] for d in data], dim=dim) for i in range(len(data[0])))) + elif isinstance(data[0], Mapping): + return type(data[0])({k: concatenate([d[k] for d in data], dim=dim) for k in data[0].keys()}) + elif isinstance(data[0], torch.Tensor): + return torch.cat(data, dim=dim) + elif isinstance(data, (tuple, list)) and len(data) == 1: + return data[0] + else: + raise TypeError(f"Can only concatenate tensors but got {type(data[0])}") + + +class CannotPadNestedTensorWarning(UserWarning): + pass + + +@chained_operation +def pad_across_processes(tensor, dim=0, pad_index=0, pad_first=False): + """ + Recursively pad the tensors in a nested list/tuple/dictionary of tensors from all devices to the same size so they + can safely be gathered. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to gather. + dim (`int`, *optional*, defaults to 0): + The dimension on which to pad. + pad_index (`int`, *optional*, defaults to 0): + The value with which to pad. + pad_first (`bool`, *optional*, defaults to `False`): + Whether to pad at the beginning or the end. + """ + + def _pad_across_processes(tensor, dim=0, pad_index=0, pad_first=False): + if getattr(tensor, "is_nested", False): + warnings.warn( + "Cannot pad nested tensors without more information. Leaving unprocessed.", + CannotPadNestedTensorWarning, + ) + return tensor + if dim >= len(tensor.shape) or dim < -len(tensor.shape): + return tensor + # Convert negative dimensions to non-negative + if dim < 0: + dim += len(tensor.shape) + + # Gather all sizes + size = torch.tensor(tensor.shape, device=tensor.device)[None] + sizes = gather(size).cpu() + # Then pad to the maximum size + max_size = max(s[dim] for s in sizes) + if max_size == tensor.shape[dim]: + return tensor + + old_size = tensor.shape + new_size = list(old_size) + new_size[dim] = max_size + new_tensor = tensor.new_zeros(tuple(new_size)) + pad_index + if pad_first: + indices = tuple( + slice(max_size - old_size[dim], max_size) if i == dim else slice(None) for i in range(len(new_size)) + ) + else: + indices = tuple(slice(0, old_size[dim]) if i == dim else slice(None) for i in range(len(new_size))) + new_tensor[indices] = tensor + return new_tensor + + return recursively_apply( + _pad_across_processes, tensor, error_on_other_type=True, dim=dim, pad_index=pad_index, pad_first=pad_first + ) + + +def pad_input_tensors(tensor, batch_size, num_processes, dim=0): + """ + Takes a `tensor` of arbitrary size and pads it so that it can work given `num_processes` needed dimensions. + + New tensors are just the last input repeated. + + E.g.: + Tensor: ([3,4,4]) Num processes: 4 Expected result shape: ([4,4,4]) + + """ + + def _pad_input_tensors(tensor, batch_size, num_processes, dim=0): + remainder = batch_size // num_processes + last_inputs = batch_size - (remainder * num_processes) + if batch_size // num_processes == 0: + to_pad = num_processes - batch_size + else: + to_pad = num_processes - (batch_size // num_processes) + # In the rare case that `to_pad` is negative, + # we need to pad the last inputs - the found `to_pad` + if last_inputs > to_pad & to_pad < 1: + to_pad = last_inputs - to_pad + old_size = tensor.shape + new_size = list(old_size) + new_size[0] = batch_size + to_pad + new_tensor = tensor.new_zeros(tuple(new_size)) + indices = tuple(slice(0, old_size[dim]) if i == dim else slice(None) for i in range(len(new_size))) + new_tensor[indices] = tensor + return new_tensor + + return recursively_apply( + _pad_input_tensors, + tensor, + error_on_other_type=True, + batch_size=batch_size, + num_processes=num_processes, + dim=dim, + ) + + +@verify_operation +def reduce(tensor, reduction="mean", scale=1.0): + """ + Recursively reduce the tensors in a nested list/tuple/dictionary of lists of tensors across all processes by the + mean of a given operation. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to reduce. + reduction (`str`, *optional*, defaults to `"mean"`): + A reduction method. Can be of "mean", "sum", or "none" + scale (`float`, *optional*): + A default scaling value to be applied after the reduce, only valid on XLA. + + Returns: + The same data structure as `data` with all the tensors reduced. + """ + + def _reduce_across_processes(tensor, reduction="mean", scale=1.0): + state = PartialState() + cloned_tensor = tensor.clone() + if state.distributed_type == DistributedType.NO: + return cloned_tensor + if state.distributed_type == DistributedType.XLA: + # Some processes may have different HLO graphs than other + # processes, for example in the breakpoint API + # accelerator.set_trigger(). Use mark_step to make HLOs + # the same on all processes. + xm.mark_step() + xm.all_reduce(xm.REDUCE_SUM, [cloned_tensor], scale) + xm.mark_step() + elif state.distributed_type.value in TORCH_DISTRIBUTED_OPERATION_TYPES: + torch.distributed.all_reduce(cloned_tensor, ReduceOp.SUM) + if reduction == "mean": + cloned_tensor /= state.num_processes + return cloned_tensor + + return recursively_apply( + _reduce_across_processes, tensor, error_on_other_type=True, reduction=reduction, scale=scale + ) + + +def convert_to_fp32(tensor): + """ + Recursively converts the elements nested list/tuple/dictionary of tensors in FP16/BF16 precision to FP32. + + Args: + tensor (nested list/tuple/dictionary of `torch.Tensor`): + The data to convert from FP16/BF16 to FP32. + + Returns: + The same data structure as `tensor` with all tensors that were in FP16/BF16 precision converted to FP32. + """ + + def _convert_to_fp32(tensor): + return tensor.float() + + def _is_fp16_bf16_tensor(tensor): + return (is_torch_tensor(tensor) or hasattr(tensor, "dtype")) and tensor.dtype in ( + torch.float16, + torch.bfloat16, + ) + + return recursively_apply(_convert_to_fp32, tensor, test_type=_is_fp16_bf16_tensor) + + +class ConvertOutputsToFp32: + """ + Decorator to apply to a function outputting tensors (like a model forward pass) that ensures the outputs in FP16 + precision will be convert back to FP32. + + Args: + model_forward (`Callable`): + The function which outputs we want to treat. + + Returns: + The same function as `model_forward` but with converted outputs. + """ + + def __init__(self, model_forward): + self.model_forward = model_forward + update_wrapper(self, model_forward) + + def __call__(self, *args, **kwargs): + return convert_to_fp32(self.model_forward(*args, **kwargs)) + + def __getstate__(self): + raise pickle.PicklingError( + "Cannot pickle a prepared model with automatic mixed precision, please unwrap the model with `Accelerator.unwrap_model(model)` before pickling it." + ) + + +def convert_outputs_to_fp32(model_forward): + model_forward = ConvertOutputsToFp32(model_forward) + + def forward(*args, **kwargs): + return model_forward(*args, **kwargs) + + # To act like a decorator so that it can be popped when doing `extract_model_from_parallel` + forward.__wrapped__ = model_forward + + return forward + + +def find_device(data): + """ + Finds the device on which a nested dict/list/tuple of tensors lies (assuming they are all on the same device). + + Args: + (nested list/tuple/dictionary of `torch.Tensor`): The data we want to know the device of. + """ + if isinstance(data, Mapping): + for obj in data.values(): + device = find_device(obj) + if device is not None: + return device + elif isinstance(data, (tuple, list)): + for obj in data: + device = find_device(obj) + if device is not None: + return device + elif isinstance(data, torch.Tensor): + return data.device + + +@contextmanager +def GatheredParameters(params, modifier_rank=None, fwd_module=None, enabled=True): + """ + Wrapper around `deepspeed.runtime.zero.GatheredParameters`, but if Zero-3 is not enabled, will be a no-op context + manager. + """ + # We need to use the `AcceleratorState` here since it has access to the deepspeed plugin + if AcceleratorState().distributed_type != DistributedType.DEEPSPEED or ( + AcceleratorState().deepspeed_plugin is not None + and not AcceleratorState().deepspeed_plugin.is_zero3_init_enabled() + ): + gather_param_context = nullcontext() + else: + import deepspeed + + gather_param_context = deepspeed.zero.GatheredParameters( + params, modifier_rank=modifier_rank, fwd_module=fwd_module, enabled=enabled + ) + with gather_param_context: + yield diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/other.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/other.py new file mode 100644 index 0000000000000000000000000000000000000000..398639a164320b9f9f6a14ab82166ddaf8d75f4f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/other.py @@ -0,0 +1,568 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import platform +import re +import socket +from codecs import encode +from collections import OrderedDict +from functools import partial, reduce +from types import MethodType +from typing import Optional + +import numpy as np +import torch +from packaging.version import Version +from safetensors.torch import save_file as safe_save_file + +from ..commands.config.default import write_basic_config # noqa: F401 +from ..logging import get_logger +from ..state import PartialState +from .constants import FSDP_PYTORCH_VERSION +from .dataclasses import DistributedType +from .imports import ( + is_deepspeed_available, + is_numpy_available, + is_torch_distributed_available, + is_torch_xla_available, + is_weights_only_available, +) +from .modeling import id_tensor_storage +from .transformer_engine import convert_model +from .versions import is_torch_version + + +logger = get_logger(__name__) + + +if is_torch_xla_available(): + import torch_xla.core.xla_model as xm + + +def is_compiled_module(module: torch.nn.Module) -> bool: + """ + Check whether the module was compiled with torch.compile() + """ + if not hasattr(torch, "_dynamo"): + return False + + return isinstance(module, torch._dynamo.eval_frame.OptimizedModule) + + +def has_compiled_regions(module: torch.nn.Module) -> bool: + """ + Check whether the module has submodules that were compiled with `torch.compile()`. + """ + if not hasattr(torch, "_dynamo"): + return False + + if module._modules: + for submodule in module.modules(): + if isinstance(submodule, torch._dynamo.eval_frame.OptimizedModule): + return True + + return False + + +def is_repeated_blocks(module: torch.nn.Module) -> bool: + """ + Check whether the module is a repeated block, i.e. `torch.nn.ModuleList` with all children of the same class. This + is useful to determine whether we should apply regional compilation to the module. + """ + + return ( + isinstance(module, torch.nn.ModuleList) + and len(module) > 0 + and all(isinstance(m, module[0].__class__) for m in module) + ) + + +def has_repeated_blocks(module: torch.nn.Module) -> bool: + """ + Check whether the module has repeated blocks, i.e. `torch.nn.ModuleList` with all children of the same class, at + any level of the module hierarchy. This is useful to determine whether we should apply regional compilation to the + module. + """ + if module._modules: + for submodule in module.modules(): + if is_repeated_blocks(submodule): + return True + + return False + + +def compile_regions(module: torch.nn.Module, **compile_kwargs) -> torch.nn.Module: + """ + Performs regional compilation where we target repeated blocks of the same class and compile them sequentially to + hit the compiler's cache. For example, in `GPT2LMHeadModel`, the repeated block/class is `GPT2Block`, and can be + accessed as `model.transformer.h[0]`. The rest of the model (e.g. model.lm_head) is compiled separately. + + This allows us to speed up the compilation overhead / cold start of models like LLMs and Transformers in general. + See https://pytorch.org/tutorials/recipes/regional_compilation.html for more details. + + Args: + module (`torch.nn.Module`): + The model to compile. + **compile_kwargs: + Additional keyword arguments to pass to `torch.compile()`. + + Returns: + `torch.nn.Module`: A new instance of the model with some compiled regions. + + Example: + ```python + >>> from accelerate.utils import compile_regions + >>> from transformers import AutoModelForCausalLM + + >>> model = AutoModelForCausalLM.from_pretrained("gpt2") + >>> compiled_model = compile_regions(model, mode="reduce-overhead") + >>> compiled_model.transformer.h[0] + OptimizedModule( + (_orig_mod): GPT2Block( + (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True) + (attn): GPT2Attention( + (c_attn): Conv1D(nf=2304, nx=768) + (c_proj): Conv1D(nf=768, nx=768) + (attn_dropout): Dropout(p=0.1, inplace=False) + (resid_dropout): Dropout(p=0.1, inplace=False) + ) + (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True) + (mlp): GPT2MLP( + (c_fc): Conv1D(nf=3072, nx=768) + (c_proj): Conv1D(nf=768, nx=3072) + (act): NewGELUActivation() + (dropout): Dropout(p=0.1, inplace=False) + ) + ) + ) + ``` + """ + + def _compile_regions(module: torch.nn.Module, **compile_kwargs) -> torch.nn.Module: + if is_repeated_blocks(module): + new_module = torch.nn.ModuleList() + for submodule in module: + new_module.append(torch.compile(submodule, **compile_kwargs)) + elif has_repeated_blocks(module): + new_module = module.__class__.__new__(module.__class__) + new_module.__dict__.update(module.__dict__) + new_module._modules = {} + for name, submodule in module.named_children(): + new_module.add_module(name, _compile_regions(submodule, **compile_kwargs)) + else: + new_module = torch.compile(module, **compile_kwargs) + + return new_module + + new_module = _compile_regions(module, **compile_kwargs) + + if "_orig_mod" not in new_module.__dict__: + # Keeps a reference to the original module to decompile/unwrap it later + new_module.__dict__["_orig_mod"] = module + + return new_module + + +def compile_regions_deepspeed(module: torch.nn.Module, **compile_kwargs): + """ + Performs regional compilation the same way as `compile_regions`, but specifically for `DeepSpeedEngine.module`. + Since the model is wrapped in a `DeepSpeedEngine` and has many added hooks, offloaded parameters, etc that + `torch.compile(...)` interferes with, version of trgional compilation uses the inplace `module.compile()` method + instead. + + Args: + module (`torch.nn.Module`): + The model to compile. + **compile_kwargs: + Additional keyword arguments to pass to `module.compile()`. + """ + + if is_repeated_blocks(module): + for submodule in module: + submodule.compile(**compile_kwargs) + elif has_repeated_blocks(module): + for child in module.children(): + compile_regions_deepspeed(child, **compile_kwargs) + else: # leaf node + module.compile(**compile_kwargs) + + +def model_has_dtensor(model: torch.nn.Module) -> bool: + """ + Check if the model has DTensor parameters. + + Args: + model (`torch.nn.Module`): + The model to check. + + Returns: + `bool`: Whether the model has DTensor parameters. + """ + if is_torch_version(">=", "2.5.0"): + from torch.distributed.tensor import DTensor + else: + # from torch 2.0.0 (oldest supported accelerate torch version), DTensor is in torch.distributed._tensor + from torch.distributed._tensor import DTensor + + return any(isinstance(p, DTensor) for p in model.parameters()) + + +def extract_model_from_parallel( + model, keep_fp32_wrapper: bool = True, keep_torch_compile: bool = True, recursive: bool = False +): + """ + Extract a model from its distributed containers. + + Args: + model (`torch.nn.Module`): + The model to extract. + keep_fp32_wrapper (`bool`, *optional*): + Whether to remove mixed precision hooks from the model. + keep_torch_compile (`bool`, *optional*): + Whether to unwrap compiled model. + recursive (`bool`, *optional*, defaults to `False`): + Whether to recursively extract all cases of `module.module` from `model` as well as unwrap child sublayers + recursively, not just the top-level distributed containers. + + Returns: + `torch.nn.Module`: The extracted model. + """ + options = (torch.nn.parallel.DistributedDataParallel, torch.nn.DataParallel) + + is_compiled = is_compiled_module(model) + has_compiled = has_compiled_regions(model) + + compiled_model = None + if is_compiled: + compiled_model = model + model = model._orig_mod + elif has_compiled: + # Skip if top-level not compiled, subs stay wrapped + if "_orig_mod" in model.__dict__: + compiled_model = model + model = model.__dict__["_orig_mod"] + + if is_deepspeed_available(): + from deepspeed import DeepSpeedEngine + + options += (DeepSpeedEngine,) + + if is_torch_version(">=", FSDP_PYTORCH_VERSION) and is_torch_distributed_available(): + from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP + + options += (FSDP,) + + while isinstance(model, options): + model = model.module + + if recursive: + # This is needed in cases such as using FSDPv2 on XLA + def _recursive_unwrap(module): + # Wrapped modules are standardly wrapped as `module`, similar to the cases earlier + # with DDP, DataParallel, DeepSpeed, and FSDP + if hasattr(module, "module"): + unwrapped_module = _recursive_unwrap(module.module) + else: + unwrapped_module = module + # Next unwrap child sublayers recursively + for name, child in unwrapped_module.named_children(): + setattr(unwrapped_module, name, _recursive_unwrap(child)) + return unwrapped_module + + # Start with top-level + model = _recursive_unwrap(model) + + if not keep_fp32_wrapper: + forward = model.forward + original_forward = model.__dict__.pop("_original_forward", None) + if original_forward is not None: + while hasattr(forward, "__wrapped__"): + forward = forward.__wrapped__ + if forward == original_forward: + break + model.forward = MethodType(forward, model) + if getattr(model, "_converted_to_transformer_engine", False): + convert_model(model, to_transformer_engine=False) + + if keep_torch_compile and compiled_model is not None: + if is_compiled: + compiled_model._orig_mod = model + model = compiled_model + elif has_compiled: + compiled_model.__dict__["_orig_mod"] = model + model = compiled_model + + return model + + +def wait_for_everyone(): + """ + Introduces a blocking point in the script, making sure all processes have reached this point before continuing. + + + + Make sure all processes will reach this instruction otherwise one of your processes will hang forever. + + + """ + PartialState().wait_for_everyone() + + +def clean_state_dict_for_safetensors(state_dict: dict): + """ + Cleans the state dictionary from a model and removes tensor aliasing if present. + + Args: + state_dict (`dict`): + The state dictionary from a model + """ + ptrs = collections.defaultdict(list) + # When bnb serialization is used, weights in state dict can be strings + for name, tensor in state_dict.items(): + if not isinstance(tensor, str): + ptrs[id_tensor_storage(tensor)].append(name) + + # These are all pointers of tensors with shared memory + shared_ptrs = {ptr: names for ptr, names in ptrs.items() if len(names) > 1} + warn_names = set() + for names in shared_ptrs.values(): + # When not all duplicates have been cleaned, we still remove those keys but put a clear warning. + # If the link between tensors was done at runtime then `from_pretrained` will not get + # the key back leading to random tensor. A proper warning will be shown + # during reload (if applicable), but since the file is not necessarily compatible with + # the config, better show a proper warning. + found_names = [name for name in names if name in state_dict] + warn_names.update(found_names[1:]) + for name in found_names[1:]: + del state_dict[name] + if len(warn_names) > 0: + logger.warning( + f"Removed shared tensor {warn_names} while saving. This should be OK, but check by verifying that you don't receive any warning while reloading", + ) + state_dict = {k: v.contiguous() if isinstance(v, torch.Tensor) else v for k, v in state_dict.items()} + return state_dict + + +def save(obj, f, save_on_each_node: bool = False, safe_serialization: bool = False): + """ + Save the data to disk. Use in place of `torch.save()`. + + Args: + obj: + The data to save + f: + The file (or file-like object) to use to save the data + save_on_each_node (`bool`, *optional*, defaults to `False`): + Whether to only save on the global main process + safe_serialization (`bool`, *optional*, defaults to `False`): + Whether to save `obj` using `safetensors` or the traditional PyTorch way (that uses `pickle`). + """ + # When TorchXLA is enabled, it's necessary to transfer all data to the CPU before saving. + # Another issue arises with `id_tensor_storage`, which treats all XLA tensors as identical. + # If tensors remain on XLA, calling `clean_state_dict_for_safetensors` will result in only + # one XLA tensor remaining. + if PartialState().distributed_type == DistributedType.XLA: + obj = xm._maybe_convert_to_cpu(obj) + # Check if it's a model and remove duplicates + if safe_serialization: + save_func = partial(safe_save_file, metadata={"format": "pt"}) + if isinstance(obj, OrderedDict): + obj = clean_state_dict_for_safetensors(obj) + else: + save_func = torch.save + + if PartialState().is_main_process and not save_on_each_node: + save_func(obj, f) + elif PartialState().is_local_main_process and save_on_each_node: + save_func(obj, f) + + +# The following are considered "safe" globals to reconstruct various types of objects when using `weights_only=True` +# These should be added and then removed after loading in the file +np_core = np._core if is_numpy_available("2.0.0") else np.core +TORCH_SAFE_GLOBALS = [ + # numpy arrays are just numbers, not objects, so we can reconstruct them safely + np_core.multiarray._reconstruct, + np.ndarray, + # The following are needed for the RNG states + encode, + np.dtype, +] + +if is_numpy_available("1.25.0"): + TORCH_SAFE_GLOBALS.append(np.dtypes.UInt32DType) + + +def load(f, map_location=None, **kwargs): + """ + Compatible drop-in replacement of `torch.load()` which allows for `weights_only` to be used if `torch` version is + 2.4.0 or higher. Otherwise will ignore the kwarg. + + Will also add (and then remove) an exception for numpy arrays + + Args: + f: + The file (or file-like object) to use to load the data + map_location: + a function, `torch.device`, string or a dict specifying how to remap storage locations + **kwargs: + Additional keyword arguments to pass to `torch.load()`. + """ + try: + if is_weights_only_available(): + old_safe_globals = torch.serialization.get_safe_globals() + if "weights_only" not in kwargs: + kwargs["weights_only"] = True + torch.serialization.add_safe_globals(TORCH_SAFE_GLOBALS) + else: + kwargs.pop("weights_only", None) + loaded_obj = torch.load(f, map_location=map_location, **kwargs) + finally: + if is_weights_only_available(): + torch.serialization.clear_safe_globals() + if old_safe_globals: + torch.serialization.add_safe_globals(old_safe_globals) + return loaded_obj + + +def get_pretty_name(obj): + """ + Gets a pretty name from `obj`. + """ + if not hasattr(obj, "__qualname__") and not hasattr(obj, "__name__"): + obj = getattr(obj, "__class__", obj) + if hasattr(obj, "__qualname__"): + return obj.__qualname__ + if hasattr(obj, "__name__"): + return obj.__name__ + return str(obj) + + +def merge_dicts(source, destination): + """ + Recursively merges two dictionaries. + + Args: + source (`dict`): The dictionary to merge into `destination`. + destination (`dict`): The dictionary to merge `source` into. + """ + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + merge_dicts(value, node) + else: + destination[key] = value + + return destination + + +def is_port_in_use(port: Optional[int] = None) -> bool: + """ + Checks if a port is in use on `localhost`. Useful for checking if multiple `accelerate launch` commands have been + run and need to see if the port is already in use. + """ + if port is None: + port = 29500 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 + + +def get_free_port() -> int: + """ + Gets a free port on `localhost`. Useful for automatic port selection when port 0 is specified in distributed + training scenarios. + + Returns: + int: An available port number + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("", 0)) # bind to port 0 for OS to assign a free port + return s.getsockname()[1] + + +def convert_bytes(size): + "Converts `size` from bytes to the largest possible unit" + for x in ["bytes", "KB", "MB", "GB", "TB"]: + if size < 1024.0: + return f"{round(size, 2)} {x}" + size /= 1024.0 + + return f"{round(size, 2)} PB" + + +def check_os_kernel(): + """Warns if the kernel version is below the recommended minimum on Linux.""" + # see issue #1929 + info = platform.uname() + system = info.system + if system != "Linux": + return + + _, version, *_ = re.split(r"(\d+\.\d+\.\d+)", info.release) + min_version = "5.5.0" + if Version(version) < Version(min_version): + msg = ( + f"Detected kernel version {version}, which is below the recommended minimum of {min_version}; this can " + "cause the process to hang. It is recommended to upgrade the kernel to the minimum version or higher." + ) + logger.warning(msg, main_process_only=True) + + +def recursive_getattr(obj, attr: str): + """ + Recursive `getattr`. + + Args: + obj: + A class instance holding the attribute. + attr (`str`): + The attribute that is to be retrieved, e.g. 'attribute1.attribute2'. + """ + + def _getattr(obj, attr): + return getattr(obj, attr) + + return reduce(_getattr, [obj] + attr.split(".")) + + +def get_module_children_bottom_up(model: torch.nn.Module, return_fqns: bool = False) -> list[torch.nn.Module]: + """Traverse the model in bottom-up order and return the children modules in that order. + + Args: + model (`torch.nn.Module`): the model to get the children of + + Returns: + `list[torch.nn.Module]`: a list of children modules of `model` in bottom-up order. The last element is the + `model` itself. + """ + top = model if not return_fqns else ("", model) + stack = [top] + ordered_modules = [] + while stack: + current_module = stack.pop() + if return_fqns: + current_module_name, current_module = current_module + for name, attr in current_module.named_children(): + if isinstance(attr, torch.nn.Module): + if return_fqns: + child_name = current_module_name + "." + name if current_module_name else name + stack.append((child_name, attr)) + else: + stack.append(attr) + if return_fqns: + ordered_modules.append((current_module_name, current_module)) + else: + ordered_modules.append(current_module) + return ordered_modules[::-1] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/random.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/random.py new file mode 100644 index 0000000000000000000000000000000000000000..49639b417dfa41259223c468d6f56b5f7926502c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/random.py @@ -0,0 +1,165 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +from typing import Optional, Union + +import numpy as np +import torch + +from ..state import AcceleratorState +from .constants import CUDA_DISTRIBUTED_TYPES +from .dataclasses import DistributedType, RNGType +from .imports import ( + is_hpu_available, + is_mlu_available, + is_musa_available, + is_neuron_available, + is_npu_available, + is_sdaa_available, + is_torch_xla_available, + is_xpu_available, +) + + +if is_torch_xla_available(): + import torch_xla.core.xla_model as xm + + +def set_seed(seed: int, device_specific: bool = False, deterministic: bool = False): + """ + Helper function for reproducible behavior to set the seed in `random`, `numpy`, `torch`. + + Args: + seed (`int`): + The seed to set. + device_specific (`bool`, *optional*, defaults to `False`): + Whether to differ the seed on each device slightly with `self.process_index`. + deterministic (`bool`, *optional*, defaults to `False`): + Whether to use deterministic algorithms where available. Can slow down training. + """ + if device_specific: + seed += AcceleratorState().process_index + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + if is_xpu_available(): + torch.xpu.manual_seed_all(seed) + elif is_npu_available(): + torch.npu.manual_seed_all(seed) + elif is_mlu_available(): + torch.mlu.manual_seed_all(seed) + elif is_sdaa_available(): + torch.sdaa.manual_seed_all(seed) + elif is_musa_available(): + torch.musa.manual_seed_all(seed) + elif is_hpu_available(): + torch.hpu.manual_seed_all(seed) + elif is_neuron_available(): + torch.neuron.manual_seed_all(seed) + else: + torch.cuda.manual_seed_all(seed) + # ^^ safe to call this function even if cuda is not available + if is_torch_xla_available(): + xm.set_rng_state(seed) + + if deterministic: + torch.use_deterministic_algorithms(True) + + +def synchronize_rng_state(rng_type: Optional[RNGType] = None, generator: Optional[torch.Generator] = None): + # Get the proper rng state + if rng_type == RNGType.TORCH: + rng_state = torch.get_rng_state() + elif rng_type == RNGType.CUDA: + rng_state = torch.cuda.get_rng_state() + elif rng_type == RNGType.XLA: + assert is_torch_xla_available(), "Can't synchronize XLA seeds as torch_xla is unavailable." + rng_state = torch.tensor(xm.get_rng_state()) + elif rng_type == RNGType.NPU: + assert is_npu_available(), "Can't synchronize NPU seeds on an environment without NPUs." + rng_state = torch.npu.get_rng_state() + elif rng_type == RNGType.MLU: + assert is_mlu_available(), "Can't synchronize MLU seeds on an environment without MLUs." + rng_state = torch.mlu.get_rng_state() + elif rng_type == RNGType.SDAA: + assert is_sdaa_available(), "Can't synchronize SDAA seeds on an environment without SDAAs." + rng_state = torch.sdaa.get_rng_state() + elif rng_type == RNGType.MUSA: + assert is_musa_available(), "Can't synchronize MUSA seeds on an environment without MUSAs." + rng_state = torch.musa.get_rng_state() + elif rng_type == RNGType.XPU: + assert is_xpu_available(), "Can't synchronize XPU seeds on an environment without XPUs." + rng_state = torch.xpu.get_rng_state() + elif rng_type == RNGType.HPU: + assert is_hpu_available(), "Can't synchronize HPU seeds on an environment without HPUs." + rng_state = torch.hpu.get_rng_state() + elif rng_type == RNGType.NEURON: + assert is_neuron_available(), "Can't synchronize Neuron seeds on an environment without Neuron Cores." + rng_state = torch.neuron.get_rng_state() + elif rng_type == RNGType.GENERATOR: + assert generator is not None, "Need a generator to synchronize its seed." + rng_state = generator.get_state() + + # Broadcast the rng state from device 0 to other devices + state = AcceleratorState() + if state.distributed_type == DistributedType.XLA: + rng_state = rng_state.to(xm.xla_device()) + xm.collective_broadcast([rng_state]) + xm.mark_step() + rng_state = rng_state.cpu() + elif ( + state.distributed_type in CUDA_DISTRIBUTED_TYPES + or state.distributed_type == DistributedType.MULTI_MLU + or state.distributed_type == DistributedType.MULTI_SDAA + or state.distributed_type == DistributedType.MULTI_MUSA + or state.distributed_type == DistributedType.MULTI_NPU + or state.distributed_type == DistributedType.MULTI_XPU + or state.distributed_type == DistributedType.MULTI_HPU + or state.distributed_type == DistributedType.MULTI_NEURON + ): + rng_state = rng_state.to(state.device) + torch.distributed.broadcast(rng_state, 0) + rng_state = rng_state.cpu() + elif state.distributed_type == DistributedType.MULTI_CPU: + torch.distributed.broadcast(rng_state, 0) + + # Set the broadcast rng state + if rng_type == RNGType.TORCH: + torch.set_rng_state(rng_state) + elif rng_type == RNGType.CUDA: + torch.cuda.set_rng_state(rng_state) + elif rng_type == RNGType.NPU: + torch.npu.set_rng_state(rng_state) + elif rng_type == RNGType.MLU: + torch.mlu.set_rng_state(rng_state) + elif rng_type == RNGType.SDAA: + torch.sdaa.set_rng_state(rng_state) + elif rng_type == RNGType.MUSA: + torch.musa.set_rng_state(rng_state) + elif rng_type == RNGType.XPU: + torch.xpu.set_rng_state(rng_state) + elif rng_type == RNGType.HPU: + torch.hpu.set_rng_state(rng_state) + elif rng_type == RNGType.NEURON: + torch.neuron.set_rng_state(rng_state) + elif rng_type == RNGType.XLA: + xm.set_rng_state(rng_state.item()) + elif rng_type == RNGType.GENERATOR: + generator.set_state(rng_state) + + +def synchronize_rng_states(rng_types: list[Union[str, RNGType]], generator: Optional[torch.Generator] = None): + for rng_type in rng_types: + synchronize_rng_state(RNGType(rng_type), generator=generator) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/rich.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/rich.py new file mode 100644 index 0000000000000000000000000000000000000000..2d48661b7fcef92ef1168b74cc275c6d3ccc67a1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/rich.py @@ -0,0 +1,24 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .imports import is_rich_available + + +if is_rich_available(): + from rich.traceback import install + + install(show_locals=False) + +else: + raise ModuleNotFoundError("To use the rich extension, install rich with `pip install rich`") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/torch_xla.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/torch_xla.py new file mode 100644 index 0000000000000000000000000000000000000000..140133926c2f88d39c70f5a9f46a08f88bed36da --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/torch_xla.py @@ -0,0 +1,51 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib.metadata +import subprocess +import sys + + +def install_xla(upgrade: bool = False): + """ + Helper function to install appropriate xla wheels based on the `torch` version in Google Colaboratory. + + Args: + upgrade (`bool`, *optional*, defaults to `False`): + Whether to upgrade `torch` and install the latest `torch_xla` wheels. + + Example: + + ```python + >>> from accelerate.utils import install_xla + + >>> install_xla(upgrade=True) + ``` + """ + in_colab = False + if "IPython" in sys.modules: + in_colab = "google.colab" in str(sys.modules["IPython"].get_ipython()) + + if in_colab: + if upgrade: + torch_install_cmd = ["pip", "install", "-U", "torch"] + subprocess.run(torch_install_cmd, check=True) + # get the current version of torch + torch_version = importlib.metadata.version("torch") + torch_version_trunc = torch_version[: torch_version.rindex(".")] + xla_wheel = f"https://storage.googleapis.com/tpu-pytorch/wheels/colab/torch_xla-{torch_version_trunc}-cp37-cp37m-linux_x86_64.whl" + xla_install_cmd = ["pip", "install", xla_wheel] + subprocess.run(xla_install_cmd, check=True) + else: + raise RuntimeError("`install_xla` utility works only on google colab.") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/tqdm.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/tqdm.py new file mode 100644 index 0000000000000000000000000000000000000000..2d4873c1573eb2ee7392162f440a76d4f07cd8ce --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/tqdm.py @@ -0,0 +1,43 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from .imports import is_tqdm_available + + +if is_tqdm_available(): + from tqdm.auto import tqdm as _tqdm + +from ..state import PartialState + + +def tqdm(*args, main_process_only: bool = True, **kwargs): + """ + Wrapper around `tqdm.tqdm` that optionally displays only on the main process. + + Args: + main_process_only (`bool`, *optional*): + Whether to display the progress bar only on the main process + """ + if not is_tqdm_available(): + raise ImportError("Accelerate's `tqdm` module requires `tqdm` to be installed. Please run `pip install tqdm`.") + if len(args) > 0 and isinstance(args[0], bool): + raise ValueError( + "Passing `True` or `False` as the first argument to Accelerate's `tqdm` wrapper is unsupported. " + "Please use the `main_process_only` keyword argument instead." + ) + disable = kwargs.pop("disable", False) + if main_process_only and not disable: + disable = PartialState().local_process_index != 0 + return _tqdm(*args, **kwargs, disable=disable) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/transformer_engine.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/transformer_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..31b61364fe2630f2b03a0ac64749333b14f6eada --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/transformer_engine.py @@ -0,0 +1,186 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from types import MethodType + +import torch.nn as nn + +from .imports import is_hpu_available, is_transformer_engine_available +from .operations import GatheredParameters + + +# Do not import `transformer_engine` at package level to avoid potential issues + + +def convert_model(model, to_transformer_engine=True, _convert_linear=True, _convert_ln=True): + """ + Recursively converts the linear and layernorm layers of a model to their `transformers_engine` counterpart. + """ + if not is_transformer_engine_available(): + raise ImportError("Using `convert_model` requires transformer_engine to be installed.") + + if is_hpu_available(): + import intel_transformer_engine as te + + if not hasattr(te, "LayerNorm"): + # HPU does not have a LayerNorm implementation in TE + te.LayerNorm = nn.LayerNorm + else: + import transformer_engine.pytorch as te + + for name, module in model.named_children(): + if isinstance(module, nn.Linear) and to_transformer_engine and _convert_linear: + has_bias = module.bias is not None + params_to_gather = [module.weight] + if has_bias: + params_to_gather.append(module.bias) + + with GatheredParameters(params_to_gather, modifier_rank=0): + if any(p % 16 != 0 for p in module.weight.shape): + return + te_module = te.Linear( + module.in_features, module.out_features, bias=has_bias, params_dtype=module.weight.dtype + ) + te_module.weight.copy_(module.weight) + if has_bias: + te_module.bias.copy_(module.bias) + + setattr(model, name, te_module) + # Note: @xrsrke (Phuc) found that te.LayerNorm doesn't have any real memory savings or speedups over nn.LayerNorm + elif isinstance(module, nn.LayerNorm) and to_transformer_engine and _convert_ln: + with GatheredParameters([module.weight, module.bias], modifier_rank=0): + has_bias = module.bias is not None + te_module = te.LayerNorm(module.normalized_shape[0], eps=module.eps, params_dtype=module.weight.dtype) + te_module.weight.copy_(module.weight) + if has_bias: + te_module.bias.copy_(module.bias) + + setattr(model, name, te_module) + elif isinstance(module, te.Linear) and not to_transformer_engine and _convert_linear: + has_bias = module.bias is not None + new_module = nn.Linear( + module.in_features, module.out_features, bias=has_bias, params_dtype=module.weight.dtype + ) + new_module.weight.copy_(module.weight) + if has_bias: + new_module.bias.copy_(module.bias) + + setattr(model, name, new_module) + elif isinstance(module, te.LayerNorm) and not to_transformer_engine and _convert_ln: + new_module = nn.LayerNorm(module.normalized_shape[0], eps=module.eps, params_dtype=module.weight.dtype) + new_module.weight.copy_(module.weight) + new_module.bias.copy_(module.bias) + + setattr(model, name, new_module) + else: + convert_model( + module, + to_transformer_engine=to_transformer_engine, + _convert_linear=_convert_linear, + _convert_ln=_convert_ln, + ) + + +def has_transformer_engine_layers(model): + """ + Returns whether a given model has some `transformer_engine` layer or not. + """ + if not is_transformer_engine_available(): + raise ImportError("Using `has_transformer_engine_layers` requires transformer_engine to be installed.") + + if is_hpu_available(): + import intel_transformer_engine as te + + module_cls_to_check = te.Linear + else: + import transformer_engine.pytorch as te + + module_cls_to_check = (te.LayerNorm, te.Linear, te.TransformerLayer) + + for m in model.modules(): + if isinstance(m, module_cls_to_check): + return True + + return False + + +def contextual_fp8_autocast(model_forward, fp8_recipe, use_during_eval=False): + """ + Wrapper for a model's forward method to apply FP8 autocast. Is context aware, meaning that by default it will + disable FP8 autocast during eval mode, which is generally better for more accurate metrics. + """ + if not is_transformer_engine_available(): + raise ImportError("Using `contextual_fp8_autocast` requires transformer_engine to be installed.") + + if is_hpu_available(): + from intel_transformer_engine import fp8_autocast + else: + from transformer_engine.pytorch import fp8_autocast + + def forward(self, *args, **kwargs): + enabled = use_during_eval or self.training + with fp8_autocast(enabled=enabled, fp8_recipe=fp8_recipe): + return model_forward(*args, **kwargs) + + # To act like a decorator so that it can be popped when doing `extract_model_from_parallel` + forward.__wrapped__ = model_forward + + return forward + + +def apply_fp8_autowrap(model, fp8_recipe_handler): + """ + Applies FP8 context manager to the model's forward method + """ + if not is_transformer_engine_available(): + raise ImportError("Using `apply_fp8_autowrap` requires transformer_engine to be installed.") + + if is_hpu_available(): + import intel_transformer_engine.recipe as te_recipe + + is_fp8_block_scaling_available = False + message = "MXFP8 block scaling is not available on HPU." + + else: + import transformer_engine.common.recipe as te_recipe + from transformer_engine.pytorch.fp8 import check_mxfp8_support + + is_fp8_block_scaling_available, message = check_mxfp8_support() + + kwargs = fp8_recipe_handler.to_kwargs() if fp8_recipe_handler is not None else {} + if "fp8_format" in kwargs: + kwargs["fp8_format"] = getattr(te_recipe.Format, kwargs["fp8_format"]) + use_during_eval = kwargs.pop("use_autocast_during_eval", False) + use_mxfp8_block_scaling = kwargs.pop("use_mxfp8_block_scaling", False) + + if use_mxfp8_block_scaling and not is_fp8_block_scaling_available: + raise ValueError(f"MXFP8 block scaling is not available: {message}") + + if use_mxfp8_block_scaling: + if "amax_compute_algo" in kwargs: + raise ValueError("`amax_compute_algo` is not supported for MXFP8 block scaling.") + if "amax_history_len" in kwargs: + raise ValueError("`amax_history_len` is not supported for MXFP8 block scaling.") + fp8_recipe = te_recipe.MXFP8BlockScaling(**kwargs) + else: + fp8_recipe = te_recipe.DelayedScaling(**kwargs) + + new_forward = contextual_fp8_autocast(model.forward, fp8_recipe, use_during_eval) + + if hasattr(model.forward, "__func__"): + model.forward = MethodType(new_forward, model) + else: + model.forward = new_forward + + return model diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/versions.py b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/versions.py new file mode 100644 index 0000000000000000000000000000000000000000..985c918f0e057bacc70c372f6906071bb73db577 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/accelerate/utils/versions.py @@ -0,0 +1,56 @@ +# Copyright 2022 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import importlib.metadata +from typing import Union + +from packaging.version import Version, parse + +from .constants import STR_OPERATION_TO_FUNC + + +torch_version = parse(importlib.metadata.version("torch")) + + +def compare_versions(library_or_version: Union[str, Version], operation: str, requirement_version: str): + """ + Compares a library version to some requirement using a given operation. + + Args: + library_or_version (`str` or `packaging.version.Version`): + A library name or a version to check. + operation (`str`): + A string representation of an operator, such as `">"` or `"<="`. + requirement_version (`str`): + The version to compare the library version against + """ + if operation not in STR_OPERATION_TO_FUNC.keys(): + raise ValueError(f"`operation` must be one of {list(STR_OPERATION_TO_FUNC.keys())}, received {operation}") + operation = STR_OPERATION_TO_FUNC[operation] + if isinstance(library_or_version, str): + library_or_version = parse(importlib.metadata.version(library_or_version)) + return operation(library_or_version, parse(requirement_version)) + + +def is_torch_version(operation: str, version: str): + """ + Compares the current PyTorch version to a given reference with an operation. + + Args: + operation (`str`): + A string representation of an operator, such as `">"` or `"<="` + version (`str`): + A string version of PyTorch + """ + return compare_versions(torch_version, operation, version) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/diffusers-0.37.0.dist-info/licenses/LICENSE b/URSA/.venv_ursa/lib/python3.12/site-packages/diffusers-0.37.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..038e32f6445e8f265bde482613cf0d2f43d86dbc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/diffusers-0.37.0.dist-info/licenses/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, Any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79206c24ca645a070ef522b9c38c22667e9f8444 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef12bdc415a9dc36c594915e77c3373fb12b2741 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..94f258df84ba8730208768fc44222bee4b3ebc33 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__init__.py @@ -0,0 +1,8 @@ +# This file has moved to under torch/_functorch. It is not public API. +# If you are not a PyTorch developer and you are relying on the following +# imports, please file an issue. +from torch._functorch.aot_autograd import ( + aot_autograd_decompositions, + KNOWN_TYPES, + PytreeThunk, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..881afda88acb020e66de2d28adced0fbf9bdefe6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/aot_autograd/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6052b5548f4af3dbc6d9d45b0ffe72a8d5013d41 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__init__.py @@ -0,0 +1,7 @@ +# This file has moved to under torch/_functorch. It is not public API. +# If you are not a PyTorch developer and you are relying on the following +# imports, please file an issue. +from torch._functorch.eager_transforms import ( + _assert_wrapped_functional, + _unwrap_functional_tensor, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..646591117dd5e425b4702bf63a537f4f561fd93f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/eager_transforms/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3de7787df0c3304207b42b51e9fb62da9d33c7d0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__init__.py @@ -0,0 +1,4 @@ +# This file has moved to under torch/_functorch. It is not public API. +# If you are not a PyTorch developer and you are relying on the following +# imports, please file an issue. +from torch._functorch.make_functional import _swap_state diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d31fd65c5007e9d8376e3f42caeabaa45672b3f5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/make_functional/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dc90517753e50f92362ba954248e31f69f7cfcd5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__init__.py @@ -0,0 +1,16 @@ +# This file has moved to under torch/_functorch. It is not public API. +# If you are not a PyTorch developer and you are relying on the following +# imports, please file an issue. +from torch._functorch.vmap import ( + _add_batch_dim, + _broadcast_to_and_flatten, + _create_batched_inputs, + _get_name, + _process_batched_inputs, + _remove_batch_dim, + _unwrap_batched, + _validate_and_get_batch_size, + Tensor, + tree_flatten, + tree_unflatten, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f937e9dbe0330c915495a47e5df865df22ae7a3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/_src/vmap/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e7548a5ff6b91bae4fa561f0de7ad5d3492eda05 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__init__.py @@ -0,0 +1,30 @@ +from torch._functorch import config +from torch._functorch.aot_autograd import ( + aot_function, + aot_module, + aot_module_simplified, + compiled_function, + compiled_module, + get_aot_compilation_context, + get_aot_graph_name, + get_graph_being_compiled, + make_boxed_compiler, + make_boxed_func, +) +from torch._functorch.compilers import ( + debug_compile, + default_decompositions, + draw_graph_compile, + memory_efficient_fusion, + nnc_jit, + nop, + print_compile, + ts_compile, +) +from torch._functorch.fx_minifier import minifier +from torch._functorch.partitioners import ( + default_partition, + draw_graph, + min_cut_rematerialization_partition, +) +from torch._functorch.python_key import pythonkey_decompose diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9121b496edc70c93464873258ce2661f44e23f8a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/compile/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..df9ca766e28f68440262e17c0d9b0566656ae344 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__init__.py @@ -0,0 +1,1592 @@ +from __future__ import annotations + +import dis +import inspect +import sys +from typing import Any, Optional, TYPE_CHECKING, Union + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + +import torch +from torch.utils._pytree import tree_flatten, tree_map, tree_unflatten + +from ._dim_entry import _match_levels, DimEntry, ndim_of_levels +from ._enable_all_layers import EnableAllLayers +from ._py_inst_decoder import _PyInstDecoder +from ._tensor_info import TensorInfo + + +POINTWISE_OPTIMIZE = True +DOT_OPTIMIZED = True + +# Global dimension level counter +_n_dims_created = 0 + + +def _relevant_op(opcode: Optional[str]) -> bool: + """Check if opcode is relevant for variable assignment.""" + return bool(opcode and opcode.startswith("STORE_")) + + +def handle_from_tensor(tensor: torch.Tensor) -> torch.Tensor: + """Handle tensor conversion for torch function integration.""" + return tensor + + +def _create_dim(name: str, size: Optional[int] = None) -> Dim: + """Create a new Dim object.""" + return Dim(name, size if size is not None else -1) + + +def dims( + n: Optional[int] = None, sizes: Optional[list[Optional[int]]] = None +) -> Union[Dim, tuple[Dim, ...]]: + """ + Create and return one or more Dim objects. + + Uses bytecode inspection to determine variable names when possible. + + Args: + n (int, optional): The number of dimensions to create. Can be omitted if sizes is specified. + sizes (List[Optional[int]], optional): A list the same size as the number of dimensions to be + created, specifying each dimensions size, or None to leave the size unset. + + Returns: + Union[Dim, Tuple[Dim, ...]]: Single Dim if n=1, tuple of Dims otherwise. + + Examples: + >>> batch, channel, width, height = dims(4) + >>> batch, channel, width, height = dims(sizes=[None, 3, 224, 224]) + >>> single_dim = dims(1) + """ + specified_ndims = -1 + found_ndims = 0 + + # Parse arguments + if sizes is not None: + specified_ndims = len(sizes) + if n is not None: + specified_ndims = n + + # Use bytecode inspection + frame = inspect.currentframe() + if frame is None: + raise RuntimeError("Unable to get current frame") + frame = frame.f_back + try: + if frame is None: + raise RuntimeError("Unable to get caller frame") + code = frame.f_code + lasti = frame.f_lasti + + decoder = _PyInstDecoder(code, lasti) + + if sys.version_info >= (3, 11): + if decoder.opcode() == "PRECALL": + decoder.next() + + # Move to next instruction after the call + decoder.next() + + # Determine number of dimensions from bytecode + if _relevant_op(decoder.opcode()): + found_ndims = 1 + elif decoder.opcode() == "UNPACK_SEQUENCE": + found_ndims = decoder.oparg() + decoder.next() # Move past UNPACK_SEQUENCE + + if specified_ndims == -1: + if found_ndims == 0: + raise SyntaxError( + "dims() must be assigned to a sequence of variable names or have argument n specified" + ) + specified_ndims = found_ndims + + if found_ndims != specified_ndims: + found_ndims = 0 + + def genobject(i: int) -> Dim: + nonlocal found_ndims + name = None + if i < found_ndims: + name = decoder.name() + + if not name: + name = f"d{i}" + found_ndims = 0 + else: + decoder.next() # Move to next STORE instruction + + size = sizes[i] if sizes is not None else None + return _create_dim(name, size) + + # Validate sizes parameter + if sizes is not None and len(sizes) != specified_ndims: + raise ValueError(f"expected {specified_ndims} sizes but found {len(sizes)}") + + if specified_ndims == 1: + return genobject(0) + + result = [] + for i in range(specified_ndims): + result.append(genobject(i)) + + return tuple(result) + + finally: + del frame + + +class DimList: + """ + A list of first-class dimensions that can be bound to tensor dimensions. + + A DimList can be in one of two states: + 1. Unbound: Created with just a name, no specific dimensions yet + 2. Bound: Either created with specific dimensions/sizes, or bound later via bind() or bind_len() + """ + + _name: Optional[str] + _dims: list[Dim] + _bound: bool + + def __init__( + self, + len_or_dims: Optional[Union[int, Sequence]] = None, + name: Optional[str] = None, + ): + """ + Initialize a new DimList object. + + Args: + len_or_dims: Optional length (int) or sequence of dimensions/sizes + name: Optional name for the dimension list + """ + # Initialize attributes + self._name = name + self._dims: list = [] + self._bound = False + + if isinstance(len_or_dims, int): + self.bind_len(len_or_dims) + elif len_or_dims is not None: + dims = [] + for i, item in enumerate(len_or_dims): + if isinstance(item, int): + dim_name = f"{self._name}{i}" if self._name else f"dim{i}" + dims.append(Dim(dim_name, item)) + else: + dims.append(Dim(item)) + self._set_dims(dims) + + def _set_dims(self, dims: list) -> None: + """Set the dimensions and mark as bound.""" + self._bound = True + self._dims = dims + + def bind_len(self, size: int) -> None: + """ + Bind this DimList to a specific length. + + Args: + size: Number of dimensions to bind to + + Raises: + DimensionBindError: If already bound to a different size + """ + if self._bound: + if len(self._dims) != size: + raise DimensionBindError( + f"Dimlist has size {len(self._dims)} but it is being bound to size {size}" + ) + else: + self._bound = True + self._dims = [] + for i in range(size): + dim_name = f"{self._name}{i}" if self._name else f"dim{i}" + self._dims.append(Dim(dim_name)) + + def bind(self, sizes: Sequence[int]) -> None: + """ + Bind this DimList to specific sizes. + + Args: + sizes: Sequence of sizes for each dimension + + Raises: + ValueError: If sizes is not a sequence + """ + if not hasattr(sizes, "__len__") or not hasattr(sizes, "__getitem__"): + raise ValueError("expected a sequence") + + size = len(sizes) + self.bind_len(size) + + for i, dim_size in enumerate(sizes): + self._dims[i].size = int(dim_size) + + def _size(self) -> int: + if not self._bound: + raise DimensionBindError("DimList not bound") + return len(self._dims) + + def size(self) -> int: + """Return the size (number of dimensions) of this DimList.""" + return self._size() + + def _set_bound(self, b: bool) -> None: + """Set the bound status (for internal use).""" + self._bound = b + + @property + def is_bound(self) -> bool: + """Property to check if DimList is bound.""" + return self._bound + + def __len__(self) -> int: + """Return the length of the DimList.""" + return self.size() + + def __getitem__(self, key: Union[int, slice]) -> Union[Dim, tuple[Dim, ...]]: + if not self._bound: + raise DimensionBindError("DimList not bound") + + if isinstance(key, int): + if key < 0 or key >= len(self._dims): + raise IndexError("index out of bounds") + return self._dims[key] + elif isinstance(key, slice): + start, stop, step = key.indices(len(self._dims)) + result = [] + for i in range(start, stop, step): + result.append(self._dims[i]) + return tuple(result) + else: + raise ValueError("expected an int or a slice") + + def __repr__(self) -> str: + """Return string representation of the DimList.""" + if self._bound: + # Show as tuple representation + return f"({', '.join(repr(dim) for dim in self._dims)})" + elif self._name is not None: + # Show as *name for unbound with name + return f"*{self._name}" + else: + # Show as for unbound without name + return "" + + def __str__(self) -> str: + """Return string representation of the DimList.""" + return self.__repr__() + + @classmethod + def __torch_function__( + cls, + func: Callable, + types: tuple, + args: tuple = (), + kwargs: Optional[dict] = None, + ) -> Any: + return _Tensor.__torch_function__(func, types, args, kwargs) + + +def _create_dimlist( + name: str, size: Optional[Union[int, list[Optional[int]]]] = None +) -> DimList: + """Create a DimList object with the given name and optional size.""" + dimlist = DimList(name=name) + if size is not None: + if isinstance(size, int): + dimlist.bind_len(size) + else: + # size is a list of optional ints + dimlist.bind_len(len(size)) + for i, s in enumerate(size): + if s is not None: + dimlist._dims[i].size = s + return dimlist + + +def dimlists( + n: Optional[int] = None, sizes: Optional[list[Optional[int]]] = None +) -> Union[DimList, tuple[DimList, ...]]: + """ + Create and return one or more DimList objects. + + Similar to dims() but creates DimList objects instead. + """ + specified_ndims = -1 + found_ndims = 0 + + # Parse arguments + if sizes is not None: + specified_ndims = len(sizes) + if n is not None: + specified_ndims = n + + frame = inspect.currentframe() + if frame is None: + raise RuntimeError("Unable to get current frame") + frame = frame.f_back + try: + if frame is None: + raise RuntimeError("Unable to get caller frame") + code = frame.f_code + lasti = frame.f_lasti + + decoder = _PyInstDecoder(code, lasti) + + if sys.version_info >= (3, 11): + if decoder.opcode() == "PRECALL": + decoder.next() + + # Move to next instruction after the call + decoder.next() + + # Determine number of dimensions from bytecode + if _relevant_op(decoder.opcode()): + found_ndims = 1 + elif decoder.opcode() == "UNPACK_SEQUENCE": + found_ndims = decoder.oparg() + decoder.next() # Move past UNPACK_SEQUENCE + + if specified_ndims == -1: + if found_ndims == 0: + raise SyntaxError( + "dimlists() must be assigned to a sequence of variable names or have argument n specified" + ) + specified_ndims = found_ndims + + if found_ndims != specified_ndims: + found_ndims = 0 + + # Generator function for dimlist names + def genobject(i: int) -> str: + nonlocal found_ndims + name = None + if i < found_ndims: + name = decoder.name() + + if not name: + name = f"d{i}" + found_ndims = 0 + else: + decoder.next() # Move to next STORE instruction + + return name + + # Validate sizes + if sizes is not None and len(sizes) != specified_ndims: + raise ValueError(f"expected {specified_ndims} sizes but found {len(sizes)}") + + # Create dimlists + if specified_ndims == 1: + name = genobject(0) + return _create_dimlist(name, sizes[0] if sizes is not None else None) + + result = [] + for i in range(specified_ndims): + name = genobject(i) + size = sizes[i] if sizes is not None else None + result.append(_create_dimlist(name, size)) + + return tuple(result) + + finally: + del frame + + +class DimensionMismatchError(Exception): + pass + + +class DimensionBindError(Exception): + pass + + +from . import op_properties + + +def _safe_print(*args: Any, **kwargs: Any) -> None: + """Safe print that avoids recursive torch function dispatches.""" + import sys + + # Convert any torch objects to basic representations + safe_args = [] + for arg in args: + if hasattr(arg, "__class__") and "torch" in str(type(arg)): + safe_args.append(f"<{type(arg).__name__}>") + else: + safe_args.append(str(arg)) + + print(*safe_args, **kwargs, file=sys.stderr) + + +class _Tensor: + def _get_levels(self) -> list[Any]: + raise NotImplementedError("_get_levels must be implemented by subclass") + + def _get_tensor(self) -> Optional[torch.Tensor]: + raise NotImplementedError("_get_tensor must be implemented by subclass") + + @property + def ndim(self) -> int: + raise NotImplementedError("ndim must be implemented by subclass") + + @property + def dims(self) -> tuple[Any, ...]: + return tuple(l.dim() for l in self._get_levels() if not l.is_positional()) + + def dim(self) -> int: + return self.ndim + + @classmethod + def __torch_function__( + cls, + func: Callable, + types: tuple, + args: tuple = (), + kwargs: Optional[dict] = None, + ) -> Any: + if kwargs is None: + kwargs = {} + + if DOT_OPTIMIZED and func is torch.Tensor.__mul__: + # Check conditions: 2 args, both are tensor-like, both 0-dimensional + if ( + len(args) == 2 + and not kwargs + and isinstance(args[0], (_Tensor, torch.Tensor)) + and isinstance(args[1], (_Tensor, torch.Tensor)) + ): + # Get tensor info for both operands + lhs_info = TensorInfo.create( + args[0], ensure_batched=False, ensure_present=False + ) + rhs_info = TensorInfo.create( + args[1], ensure_batched=False, ensure_present=False + ) + + if ( + lhs_info + and rhs_info + and lhs_info.tensor is not None + and rhs_info.tensor is not None + and lhs_info.tensor.dim() == 0 + and rhs_info.tensor.dim() == 0 + ): + if ( + lhs_info.tensor.is_floating_point() + and rhs_info.tensor.is_floating_point() + ): + # Collect all unique levels and has_device + has_device = lhs_info.has_device or rhs_info.has_device + levels = [] + + for level in lhs_info.levels: + if level not in levels: + levels.append(level) + for level in rhs_info.levels: + if level not in levels: + levels.append(level) + + # Debug print + # print(f"DEBUG: Creating delayed mul, levels: {levels}, has_device: {has_device}") + + # Create delayed tensor + return Tensor.create_delayed(func, args, levels, has_device) + + if func is torch.Tensor.__getitem__: + from functorch.dim._getsetitem import getitem + + return getitem(cls, func, types, args, kwargs) + + if func is torch.Tensor.__setitem__: + from functorch.dim._getsetitem import setitem + + # args should be (tensor, index, value) + if len(args) == 3: + setitem(args[0], args[1], args[2]) + return None + else: + raise ValueError(f"Expected 3 args for __setitem__, got {len(args)}") + + # Fast-path for len; mostly to avoid infinite loop in TestMinFunctorchOnly.test_softmax_split + if func is torch.Tensor.__len__: + return args[0].size(0) + + # Special handling for torch.softmax - use the pre-wrapped version + if func is torch.softmax: + return softmax(*args, **kwargs) + + # Special handling for torch.stack - use the custom stack function + if func is torch.stack: + return stack(*args, **kwargs) + + if ( + func is torch.Tensor.split + or func is torch._VF.split # type: ignore[attr-defined] + or func is torch._VF.split_with_sizes # type: ignore[attr-defined] + or func is torch.split + ): + return split(*args, **kwargs) + + return _Tensor._torch_function_fallback(func, types, args, kwargs) + + @staticmethod + def _torch_function_fallback( + func: Callable, types: tuple, args: tuple, kwargs: dict + ) -> Any: + """Fallback torch function implementation for non-special-cased functions.""" + is_pointwise = POINTWISE_OPTIMIZE and func in op_properties.pointwise + # TODO: optimize pytree here + flat_args, spec = tree_flatten((args, kwargs)) + device_holding_tensor = None + + infos: list[TensorInfo] = [] + result_levels: list[DimEntry] = [] + + for f in flat_args: + info = TensorInfo.create(f, not is_pointwise, False) + infos.append(info) + if info: + assert is_pointwise or info.batchedtensor is not None + if device_holding_tensor is None and info.has_device: + device_holding_tensor = info.tensor + # Collect all unique levels + for level in info.levels: + assert isinstance(level, DimEntry) + if level not in result_levels: + result_levels.append(level) + + if is_pointwise: + # Pointwise operation: match all tensors to common levels + for i, info in enumerate(infos): + if info and info.tensor is not None: + tensor = info.tensor + if device_holding_tensor is not None and not info.has_device: + tensor = tensor.to(device_holding_tensor.device) + ml = _match_levels(tensor, info.levels, result_levels) + flat_args[i] = handle_from_tensor(ml) + + unflat_args, unflat_kwargs = tree_unflatten(flat_args, spec) + result = func(*unflat_args, **unflat_kwargs) + + # Wrap tensor results + def wrap_tensor(obj: Any) -> Any: + if isinstance(obj, torch.Tensor): + return Tensor.from_positional( + obj, result_levels, device_holding_tensor is not None + ) + return obj + + # Small fastpath + if isinstance(result, torch.Tensor): + return wrap_tensor(result) + else: + return tree_map(wrap_tensor, result) + + # Non-pointwise operation: use functorch vmap layers + with EnableAllLayers(result_levels) as guard: + # Update arguments with batched tensors + for i, info in enumerate(infos): + if info and info.batchedtensor is not None: + batched = info.batchedtensor + if device_holding_tensor is not None and not info.has_device: + batched = batched.to(device_holding_tensor.device) + guard.inplace_update_layers(batched, info.levels) + flat_args[i] = handle_from_tensor(batched) + + unflat_args, unflat_kwargs = tree_unflatten(flat_args, spec) + result = func(*unflat_args, **unflat_kwargs) + + # Unwrap results from functorch layers + def unwrap_tensor(obj: Any) -> Any: + if isinstance(obj, torch.Tensor): + return guard.from_batched(obj, device_holding_tensor is not None) + return obj + + if isinstance(result, torch.Tensor): + return unwrap_tensor(result) + else: + return tree_map(unwrap_tensor, result) + + def __setitem__(self, index: Any, value: Any) -> None: + """Set values in tensor using first-class dimensions.""" + from functorch.dim._getsetitem import setitem + + return setitem(self, index, value) + + # expand and index are OK to be methods because they don't have torch.* + # versions, but if they did they need the stack/cat treatment + + def expand(self, *args: Dim) -> _Tensor: + """ + Expand tensor by adding new dimensions or expanding existing dimensions. + + If all arguments are Dim objects, adds new named dimensions. + Otherwise, falls back to regular tensor expansion behavior. + + Args: + args: Either Dim objects for new dimensions or sizes for regular expansion + + Returns: + New tensor with expanded dimensions + + Example: + >>> i, j = dims() + >>> t = torch.randn(3, 4) + >>> expanded = t[i].expand(j, k) # Add j, k dimensions + >>> expanded2 = t[i].expand(2, 4) # Regular expand with sizes + """ + info = TensorInfo.create(self, ensure_batched=False, ensure_present=False) + + for arg in args: + if not isinstance(arg, Dim): + # Not all args are Dims, fallback to regular expand + if isinstance(self, torch.Tensor) and not isinstance(self, _Tensor): + return torch.Tensor.expand(self, *args) + else: + return self.__torch_function__( + torch.Tensor.expand, (type(self),), (self,) + args + ) + + # All args are Dim objects - proceed with first-class dimension expansion + if not info: + # No tensor info available, fallback + return self.__torch_function__( + torch.Tensor.expand, (type(self),), (self,) + args + ) + + # First-class dimension expansion - all args are Dim objects + data = info.tensor + if data is None: + # No tensor data available, fallback + return self.__torch_function__( + torch.Tensor.expand, (type(self),), (self,) + args + ) + + levels = info.levels + + new_levels: list[DimEntry] = [] + new_sizes = [] + new_strides = [] + + for d in args: + # Check if dimension already exists in current levels or new_levels + for level in levels: + if not level.is_positional() and level.dim() is d: + raise DimensionBindError( + f"expanding dimension {d} already exists in tensor with dims" + ) + for new_level in new_levels: + if not new_level.is_positional() and new_level.dim() is d: + raise DimensionBindError( + f"expanding dimension {d} already exists in tensor with dims" + ) + + new_levels.append(DimEntry(d)) + new_sizes.append(d.size) + new_strides.append(0) + + # Add existing levels + new_levels.extend(levels) + + # Add existing sizes and strides + orig_sizes = list(data.size()) + orig_strides = list(data.stride()) + new_sizes.extend(orig_sizes) + new_strides.extend(orig_strides) + + # Create expanded tensor using as_strided + expanded_data = data.as_strided(new_sizes, new_strides, data.storage_offset()) + + # Return new tensor with expanded dimensions + result = Tensor.from_positional(expanded_data, new_levels, info.has_device) + return result # type: ignore[return-value] # Tensor and torch.Tensor are interchangeable + + def index( + self, + dims: Union[int, Dim, tuple[Union[int, Dim], ...], list[Union[int, Dim]]], + indices: Union[ + int, + slice, + torch.Tensor, + tuple[Union[int, slice, torch.Tensor], ...], + list[Union[int, slice, torch.Tensor]], + ], + ) -> _Tensor: + """ + Index tensor using first-class dimensions. + """ + from ._dim_entry import _match_levels + from ._getsetitem import getsetitem_flat, invoke_getitem + from ._wrap import _wrap_dim + + # Helper to check if obj is a dimpack (tuple/list) and extract items + def maybe_dimpack(obj: Any, check_first: bool = False) -> tuple[Any, bool]: + if isinstance(obj, (tuple, list)): + return list(obj), True + return None, False + + def parse_dim_entry(s: Any) -> Any: + d = _wrap_dim(s, self.ndim, False) + if d.is_none(): + raise TypeError(f"expected a dimension specifyer but found {repr(s)}") + return d + + # Helper for dimension not present errors + def dim_not_present(d: Any) -> None: + if d.is_positional(): + raise TypeError( + f"dimension {d.position() + self.ndim} not in tensor of {self.ndim} dimensions" + ) + else: + raise TypeError(f"dimension {repr(d.dim())} not in tensor") + + dims_list: list[Union[int, Dim]] = [] + indices_list: list[Union[int, slice, torch.Tensor]] = [] + + lhs_list = isinstance(dims, (tuple, list)) + rhs_list = isinstance(indices, (tuple, list)) + + if lhs_list and rhs_list: + # Type narrowing: we know dims and indices are sequences here + dims_seq = dims # type: ignore[assignment] + indices_seq = indices # type: ignore[assignment] + if len(dims_seq) != len(indices_seq): # type: ignore[arg-type] + raise TypeError( + f"dims ({len(dims_seq)}) and indices ({len(indices_seq)}) must have the same length" # type: ignore[arg-type] + ) + dims_list.extend(dims_seq) # type: ignore[arg-type] + indices_list.extend(indices_seq) # type: ignore[arg-type] + else: + dims_list.append(dims) # type: ignore[arg-type] + indices_list.append(indices) # type: ignore[arg-type] + + # Create tensor info + self_info = TensorInfo.create(self, False, False) + + new_levels: list[Any] = [] + to_flatten: list[Any] = [] + dims_list_flat = [] + + # Process each dim specification + for i in range(len(dims_list)): + m, is_dimpack = maybe_dimpack(dims_list[i], check_first=False) + if is_dimpack: + if len(m) == 0: + dims_list_flat.append(DimEntry()) # Empty dimpack + continue + + first = parse_dim_entry(m[0]) + dims_list_flat.append(first) + + if len(m) == 1: + continue + + # Multi-element dimpack requires flattening + if len(to_flatten) == 0: + new_levels.extend(self_info.levels) + + rest = [] + for j in range(1, len(m)): + d = parse_dim_entry(m[j]) + removed = False + for k in range(len(new_levels)): + if new_levels[k] == d: + new_levels.pop(k) + removed = True + break + if not removed: + dim_not_present(d) + rest.append(d) + + # Find first in new_levels + first_idx = None + for k in range(len(new_levels)): + if new_levels[k] == first: + first_idx = k + break + if first_idx is None: + dim_not_present(first) + continue # Skip this iteration if dimension not found + + for j, r in enumerate(rest): + new_levels.insert(first_idx + 1 + j, r) + to_flatten.extend(rest) + else: + dims_list_flat.append(parse_dim_entry(dims_list[i])) + + # Handle dimension flattening if needed + if len(to_flatten) > 0: + assert self_info.tensor is not None, ( + "Cannot perform dimension flattening on None tensor" + ) + rearranged = _match_levels(self_info.tensor, self_info.levels, new_levels) + sizes = rearranged.size() + new_sizes: list[Any] = [] + reshape_levels = [] + + for i in range(len(new_levels)): + if new_levels[i] in to_flatten: + if len(new_sizes) == 0: + new_sizes.append(sizes[i]) + else: + new_sizes[-1] *= sizes[i] + else: + new_sizes.append(sizes[i]) + reshape_levels.append(new_levels[i]) + + self_info.tensor = rearranged.reshape(new_sizes) + self_info.levels = reshape_levels + + # Check for dimpacks in indices + has_dimpacks = False + for idx in indices_list: + if isinstance(idx, (tuple, list)): + has_dimpacks = True + break + + # Call getsetitem_flat with correct parameters + info = getsetitem_flat( + self_info, + [], # empty input_list + dims_list_flat, # keys + indices_list, # values + has_dimpacks, + ) + + return invoke_getitem(info) + + def __repr__(self) -> str: + tensor, levels, ndim = self._get_tensor(), self._get_levels(), self.ndim + dims_repr = [] + for l in levels: + if hasattr(l, "is_positional") and l.is_positional(): + # Convert negative positional to positive: -1 -> ndim-1, -2 -> ndim-2, etc. + dims_repr.append(l.position() + ndim) + elif hasattr(l, "dim"): + dims_repr.append(l.dim()) + elif hasattr(l, "data"): + dims_repr.append(l.data) + else: + dims_repr.append(l) + return f"{tensor}\nwith dims={tuple(dims_repr)} sizes={tuple(tensor.size())}" # type: ignore[union-attr] + + +TensorLike = (_Tensor, torch.Tensor) + + +class Dim(_Tensor): + _level: int + _name: str + _size: int + _range: Optional[torch.Tensor] + _batchtensor: Optional[torch.Tensor] + + def __init__(self, name: str, s: int = -1) -> None: + global _n_dims_created + self._name = name + self._size = s + self._level = _n_dims_created + _n_dims_created += 1 + self._range = None + self._batchtensor = None + + @property + def ndim(self) -> int: + return 1 + + @classmethod + def check_exact(cls, obj: Any) -> bool: + return type(obj) is cls + + @property + def size(self) -> int: + if self._size == -1: + raise ValueError(f"dimension {self._name} is unbound") + return self._size + + @size.setter + def size(self, v: int) -> None: + if self._size == -1: + self._size = v + elif self._size != v: + raise DimensionBindError( + f"Dim '{repr(self)}' previously bound to a dimension of size {self._size} " + f"cannot bind to a dimension of size {v}" + ) + + @property + def is_bound(self) -> bool: + """Return True if this dimension is bound to a size.""" + return self._size != -1 + + def _get_range(self) -> torch.Tensor: + """ + Get a tensor representing the range [0, size) for this dimension. + + Returns: + A 1D tensor with values [0, 1, 2, ..., size-1] + """ + if self._range is None: + self._range = torch.arange(self.size) + return self._range + + def _get_batchtensor(self) -> torch.Tensor: + """ + Get a batched tensor representation of this dimension. + + Returns: + A batched tensor created from the range tensor + """ + if self._batchtensor is None: + self._batchtensor = torch._C._functorch._add_batch_dim( + self._get_range(), 0, self._level + ) + return self._batchtensor + + def __repr__(self) -> str: + """String representation of a Dim object.""" + return self._name + + # note that Dim comes before tensor because we want the Dim API for things like size to take precedence. + # Tensor defines format, but we want to print Dims with special formatting + __format__ = object.__format__ + + +# Somewhat confusingly, an FCD tensor is also called Tensor. This confusion +# is somewhat intentional, as FCD tensors are intended to be substitutable +# with regular Tensor (just with some positional dims hidden). +class Tensor(_Tensor): + _tensor: Optional[torch.Tensor] + _batchtensor: Optional[torch.Tensor] + _levels: list[DimEntry] + _has_device: bool + _delayed: Optional[Callable[[], torch.Tensor]] + _delayed_orig: Optional[Callable] + _delayed_args: Optional[tuple] + + @property + def ndim(self) -> int: + return sum(1 if l.is_positional() else 0 for l in self._levels) + + @classmethod + def check_exact(cls, other: Any) -> bool: + return type(other) is cls + + @classmethod + def from_positional( + cls, tensor: torch.Tensor, levels: list[DimEntry], has_device: bool + ) -> Union[_Tensor, torch.Tensor]: + """ + Create a functorch Tensor from a regular PyTorch tensor with specified dimension levels. + + This is the primary way to create Tensor objects with first-class dimensions. + + Args: + tensor: The underlying PyTorch tensor + levels: List of DimEntry objects specifying the dimension structure + has_device: Whether the tensor is on a device (not CPU) + + Returns: + A new Tensor instance with the specified dimensions, or a regular torch.Tensor + if there are no named dimensions + """ + seen_dims = 0 + last = 0 + + for i, l in enumerate(levels): + if l.is_positional(): + # Validate consecutive positional dimensions + assert last == 0 or last + 1 == l.position(), ( + f"Positional dimensions must be consecutive, got {last} then {l.position()}" + ) + last = l.position() + else: + # This is a named dimension + seen_dims += 1 + + # Validate final positional dimension + assert last == 0 or last == -1, ( + f"Final positional dimension must be 0 or -1, got {last}" + ) + + if not seen_dims: + return tensor + + # Create Tensor object with proper level management + result = cls() + result._tensor = tensor + result._levels = levels + result._has_device = has_device + result._batchtensor = None # Will be created lazily if needed + result._delayed = None + result._delayed_orig = None + result._delayed_args = None + + # Validate tensor dimensionality matches levels + assert tensor.dim() == len(levels), ( + f"Tensor has {tensor.dim()} dimensions but {len(levels)} levels provided" + ) + + return result + + @classmethod + def create_delayed( + cls, orig: Callable, args: tuple, levels: list[DimEntry], has_device: bool + ) -> _Tensor: + """ + Create a delayed tensor that defers the operation until later. + """ + result = cls() + result._tensor = None # Will be computed when needed + result._levels = levels + result._has_device = has_device + result._batchtensor = None + result._delayed_orig = orig + result._delayed_args = args + + # Create delayed evaluation function that unwraps Tensor objects + def evaluate_delayed() -> torch.Tensor: + unwrapped_args = [] + for arg in args: + if hasattr(arg, "_get_tensor"): + unwrapped_args.append(arg._get_tensor()) + else: + unwrapped_args.append(arg) + return orig(*unwrapped_args) + + result._delayed = evaluate_delayed + + return result + + def _get_tensor(self) -> Optional[torch.Tensor]: + """Get the underlying tensor, handling delayed operations if needed.""" + if ( + hasattr(self, "_delayed") + and self._delayed is not None + and self._tensor is None + ): + # Execute the delayed operation + self._tensor = self._delayed() + # Clear delayed operation to avoid re-execution + self._delayed = None + self._delayed_orig = None + self._delayed_args = None + return self._tensor + + def _get_levels(self) -> list[Any]: + """Get the dimension levels.""" + return self._levels + + def _get_has_device(self) -> bool: + """Get whether this tensor has device information.""" + return self._has_device + + def _get_batchtensor(self) -> Optional[torch.Tensor]: + """Get the batched tensor representation, creating it lazily if needed.""" + if self._batchtensor is None: + self._batchtensor = self._add_batch_dims( + self._get_tensor(), self._get_levels() + ) + return self._batchtensor + + def _add_batch_dims( + self, t: Optional[torch.Tensor], levels_: list[Any] + ) -> Optional[torch.Tensor]: + levels = list(levels_) + + while True: + min_real_index = -1 + min_index = -1 + min_value = float("inf") # INT_MAX equivalent + i = 0 + r = 0 + + for r, l in enumerate(levels): + if not l.is_none(): + if not l.is_positional() and l.dim()._level < min_value: + min_value = l.dim()._level + min_index = i + min_real_index = r + i += 1 + + if min_index == -1: + return t + + assert t is not None + t = torch._C._functorch._add_batch_dim(t, min_index, int(min_value)) + + levels[min_real_index] = DimEntry() + return None + + def order(self, *dims: Any) -> _Tensor: + """Reorder the dimensions of this tensor.""" + from ._order import order + + result = order(self, *dims) + return result # type: ignore[return-value] # Tensor and torch.Tensor are interchangeable + + +def stack(tensors: Any, new_dim: Any, dim: int = 0) -> _Tensor: + """ + Stack tensors along a new dimension. + + Args: + tensors: Sequence of tensors to stack + new_dim: The new Dim to create for stacking + dim: The dimension position to insert the new dimension (default: 0) + + Returns: + Stacked tensor with the new dimension + """ + if not tensors: + raise ValueError("stack expects a non-empty sequence of tensors") + + # Check if new_dim is a Dim object + if not isinstance(new_dim, Dim): + # Fall back to regular torch.stack + result = torch.stack(tensors, dim=dim) + return result # type: ignore[return-value] + + # Collect all result_levels from input tensors + result_levels = [] + infos = [] + + for t in tensors: + info = TensorInfo.create(t, ensure_batched=False, ensure_present=False) + infos.append(info) + for level in info.levels: + if level not in result_levels: + result_levels.append(level) + + # Set the new_dim size to match number of tensors + new_dim.size = len(tensors) + + # Match all tensors to the common level structure using _match_levels + inputs = [] + for info in infos: + assert info.tensor is not None, "Cannot stack tensors with None tensor data" + matched_tensor = _match_levels(info.tensor, info.levels, result_levels) + inputs.append(matched_tensor) + + # Calculate ndim and resolve the dim parameter + ndim = ndim_of_levels(result_levels) + rawdim = 0 + if dim is not None and not (isinstance(dim, int) and dim == 0): + from ._wrap import _wrap_dim + + d = _wrap_dim(dim, ndim, False) + try: + idx = result_levels.index(d) + except ValueError: + raise TypeError(f"Dimension {dim} does not exist in inputs") from None + rawdim = idx + + # Stack tensors at the resolved dimension + result = torch.stack(inputs, rawdim) + + # Insert new dimension entry at the correct position + result_levels.insert(rawdim, DimEntry(new_dim)) + + # Return as a first-class tensor + tensor_result = Tensor.from_positional( + result, result_levels, infos[0].has_device if infos else True + ) + return tensor_result # type: ignore[return-value] + + +def split(tensor: Any, split_size_or_sections: Any, dim: Any = None) -> tuple: + """ + Split tensor along a dimension. + + Can handle both regular integer sizes and Dim objects for split sizes. + When Dim objects are used, they get bound to the resulting tensor dimensions. + """ + from ._wrap import _wrap_dim + + # Check if dim is a Dim object + dim_is_object = isinstance(dim, Dim) + + # Parse split_size_or_sections + if isinstance(split_size_or_sections, int): + # Single integer - use regular split + if dim_is_object: + raise TypeError( + "when dim is specified as a Dim object, split sizes must also be dimensions." + ) + return _Tensor._torch_function_fallback( + torch.Tensor.split, + (type(tensor),), + (tensor, split_size_or_sections), + {"dim": dim}, + ) + + # Check if it's a sequence + sizes = [] + all_dims = True + all_ints = True + + for item in split_size_or_sections: + sizes.append(item) + if isinstance(item, Dim): + all_ints = False + else: + all_dims = False + + if all_ints: + # All integers - use regular split + if dim_is_object: + raise TypeError( + "when dim is specified as a Dim object, split sizes must also be dimensions." + ) + return _Tensor._torch_function_fallback( + torch.Tensor.split, + (type(tensor),), + (tensor, split_size_or_sections), + {"dim": dim}, + ) + + if not all_dims: + raise TypeError("split list must be ints or dims but got a mix") + + # All are Dim objects - handle first-class dimension split + self_info = TensorInfo.create(tensor, ensure_batched=False, ensure_present=False) + ndim = self_info.ndim() + + if not dim_is_object and ndim == 0: + raise TypeError("split expects at least a 1-dimension tensor") + + # Wrap the dimension + dim_l = _wrap_dim(dim, ndim, False) if dim is not None else DimEntry(-ndim) + + # Find the index of the dimension in levels + idx = None + for i, level in enumerate(self_info.levels): + if level == dim_l: + idx = i + break + + if idx is None: + if dim is None: + dim = 0 + raise TypeError(f"tensor does not contain dimension {dim}") + + # Calculate split indices + indices = [] + total_size = 0 + unbound = [] + + for i, size_dim in enumerate(sizes): + if size_dim.is_bound: + indices.append(size_dim.size) + total_size += indices[-1] + else: + indices.append(0) + unbound.append(i) + + assert self_info.tensor is not None, "Cannot get tensor size on None tensor" + tensor_size = self_info.tensor.size(idx) + + # Handle unbound dimensions + if unbound: + if total_size > tensor_size: + raise TypeError( + f"sizes of target dimensions add up to more ({total_size}) than source dim ({tensor_size})" + ) + remaining_size = tensor_size - total_size + chunk_size = (remaining_size + len(unbound) - 1) // len(unbound) + for u in unbound: + sz = min(chunk_size, remaining_size) + sizes[u].size = sz + indices[u] = sz + remaining_size -= sz + elif tensor_size != total_size: + raise TypeError( + f"sum of sizes of target dimensions ({total_size}) do not match the source dim ({tensor_size})" + ) + + # Perform the split + result_tensors = self_info.tensor.split_with_sizes(indices, idx) + + # Create result with new levels + result = [] + new_levels = list(self_info.levels) + + for i, (result_tensor, size_dim) in enumerate(zip(result_tensors, sizes)): + new_levels[idx] = DimEntry(size_dim) + result.append( + Tensor.from_positional( + result_tensor, list(new_levels), self_info.has_device + ) + ) + + return tuple(result) + + +def cat(tensors: Any, dim: Any, new_dim: Any) -> _Tensor: + n = dims(1) # Get single Dim instead of tuple + return stack(tensors, n, dim).index([n, dim], new_dim) # type: ignore[list-item] + + +class DotPart: + """ + Helper class for organizing dimensions in dot products. + """ + + def __init__(self) -> None: + self.dims: list[DimEntry] = [] + self.total_size = 1 + + def append(self, dim_entry: Any) -> None: + """Add a dimension entry to this part.""" + self.dims.append(dim_entry) + if not dim_entry.is_positional(): + self.total_size *= dim_entry.dim().size + + +def dot_prepare(parts: list[DotPart], tensor_info: TensorInfo) -> torch.Tensor: + """ + Prepare tensor for dot product by matching levels and reshaping. + """ + new_levels = [] + needs_reshape = False + + for part in parts: + if len(part.dims) != 1: + needs_reshape = True + new_levels.extend(part.dims) + + if tensor_info.tensor is None: + raise RuntimeError("Cannot perform dot product on None tensor") + result = _match_levels(tensor_info.tensor, tensor_info.levels, new_levels) + + if not needs_reshape: + return result + + # Reshape for matrix operations + view = [part.total_size for part in parts] + return result.reshape(view) + + +def dot_finish(parts: list[DotPart], result_tensor: torch.Tensor) -> Tensor: + """ + Finish dot product by reshaping result and creating Tensor. + """ + result_levels = [] + needs_reshape = False + + for part in parts: + if len(part.dims) != 1: + needs_reshape = True + result_levels.extend(part.dims) + + if needs_reshape: + new_size = [] + for level in result_levels: + new_size.append(level.dim().size) + result_tensor = result_tensor.reshape(new_size) + + tensor_result = Tensor.from_positional(result_tensor, result_levels, True) + return tensor_result # type: ignore[return-value] + + +def dot(lhs: Any, rhs: Any, sum_dims: Any) -> Union[_Tensor, torch.Tensor]: + """ + Perform dot product between two tensors along specified dimensions. + + Args: + lhs: Left-hand side tensor + rhs: Right-hand side tensor + sum_dims: Dimensions to sum over (contract) + + Returns: + Result of dot product + """ + # Get tensor info + lhs_info = TensorInfo.create(lhs, ensure_batched=False, ensure_present=False) + rhs_info = TensorInfo.create(rhs, ensure_batched=False, ensure_present=False) + + if not (lhs_info and rhs_info): + # Fall back to regular operations + return torch.matmul(lhs, rhs) + + assert lhs_info.tensor is not None and rhs_info.tensor is not None, ( + "Cannot perform dot product on None tensors" + ) + + lhs_strides = lhs_info.tensor.stride() + rhs_strides = rhs_info.tensor.stride() + + # Create dot parts for different dimension categories + lro_dims = DotPart() # Left-right-output (batch dims) + lo_dims = DotPart() # Left-output only + ro_dims = DotPart() # Right-output only + lr_dims = DotPart() # Left-right (contracted dims) + + def insert_dim(d: Any, lhs_idx: Any, rhs_idx: Any) -> None: + """Insert dimension into appropriate part based on stride pattern.""" + reduced = d in sum_dims + lhs_stride = lhs_strides[lhs_idx] if lhs_idx is not None else 0 + rhs_stride = rhs_strides[rhs_idx] if rhs_idx is not None else 0 + + if reduced: + lr_dims.append(d) + else: + if (lhs_stride == 0) == (rhs_stride == 0): + lro_dims.append(d) # Both have or both lack this dim + elif lhs_stride != 0: + lo_dims.append(d) # Only lhs has this dim + else: + ro_dims.append(d) # Only rhs has this dim + + # Track which rhs dimensions we've seen + rhs_seen = [False] * len(rhs_info.levels) + + # Process lhs dimensions + for i, lhs_level in enumerate(lhs_info.levels): + rhs_idx = None + for j, rhs_level in enumerate(rhs_info.levels): + if lhs_level == rhs_level: + rhs_idx = j + rhs_seen[j] = True + break + + insert_dim(lhs_level, i, rhs_idx) + + # Process remaining rhs dimensions + for i, rhs_level in enumerate(rhs_info.levels): + if not rhs_seen[i]: + insert_dim(rhs_level, None, i) + + # Validate sum dimensions exist + if len(lr_dims.dims) != len(sum_dims): + for d in sum_dims: + if d not in lhs_info.levels and d not in rhs_info.levels: + raise ValueError(f"summing over non-existent dimension {d}") + + # Prepare tensors and perform matrix multiplication + if len(lro_dims.dims) != 0: + # Batched matrix multiply + lhs_tensor = dot_prepare([lro_dims, lo_dims, lr_dims], lhs_info) + rhs_tensor = dot_prepare([lro_dims, lr_dims, ro_dims], rhs_info) + result = torch.bmm(lhs_tensor, rhs_tensor) + return dot_finish([lro_dims, lo_dims, ro_dims], result) + else: + # Regular matrix multiply + lhs_tensor = dot_prepare([lo_dims, lr_dims], lhs_info) + rhs_tensor = dot_prepare([lr_dims, ro_dims], rhs_info) + result = torch.mm(lhs_tensor, rhs_tensor) + return dot_finish([lo_dims, ro_dims], result) + + +from functorch.dim._wrap import _wrap +from functorch.dim.wrap_type import wrap_type + + +wrap_type(_Tensor, torch.Tensor, _Tensor.__torch_function__) +del _Tensor.ndim + + +def index(self: Any, positions: Any, dims: Any) -> _Tensor: + """ + Index a regular tensor by binding specified positions to dims. + + This converts a regular tensor to a first-class tensor by binding + the specified positional dimensions to Dim objects. + + Args: + positions: Tuple of dimension positions to bind + dims: Dim objects or tuple of Dim objects to bind to + + Returns: + First-class tensor with specified dimensions bound + """ + # If this is already a first-class tensor (_Tensor), call its index method directly + if isinstance(self, _Tensor): + return _Tensor.index(self, positions, dims) + + # Convert regular tensor to first-class tensor + info = TensorInfo.create(self, ensure_batched=False, ensure_present=False) + + # Create the first-class tensor + assert info.tensor is not None, "Cannot index None tensor" + result = Tensor.from_positional(info.tensor, info.levels, info.has_device) + + # Now call the index method on the first-class tensor + # Cast result to _Tensor for the method call + return _Tensor.index(result, positions, dims) # type: ignore[arg-type] + + +def _def(name: str, *args: Any, **kwargs: Any) -> None: + orig = getattr(torch.Tensor, name) + setattr(_Tensor, name, _wrap(orig, *args, **kwargs)) + + +_def("mean") +_def("sum") +_def("all") +_def("amax") +_def("amin") +_def("aminmax") +_def("any") +_def("count_nonzero") +_def("logsumexp") +_def("nanmean") +_def("nansum") +_def("prod") +_def("std", keepdim_offset=2) +_def("var", keepdim_offset=2) +_def("max", single_dim=True) +_def("min", single_dim=True) +_def("argmax", single_dim=True) +_def("argmin", single_dim=True) +_def("kthvalue", single_dim=True) +_def("median", single_dim=True) +_def("nanmedian", single_dim=True) +_def("mode", single_dim=True) +_def("sort", reduce=False) +_def("argsort", reduce=False) +_def("unbind", single_dim=True) +_def("chunk", dim_offset=1, reduce=False) +_def("cummax", single_dim=True, reduce=False) +_def("cummin", single_dim=True, reduce=False) +_def("cumprod", single_dim=True, reduce=False) +_def("cumprod_", single_dim=True, reduce=False) +_def("cumsum", single_dim=True, reduce=False) +_def("cumsum_", single_dim=True, reduce=False) +_def("logcumsumexp", single_dim=True, reduce=False) +_def("renorm", dim_offset=1, single_dim=True, reduce=False) +_def("softmax", single_dim=True, reduce=False) +softmax = _wrap(torch.nn.functional.softmax, single_dim=True, reduce=False) + +# stuff to handle in the future, because they require special +# binding logic for dims +# cross +# diag_embed +# diagonal +# diagonal_scatter +# diff +# nanquantile +# quantile +# roll +# rot90 +# topk (new dimes on output) +# should these all be subsumed by inplace indexing? +# index_add_ +# index_add +# index_copy +# index_copy_ +# index_fill +# index_fill_ +# index_select +# scatter +# scatter_ +# scatter_add +# scatter_add_ +# scatter_reduce diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b77003058516951de5d759d716c880fd9d654572 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_dim_entry.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_dim_entry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c76fa007a6a4b7d73e94c9ec7d45829ec9dead6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_dim_entry.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_enable_all_layers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_enable_all_layers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d81a84f1b98629742c17d1ac3b86730ae1854d1c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_enable_all_layers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_getsetitem.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_getsetitem.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b03fae5f073d9d1332ede7d802caef2002f92dd5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_getsetitem.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_order.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_order.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83b04aff0573aeb5ce6bc451b9c1bcce295b04aa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_order.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_py_inst_decoder.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_py_inst_decoder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88586b3958369f534f2f084d763418bc5f0cdc4d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_py_inst_decoder.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_tensor_info.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_tensor_info.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..215a68fa99b9d888825f9e6a20aef8a53b85643a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_tensor_info.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_wrap.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_wrap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8eab5513b9de4dcb043e6969bee5803447733ca3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/_wrap.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/magic_trace.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/magic_trace.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43ec4a8a80ac286e9dae04e988226e4e4c05c09f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/magic_trace.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/op_properties.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/op_properties.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d2da664879edabd213e7356095181d98bb49085 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/op_properties.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/wrap_type.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/wrap_type.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae2d26b3682699f76bd23c816a14da536a2a2864 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/__pycache__/wrap_type.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_dim_entry.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_dim_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..c067a7ad0ce4028450f194744f1c4bb07b0f6358 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_dim_entry.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Union + + +if TYPE_CHECKING: + from collections.abc import Sequence + + from . import Dim + +import torch # noqa: TC002 + + +# NB: The old code represented dimension was from as negative number, so we +# follow this convention even though it shouldn't be necessary now +class DimEntry: + # The dimension this is from the rhs, or a FCD + data: Union[Dim, int] + + def __init__(self, data: Union[Dim, int, None] = None) -> None: + from . import Dim + + if type(data) is int: + assert data < 0 + elif data is None: + data = 0 + else: + assert isinstance(data, Dim) + self.data = data + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DimEntry): + return False + # Use 'is' for Dim objects to avoid triggering __torch_function__ + # Use '==' only for positional (int) comparisons + if self.is_positional() and other.is_positional(): + # Both are positional (ints) + return self.data == other.data + elif not self.is_positional() and not other.is_positional(): + # Both are Dim objects - use 'is' to avoid __eq__ + return self.data is other.data + else: + # One is positional, one is Dim - they can't be equal + return False + + def is_positional(self) -> bool: + return type(self.data) is int and self.data < 0 + + def is_none(self) -> bool: + # Use isinstance to check for Dim objects, avoid triggering __torch_function__ + from . import Dim + + if isinstance(self.data, Dim): + # This is a Dim object, it can't be "none" (which is represented by 0) + return False + else: + # This is an int or other type + return self.data == 0 + + def position(self) -> int: + assert isinstance(self.data, int) + return self.data + + def dim(self) -> Dim: + assert not isinstance(self.data, int) + return self.data + + def __repr__(self) -> str: + return repr(self.data) + + +def ndim_of_levels(levels: Sequence[DimEntry]) -> int: + r = 0 + for l in levels: + if l.is_positional(): + r += 1 + return r + + +def _match_levels( + tensor: torch.Tensor, + from_levels: list[DimEntry], + to_levels: list[DimEntry], + drop_levels: bool = False, +) -> torch.Tensor: + """ + Reshape a tensor to match target levels using as_strided. + + Args: + tensor: Input tensor to reshape + from_levels: Current levels of the tensor + to_levels: Target levels to match + drop_levels: If True, missing dimensions are assumed to have stride 0 + + Returns: + Reshaped tensor + """ + if from_levels == to_levels: + return tensor + + sizes = tensor.size() + strides = tensor.stride() + + if not drop_levels: + assert len(from_levels) <= len(to_levels), ( + "Cannot expand dimensions without drop_levels" + ) + + new_sizes = [] + new_strides = [] + + for level in to_levels: + # Find index of this level in from_levels + try: + idx = from_levels.index(level) + except ValueError: + # Level not found in from_levels + if level.is_positional(): + new_sizes.append(1) + else: + new_sizes.append(level.dim().size) + new_strides.append(0) + else: + new_sizes.append(sizes[idx]) + new_strides.append(strides[idx]) + + return tensor.as_strided(new_sizes, new_strides, tensor.storage_offset()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_enable_all_layers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_enable_all_layers.py new file mode 100644 index 0000000000000000000000000000000000000000..b05c58b2c843965d7f16be22233427f50c137481 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_enable_all_layers.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from typing import Any, TYPE_CHECKING + +import torch + +from ._dim_entry import DimEntry + + +if TYPE_CHECKING: + from . import Dim, Tensor + + +class EnableAllLayers: + """ + RAII-style context manager for enabling functorch vmap layers. + It manages the creation and cleanup of functorch dynamic layers. + + This is probably one of the more algorithmically important parts of first + class dims. Intuitively, FCD can be thought of as another way of using + vmap, where you don't actually have to vmap at the top level, instead the + vmaps are implicitly determined by inspecting the bound dimensions on the + FCD tensors involved in a compute (this is similar to our concept of + non-lexical modes that we spent a long time talking about years ago). But + under the hood you still need to actually enable the vmap mode. So once + FCD has determined all of the dims we are batching over, it needs to + enable all those layers so functorch can actually apply the batching + rules. Therefore enable all layers! + """ + + levels_start: int + levels_to_dim: list[Dim] + + def __init__(self, levels: list[DimEntry]): + """ + Initialize and push dynamic layers for all first-class dimensions. + + Args: + levels: List of dimension entries to create layers for + """ + + from . import Dim + + self.levels_start = 0 + self.levels_to_dim = [] + + for l in levels: + if not l.is_positional(): + d = l.dim() + assert isinstance(d, Dim) + self.levels_to_dim.append(d) + + # Sort by level for stable ordering + self.levels_to_dim.sort(key=lambda d: d._level) + + def __enter__(self) -> EnableAllLayers: # noqa: PYI034 + # Create functorch dynamic layers + for i, dim in enumerate(self.levels_to_dim): + batch_size = dim.size + level = torch._C._functorch._vmap_increment_nesting(batch_size, "different") + if i == 0: + self.levels_start = level + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Clean up dynamic layers in reverse order.""" + to_remove = self.levels_start + len(self.levels_to_dim) - 1 + for i in range(len(self.levels_to_dim)): + popped = torch._C._functorch._vmap_decrement_nesting() + assert popped == to_remove - i, ( + f"Expected layer {to_remove - i}, got {popped}" + ) + + def from_batched(self, batchedtensor: torch.Tensor, has_device: bool) -> Tensor: + """ + Create a Tensor from a batched tensor by unwrapping functorch layers. + + Args: + batchedtensor: Batched tensor from functorch operation + has_device: Whether tensor has device info + + Returns: + Tensor with appropriate levels + """ + # Create positional levels for base dimensions + levels: list[DimEntry] = [] + for i in range(-batchedtensor.dim(), 0): + levels.append(DimEntry(i)) + + tensor = batchedtensor + + while torch._C._functorch.is_batchedtensor(tensor): + level = torch._C._functorch.maybe_get_level(tensor) + assert level is not None + assert level >= self.levels_start and level < self.levels_start + len( + self.levels_to_dim + ) + dim = DimEntry(self.levels_to_dim[level - self.levels_start]) + bdim = torch._C._functorch.maybe_get_bdim(tensor) + assert bdim is not None + levels.insert(bdim, dim) + tensor = torch._C._functorch.get_unwrapped(tensor) + + from . import Tensor + + result = Tensor() + result._tensor = tensor + result._batchtensor = batchedtensor + result._has_device = has_device + result._levels = levels + return result + + def inplace_update_layers( + self, batchtensor: torch.Tensor, levels: list[DimEntry] + ) -> None: + """ + Update the levels of a batched tensor in place. + + This requires the _maybe_unsafe_set_level binding that we'll add to functorch. + + Args: + batchtensor: Batched tensor to update + levels: New levels to set + """ + # Check if tensor is batched + if not torch._C._functorch.is_batchedtensor(batchtensor): + return + + impl = batchtensor + + for i in reversed(range(len(self.levels_to_dim))): + if impl is None: + break + + if any(l == DimEntry(self.levels_to_dim[i]) for l in levels): + # This is very interesting! The level on batch tensor is + # meaningless! We set it RIGHT before we go into vmap + torch._C._functorch._maybe_unsafe_set_level(impl, self.levels_start + i) + impl = torch._C._functorch.get_unwrapped(impl) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_getsetitem.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_getsetitem.py new file mode 100644 index 0000000000000000000000000000000000000000..59e2f3c61e0b15294ab0f0271cee2968c273256d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_getsetitem.py @@ -0,0 +1,561 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Optional, TYPE_CHECKING, Union + +import torch + +from ._dim_entry import _match_levels, DimEntry +from ._tensor_info import TensorInfo + + +if TYPE_CHECKING: + from . import Dim + + +def _safe_index(lst: list, item: Any) -> Optional[int]: + """ + Helper function to find index of item in list. + + For DimEntry objects, uses __eq__ comparison which properly handles + both positional and Dim entries. + + Returns the index if found, None if not found. + """ + for i, list_item in enumerate(lst): + # Use == for DimEntry objects as they have proper __eq__ implementation + if isinstance(item, DimEntry) and isinstance(list_item, DimEntry): + if list_item == item: + return i + elif list_item is item: + return i + return None + + +@dataclass +class IndexingInfo: + can_call_original: bool = False + advanced_indexing: bool = False + self_tensor: Optional[torch.Tensor] = None + flat_inputs: list[Any] = field(default_factory=list) + result_levels: list[DimEntry] = field(default_factory=list) + has_device: bool = False + + +def has_dims(obj: Any) -> bool: + """ + Check if an object has first-class dimensions. + + This function checks if the object is either a Dim or a functorch Tensor + that has first-class dimensions, using the proper check_exact methods. + """ + from . import Dim, Tensor + + return Dim.check_exact(obj) or Tensor.check_exact(obj) + + +def _bind_dims_to_size(sz: int, sd: int, dims: list, nsz: list, nsd: list) -> None: + """ + Bind dimensions to size and calculate proper strides for dim packs. + """ + from . import DimensionBindError + + rhs_prod = 1 + for i, dim in enumerate(dims): + if not dim.is_bound: + # Check for multiple unbound dimensions + for j in range(i + 1, len(dims)): + if not dims[j].is_bound: + raise DimensionBindError( + f"cannot infer the sizes of two dimensions at once {dim!r} and {dims[j]!r}" + ) + rhs_prod *= dims[j].size + + # Calculate the size for this unbound dimension + if sz % rhs_prod != 0: + tup = tuple(dim.size if dim.is_bound else "?" for dim in dims) + raise DimensionBindError( + f"inferred dimension does not evenly fit into larger dimension: {sz} vs {tup}" + ) + + inferred_size = sz // rhs_prod + dim.size = inferred_size + rhs_prod = sz + break + else: + rhs_prod *= dim.size + + # Final validation that dimensions match + if rhs_prod != sz: + tup = tuple(dims) + raise DimensionBindError( + f"Dimension sizes to do not match ({sz} != {rhs_prod}) when matching dimension pack {tup}" + ) + + # Calculate new sizes and strides for each dimension in the pack + # First calculate all strides by iterating in reverse + new_strides = [0] * len(dims) + current_stride = sd + for i in reversed(range(len(dims))): + new_strides[i] = current_stride + current_stride *= dims[i].size + + # Then append sizes and strides in forward order + for i in range(len(dims)): + nsz.append(dims[i].size) + nsd.append(new_strides[i]) + + +def slice_to_tuple(flat_inputs: list) -> tuple: + return tuple(flat_inputs) + + +def extractIndices(index: Any, indices: list) -> bool: + if isinstance(index, tuple): # mpy::tuple_view::check + indices.extend(index) + return True + elif isinstance(index, torch.Tensor): # THPVariable_Check + indices.append(index) + return False + elif not hasattr(index, "__iter__") or isinstance( + index, (str, bytes) + ): # !mpy::is_sequence + indices.append(index) + return False + + # Handle sequence case (list) + if isinstance(index, list): + if len(index) >= 32: + indices.extend(index) + return True + + # Check each item in the sequence + for item in index: + if ( + isinstance(item, (torch.Tensor, slice)) + or hasattr(item, "__iter__") + or item is ... + or item is None + or has_dims(item) + ): + indices.extend(index) + return True + + # If we got here, treat as single index + indices.append(index) + return False + + # Default case + indices.append(index) + return False + + +def getitem(cls: Any, func: Any, types: Any, args: Any, kwargs: Any) -> Any: + self = args[0] + index = args[1] + + iinfo = getsetitem(self, index, has_dims(self)) + if iinfo.can_call_original: + # Call original tensor __getitem__ directly, bypassing __torch_function__ + return torch.Tensor.__getitem__(self, index) + + return invoke_getitem(iinfo) + + +def setitem(self: Any, index: Any, rhs: Any) -> None: + """Set values in tensor using first-class dimensions.""" + from . import DimensionBindError, TensorInfo + + iinfo = getsetitem(self, index, has_dims(self) or has_dims(rhs)) + + if iinfo.can_call_original: + # Call original tensor __setitem__ directly, bypassing __torch_function__ + torch._C.TensorBase.__setitem__(self, index, rhs) + return + + # Handle RHS tensor with dimensions + rhs_info = TensorInfo.create(rhs, False, False) + + if rhs_info: + # Check that rhs dimensions are compatible with result dimensions + for l in rhs_info.levels: + if not l.is_positional(): + # Find this dimension in result levels + found = False + for result_level in iinfo.result_levels: + if ( + not result_level.is_positional() + and result_level.dim() is l.dim() + ): + found = True + break + + if not found: + # Create tuple representation of result levels for error message + result_dims: list[Union[int, Dim]] = [] + for rl in iinfo.result_levels: + if rl.is_positional(): + result_dims.append(rl.position()) + else: + result_dims.append(rl.dim()) + + raise DimensionBindError( + f"rhs of setitem contains dimension {l.dim()!r} which is not in the dimension on the left " + f"({tuple(result_dims)!r})" + ) + + # Match RHS tensor to result levels + assert rhs_info.tensor is not None, "Cannot match levels on None tensor" + matched_rhs = _match_levels( + rhs_info.tensor, rhs_info.levels, iinfo.result_levels + ) + else: + matched_rhs = rhs + + # For advanced indexing with dimensions, we need special handling + if iinfo.advanced_indexing: + # Use advanced indexing - the flat_inputs already contain matched tensors + tup = slice_to_tuple(iinfo.flat_inputs) + if iinfo.self_tensor is None: + raise RuntimeError("Cannot setitem on None tensor") + torch._C.TensorBase.__setitem__(iinfo.self_tensor, tup, matched_rhs) + else: + # Simple copy operation + if iinfo.self_tensor is None: + raise RuntimeError("Cannot copy to None tensor") + iinfo.self_tensor.copy_(matched_rhs) + + +def invoke_getitem(iinfo: IndexingInfo) -> Any: + if iinfo.advanced_indexing: + self_tensor = iinfo.self_tensor + tup = slice_to_tuple(iinfo.flat_inputs) + if self_tensor is None: + raise RuntimeError("Cannot getitem on None tensor") + rtensor = self_tensor[tup] + else: + rtensor = iinfo.self_tensor # type: ignore[assignment] + if rtensor is None: + raise RuntimeError("Cannot getitem on None tensor") + # rtensor is now guaranteed to be not None + + # Create a Tensor with the proper dimensions using the class method + from . import Tensor + + return Tensor.from_positional(rtensor, iinfo.result_levels, iinfo.has_device) + + +def getsetitem(self: Any, index: Any, tensors_have_dims: bool) -> IndexingInfo: + from . import DimList # Import DimList for type checking + + can_call_original_getitem = not tensors_have_dims + + input_list = [] + if has_dims(index): + input_list.append(index) + else: + is_sequence = extractIndices(index, input_list) + # nothing about first class dims here, fallback to getitem + if can_call_original_getitem and not is_sequence: + return IndexingInfo(can_call_original=True) + + # Calculate how many dimensions have been indexed in order to compute the + # size of ... or expand a potentially unbound dimension list. + dims_indexed = 0 + expanding_object = -1 + unbound_dim_list = None + dimlists = [] # Track DimList positions for later processing + + def check_expanding(i: int) -> None: + nonlocal expanding_object + if expanding_object != -1: + from . import DimensionBindError + + raise DimensionBindError( + f"at most one ... or unbound dimension list can exist in indexing list but found 2 at offsets " + f"{expanding_object} and {i}" + ) + expanding_object = i + + def is_dimpack(s: Any) -> bool: + from . import Dim + + return ( + isinstance(s, (tuple, list)) + and len(s) > 0 + and all(Dim.check_exact(item) for item in s) + ) + + has_dimpacks_or_none = False + for i, s in enumerate(input_list): + if has_dims(s): + can_call_original_getitem = False + dims_indexed += 1 + elif s is ...: + check_expanding(i) + elif isinstance(s, DimList): + can_call_original_getitem = False + if not s.is_bound: + check_expanding(i) + unbound_dim_list = s + else: + dims_indexed += len(s._dims) + dimlists.append(i) + elif s is None: + has_dimpacks_or_none = True + elif is_dimpack(s): + can_call_original_getitem = False + has_dimpacks_or_none = True + dims_indexed += 1 + else: + dims_indexed += 1 + + # Early return if we can use original getitem + if can_call_original_getitem: + return IndexingInfo(can_call_original=True) + + self_info = TensorInfo.create(self, False, True) + total_dims = len(self_info.levels) # Total dimensions (positional + named) + if dims_indexed > total_dims: + raise ValueError( + f"at least {dims_indexed} indices were supplied but the tensor only has {total_dims} dimensions" + ) + + # Expand any unbound dimension list, or expand ... into individual : slices. + expanding_dims = total_dims - dims_indexed + if expanding_object != -1: + if unbound_dim_list is not None: + # Bind unbound dimension list to the expanding dimensions + unbound_dim_list.bind_len(expanding_dims) + else: + # Expand ... into slice(None) objects + no_slices = [slice(None)] * expanding_dims + input_list = ( + input_list[:expanding_object] + + no_slices + + input_list[expanding_object + 1 :] + ) + + # Flatten out any dimensions stored in dimlist elements directly into the inputs + # Process in reverse order to maintain indices + for i in range(len(dimlists) - 1, -1, -1): + idx = dimlists[i] + + # We added more elements to input because of ... + # so we need to also adjust the index to get back to where the + # dimlist existed + if ( + unbound_dim_list is None + and expanding_object != -1 + and idx > expanding_object + ): + idx += expanding_dims + + dl = input_list[idx] + + # PRIVATE here naughty + input_list = input_list[:idx] + dl._dims + input_list[idx + 1 :] + + return getsetitem_flat(self_info, input_list, [], [], has_dimpacks_or_none) + + +def getsetitem_flat( + self_info: TensorInfo, + input_list: list, + keys: list[DimEntry], + values: list, + has_dimpacks_or_none: bool, +) -> IndexingInfo: + from . import Dim + + # Track dimension usage + seen_dims: list[Any] = [] + seen_dims_nuses: list[int] = [] + + def add_dim(dim: Any) -> None: + # Use safe indexing to avoid triggering __torch_function__ on Dim objects + idx = _safe_index(seen_dims, dim) + if idx is not None: + seen_dims_nuses[idx] += 1 + else: + seen_dims.append(dim) + seen_dims_nuses.append(1) + + flat_inputs = [] + tensor_inputs: list[Any] = [] + device_holding_tensor = None + + def append_flat_handle(handle: Any) -> None: + flat_inputs.append(handle) + tensor_inputs.append(None) + + def append_tensor_input(ti: TensorInfo) -> None: + flat_inputs.append(None) + tensor_inputs.append(ti) + nonlocal device_holding_tensor + if ti.has_device and device_holding_tensor is None: + device_holding_tensor = ti.tensor + + nsz = [] + nsd = [] + if self_info.tensor is None: + raise RuntimeError("Cannot get size/stride on None tensor") + sz = self_info.tensor.size() + sd = self_info.tensor.stride() + + def append_size(i: int) -> None: + if has_dimpacks_or_none: + nsz.append(sz[i]) + nsd.append(sd[i]) + + input_it = input_list[:] + + def parse_nones() -> None: + nonlocal input_it + while input_it and input_it[0] is None: + append_flat_handle(slice(None)) + nsz.append(1) + nsd.append(0) + input_it = input_it[1:] + + def append_item(i: int, arg: Any) -> None: + if Dim.check_exact(arg): + d = arg + if d._size == -1: + d.size = sz[i] + add_dim(d) + append_size(i) + append_flat_handle(arg) + return + + info = TensorInfo.create(arg, False, False) + if info: + append_size(i) + append_tensor_input(info) + for level in info.levels: + if not level.is_positional(): + add_dim(level.dim()) + return + + if has_dimpacks_or_none: + if isinstance(arg, (tuple, list)) and all(Dim.check_exact(d) for d in arg): + # dim pack + dim_pack = list(arg) + for d in dim_pack: + add_dim(d) + append_flat_handle(d) + _bind_dims_to_size(sz[i], sd[i], dim_pack, nsz, nsd) + return + + append_size(i) + append_flat_handle(arg) + + # Match indexing expressions with tensor dimensions + for i, level in enumerate(self_info.levels): + # Use safe indexing to avoid triggering __torch_function__ on DimEntry comparisons + idx = _safe_index(keys, level) + if idx is not None: + append_item(i, values[idx]) + else: + if level.is_positional(): + parse_nones() + if not input_it: + append_flat_handle(slice(None)) + append_size(i) + else: + arg = input_it[0] + input_it = input_it[1:] + append_item(i, arg) + else: + add_dim(level.dim()) + append_flat_handle(level.dim()) + append_size(i) + + parse_nones() + + # Restride tensor if needed + if has_dimpacks_or_none and nsz: + if self_info.tensor is None: + raise RuntimeError("Cannot restride None tensor") + self_tensor = self_info.tensor.as_strided( + nsz, nsd, self_info.tensor.storage_offset() + ) + else: + self_tensor = self_info.tensor + + # Determine result shape and indexing requirements + result_levels: list[Any] = [] + index_levels = [] + tensor_insert_point = -1 + requires_getindex = False + + def mark_tensor_index() -> None: + nonlocal tensor_insert_point + if tensor_insert_point == -1: + tensor_insert_point = len(result_levels) + elif tensor_insert_point != len(result_levels): + tensor_insert_point = 0 + + for i, inp in enumerate(flat_inputs): + if tensor_inputs[i] is not None: + requires_getindex = True + mark_tensor_index() + for level in tensor_inputs[i].levels: + if level not in index_levels: + index_levels.append(level) + elif Dim.check_exact(inp): + d = inp + # Use safe indexing to avoid triggering __torch_function__ + dim_idx = _safe_index(seen_dims, d) + assert dim_idx is not None, f"Dim {d} not found in seen_dims" + if seen_dims_nuses[dim_idx] == 1: + flat_inputs[i] = slice(None) + result_levels.append(DimEntry(d)) + else: + requires_getindex = True + flat_inputs[i] = None + tensor_inputs[i] = TensorInfo( + d._get_range(), [DimEntry(d)], False, None + ) + if DimEntry(d) not in index_levels: + index_levels.append(DimEntry(d)) + mark_tensor_index() + else: + if inp != slice(None): + requires_getindex = True + if not isinstance(inp, int): + result_levels.append(DimEntry(-1)) + + # Insert indexing dimensions at first tensor use point + if tensor_insert_point != -1: + for level in reversed(index_levels): + result_levels.insert(tensor_insert_point, level) + + # Match tensors to indexing shape + if requires_getindex: + for i in range(len(flat_inputs)): + if tensor_inputs[i] is not None: + t = tensor_inputs[i].tensor + assert t is not None, "TensorInfo should have valid tensor data" + if ( + not tensor_inputs[i].has_device + and device_holding_tensor is not None + ): + t = t.to(device_holding_tensor.device) + flat_inputs[i] = _match_levels(t, tensor_inputs[i].levels, index_levels) + + # Number positional dimensions correctly + seen_positionals = 0 + for i in reversed(range(len(result_levels))): + if result_levels[i].is_positional(): + seen_positionals += 1 + result_levels[i] = DimEntry(-seen_positionals) + + return IndexingInfo( + can_call_original=False, + advanced_indexing=requires_getindex, + self_tensor=self_tensor, + flat_inputs=flat_inputs, + result_levels=result_levels, + has_device=self_info.has_device, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_order.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_order.py new file mode 100644 index 0000000000000000000000000000000000000000..baa0f82e4b2a2f89dbb9c27fe32d6ab1bcce42ab --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_order.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from typing import Any, TYPE_CHECKING, Union + + +if TYPE_CHECKING: + from collections.abc import Sequence + +import torch # noqa: TC002 + +from ._dim_entry import _match_levels, DimEntry, ndim_of_levels + + +def _wrap_dim(arg: Any, orig_ndim: int, allow_none: bool = True) -> DimEntry: + """ + Convert various dimension representations to DimEntry. + + Args: + arg: The argument to convert (Dim, int, or other) + orig_ndim: Original number of dimensions + allow_none: Whether to allow None values + + Returns: + DimEntry representation of the dimension + """ + from . import Dim + + if arg is None and allow_none: + return DimEntry() # None entry + elif isinstance(arg, Dim): + return DimEntry(arg) + elif isinstance(arg, int): + if arg < 0: + pos = arg + else: + pos = arg - orig_ndim + return DimEntry(pos) + else: + return DimEntry() + + +def order( + tensor_or_dim: Union[torch.Tensor, Any], *dims: Union[Any, Sequence[Any]] +) -> torch.Tensor: + """ + Reorder the dimensions of a tensor or create a tensor from a dimension. + + It allows reordering tensor dimensions using first-class dimensions and + positional indices. + + Args: + tensor_or_dim: Input tensor with first-class dimensions, or a Dim object + *dims: Dimensions or sequences of dimensions specifying the new order + + Returns: + Tensor with reordered dimensions + + Examples: + >>> import torch + >>> from functorch.dim import dims + >>> batch, channel, height, width = dims(4) + >>> x = torch.randn(2, 3, 4, 5)[batch, channel, height, width] + >>> # Reorder to [height, width, batch, channel] + >>> y = order(x, height, width, batch, channel) + """ + from . import Dim, DimList, Tensor + + # Handle first argument - tensor or dimension + if isinstance(tensor_or_dim, Tensor): + # First-class tensor + orig_levels = tensor_or_dim._levels[:] + data = tensor_or_dim._tensor + has_device = tensor_or_dim._has_device + elif isinstance(tensor_or_dim, Dim): + # Single dimension - create range tensor + orig_levels = [DimEntry(tensor_or_dim)] + data = tensor_or_dim._get_range() + has_device = False + else: + raise ValueError("First argument must be a Tensor or Dim object") + + flat_positional_dims = [] + to_flatten = [] # List of (start_index, length) pairs for flattening + levels = orig_levels[:] + + orig_ndim = ndim_of_levels(levels) + + def append_dim(d: DimEntry) -> None: + """Add a dimension to the reordering, removing it from available levels.""" + try: + idx = levels.index(d) + except ValueError: + idx = None + if idx is None: + if d.is_positional(): + raise ValueError( + f"tensor has {orig_ndim} positional dimensions, but {d.position() + orig_ndim} specified, " + f"or it was specified twice" + ) + else: + raise ValueError( + f"tensor does not contain dim {d.dim()} or it was specified twice" + ) + + levels[idx] = DimEntry() + flat_positional_dims.append(d) + + n_new_positional = 0 + + # Process each dimension argument + for arg in dims: + entry = _wrap_dim(arg, orig_ndim, False) + if not entry.is_none(): + append_dim(entry) + n_new_positional += 1 + elif isinstance(arg, DimList): + # Handle DimList + for dim in arg._dims: + append_dim(DimEntry(dim)) + n_new_positional += 1 + else: + # Handle sequences of dimensions for flattening + n_new_positional += 1 + if not hasattr(arg, "__iter__"): + raise ValueError("expected a Dim, List[Dim], or Sequence[Dim]") + + # Convert to list to get length + seq = list(arg) + to_flatten.append((len(flat_positional_dims), len(seq))) + + for item in seq: + entry = _wrap_dim(item, orig_ndim, False) + if entry.is_none(): + raise ValueError("expected a Dim or int") + append_dim(entry) + + # Build new level ordering + insert_point = -1 + new_levels: list[DimEntry] = [] + + # Add remaining (non-reordered) levels, finding insertion point for new dimensions + for level in levels: + if level.is_none(): + continue + if level.is_positional(): + if insert_point == -1: + insert_point = len(new_levels) + new_levels.extend(flat_positional_dims) + new_levels.append(level) + + # If no positional dimensions found, append new dims at the end + if insert_point == -1: + insert_point = len(new_levels) + new_levels.extend(flat_positional_dims) + + # Match tensor to new level structure + assert data is not None, "Cannot reorder None tensor" + ndata = _match_levels(data, orig_levels, new_levels) + + # Handle dimension flattening if requested + if to_flatten: + # Now build the reshape target + view_shape = [] + sizes = ndata.size() + + # Add dimensions before the reordered ones + for i in range(insert_point): + view_shape.append(sizes[i]) + + # Process flattening groups + i = 0 + for start_idx, length in to_flatten: + # Add individual dims before this flattening group + while i < start_idx: + view_shape.append(sizes[insert_point + i]) + i += 1 + + # Flatten the group + new_size = 1 + for j in range(length): + new_size *= sizes[insert_point + i + j] + view_shape.append(new_size) + i += length + + # Add remaining individual dims + while i < len(flat_positional_dims): + view_shape.append(sizes[insert_point + i]) + i += 1 + + # Add dimensions after the reordered ones + for i in range(insert_point + len(flat_positional_dims), len(levels)): + view_shape.append(sizes[i]) + + # Update levels by removing flattened dimensions + n_to_remove = len(flat_positional_dims) - n_new_positional + if n_to_remove > 0: + # Remove flattened levels + new_levels = ( + new_levels[:insert_point] + new_levels[insert_point + n_to_remove :] + ) + + ndata = ndata.reshape(view_shape) + + # Renumber positional dimensions (negative indexing from the right) + seen = 0 + for i in range(len(new_levels) - 1, -1, -1): + if new_levels[i].is_positional() or ( + i >= insert_point and i < insert_point + n_new_positional + ): + seen -= 1 + new_levels[i] = DimEntry(seen) + + result = Tensor.from_positional(ndata, new_levels, has_device) + return result # type: ignore[return-value] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_py_inst_decoder.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_py_inst_decoder.py new file mode 100644 index 0000000000000000000000000000000000000000..7f08ebb8557fb456e66eb2e2b8cf788d51098c20 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_py_inst_decoder.py @@ -0,0 +1,67 @@ +import dis +from typing import Any, Optional + + +class _PyInstDecoder: + """ + Decodes Python bytecode instructions to extract variable names + """ + + def __init__(self, code_object: Any, lasti: int) -> None: + self.code_object = code_object + self.instructions = list(dis.get_instructions(code_object)) + self.offset = self._find_instruction_index(lasti) + + def _find_instruction_index(self, lasti: int) -> int: + """Find instruction index corresponding to lasti (byte offset).""" + # Find the instruction at or before lasti + # This should find the CALL instruction, not the next one + best_idx = 0 + for i, instr in enumerate(self.instructions): + if instr.offset <= lasti: + best_idx = i + else: + break + return best_idx + + def next(self) -> None: + """Advance to the next instruction.""" + self.offset += 1 + + def opcode(self) -> Optional[str]: + """Get the opcode name of the current instruction.""" + if self.offset < len(self.instructions): + return self.instructions[self.offset].opname + return None + + def oparg(self) -> int: + """Get the argument of the current instruction.""" + if self.offset < len(self.instructions): + return self.instructions[self.offset].arg or 0 + return 0 + + def name(self) -> Optional[str]: + """ + Extract variable name from current instruction. + """ + opname = self.opcode() + if not opname: + return None + + names = None + if opname in ("STORE_NAME", "STORE_GLOBAL"): + names = self.code_object.co_names + elif opname == "STORE_FAST": + names = self.code_object.co_varnames + elif opname == "STORE_DEREF": + names = self.code_object.co_cellvars + if not names: + names = self.code_object.co_freevars + else: + return None + + arg = self.oparg() + if names and 0 <= arg < len(names): + return names[arg] + + return None diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_tensor_info.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_tensor_info.py new file mode 100644 index 0000000000000000000000000000000000000000..1e2513e36c05882452a6b83ca9e8d67234bf82d1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_tensor_info.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Optional, TYPE_CHECKING + +import torch + + +if TYPE_CHECKING: + from ._dim_entry import DimEntry + + +@dataclass +class TensorInfo: + tensor: Optional[torch.Tensor] + levels: list[DimEntry] + has_device: bool + batchedtensor: Optional[torch.Tensor] + + def __post_init__(self) -> None: + from ._dim_entry import DimEntry + + assert all(isinstance(l, DimEntry) for l in self.levels) + + def ndim(self) -> int: + from ._dim_entry import ndim_of_levels + + return ndim_of_levels(self.levels) + + def __bool__(self) -> bool: + return self.tensor is not None + + @staticmethod + def create( + h: Any, ensure_batched: bool = True, ensure_present: bool = True + ) -> TensorInfo: + from . import Dim, DimEntry, Tensor + + if Tensor.check_exact(h): + # functorch Tensor with first-class dimensions + return TensorInfo( + h._get_tensor(), + h._get_levels(), + h._get_has_device(), + h._get_batchtensor() if ensure_batched else None, + ) + elif Dim.check_exact(h): + # For Dim objects, only get range/batchtensor if needed and dimension is bound + tensor = h._get_range() if h.is_bound else None + batchtensor = ( + h._get_batchtensor() if ensure_batched and h.is_bound else None + ) + return TensorInfo( + tensor, + [DimEntry(h)], + False, + batchtensor, + ) + elif isinstance(h, torch.Tensor): + # Plain torch tensor - create positional levels + levels = [] + for i in range(-h.dim(), 0): + levels.append(DimEntry(i)) + return TensorInfo(h, levels, True, h) + else: + if ensure_present: + raise ValueError("expected a tensor object") + return TensorInfo(None, [], False, None) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_wrap.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_wrap.py new file mode 100644 index 0000000000000000000000000000000000000000..3c3a12b54cebc815f733e3400768528a2a9fd419 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/_wrap.py @@ -0,0 +1,267 @@ +""" +Python implementation of function wrapping functionality for functorch.dim. +""" + +from __future__ import annotations + +import functools +from typing import Any, Optional, TYPE_CHECKING + +import torch +from torch.utils._pytree import tree_map + +from ._dim_entry import DimEntry +from ._enable_all_layers import EnableAllLayers +from ._tensor_info import TensorInfo + + +if TYPE_CHECKING: + from collections.abc import Callable + + +def handle_from_tensor(tensor: torch.Tensor) -> torch.Tensor: + """Handle tensor conversion for torch function integration.""" + return tensor + + +class WrappedOperator: + """ + This class wraps PyTorch operations to support first-class dimensions. + """ + + def __init__( + self, orig: Callable, wrapper_implementation: Callable, dim_name: str = "dim" + ): + self.orig = orig + self.wrapper_implementation = wrapper_implementation + self.name = getattr(orig, "__name__", "") + self.doc = getattr(orig, "__doc__", None) + self.dim_name = dim_name + + self.is_pointwise = False + self.dim_offset = 0 + self.keepdim_offset = 1 + self.single_dim = False + self.reduce = True + + # Update docstring if we have a dim_name + if self.doc and self.dim_name: + self.doc = f"{self.doc}\nArgument '{self.dim_name}' can be either an integer or a torchdim.Dim object.\n" + + def function(self) -> Callable: + """Create a wrapped function that calls our wrapper implementation.""" + + def wrapped_func(*args: Any, **kwargs: Any) -> Any: + return self.wrapper_implementation(self, *args, **kwargs) + + # Copy metadata using functools.update_wrapper for just __name__ and __doc__ + functools.update_wrapper( + wrapped_func, self.orig, assigned=("__name__",), updated=() + ) + wrapped_func.__doc__ = self.doc + + return wrapped_func + + +def _wrap_dim(dim: Any, ndim: int, keepdim: bool = False) -> DimEntry: + """Convert single dimension specification to DimEntry object.""" + from . import Dim + + if isinstance(dim, Dim): + if keepdim: + raise ValueError("cannot preserve first-class dimensions with keepdim=True") + return DimEntry(dim) + elif isinstance(dim, int): + i = dim + while i >= 0: + i -= ndim + return DimEntry(i) + else: + return DimEntry() + + +def _wrap_dims(dim: Any, ndim: int, keepdim: bool = False) -> list[DimEntry]: + """Convert dimension specification to list of DimEntry objects.""" + de = _wrap_dim(dim, ndim, keepdim) + result = [] + if not de.is_none(): + result.append(de) + else: + for d in dim: + result.append(_wrap_dim(d, ndim, keepdim)) + return result + + +def patched_dim_method(wrapper: WrappedOperator, *args: Any, **kwargs: Any) -> Any: + """ + This is the core method that handles dimension-aware operations. + """ + if not args: + raise ValueError("Expected at least one argument (self)") + + # Get dimension argument + dim_arg = kwargs.get(wrapper.dim_name) + if dim_arg is None and wrapper.dim_offset < len(args): + # Try to get dim from positional args (accounting for self at index 0) + dim_idx = wrapper.dim_offset + 1 + if dim_idx < len(args): + dim_arg = args[dim_idx] + + # If no dimension argument provided, fall back to standard functorch handling + if dim_arg is None: + info = TensorInfo.create(args[0], ensure_batched=True, ensure_present=False) + if not info: + return wrapper.orig(*args, **kwargs) + + with EnableAllLayers(info.levels) as guard: + assert info.batchedtensor is not None + guard.inplace_update_layers(info.batchedtensor, info.levels) + new_args = list(args) + new_args[0] = handle_from_tensor(info.batchedtensor) + result = wrapper.orig(*new_args, **kwargs) + return guard.from_batched(result, info.has_device) + + # Handle dimension-aware operation + info = TensorInfo.create(args[0]) + if not info: + return wrapper.orig(*args, **kwargs) + + # Check for keepdim parameter + keepdim = False + if wrapper.reduce: + keepdim_arg = kwargs.get("keepdim") + if keepdim_arg is None and wrapper.keepdim_offset < len(args): + keepdim_idx = wrapper.keepdim_offset + 1 + if keepdim_idx < len(args): + keepdim_arg = args[keepdim_idx] + if keepdim_arg is not None: + keepdim = bool(keepdim_arg) + + # Wrap dimensions + ndim = info.ndim() + dims = _wrap_dims(dim_arg, ndim, keepdim) + + # Convert dimensions to indices and validate + dim_indices: list[int] = [] + seen = [False] * len(info.levels) + + for d in dims: + midx = None + for i, level in enumerate(info.levels): + if level == d: + midx = i + break + + if midx is None: + # Try to match by position/name more flexibly + for i, level in enumerate(info.levels): + if hasattr(level, "matches") and level.matches(d): + midx = i + break + + if midx is None: + level_strs = [str(level) for level in info.levels] + raise ValueError( + f"Tensor with dimensions {level_strs} does not contain {d}" + ) + + seen[midx] = True + dim_indices.append(midx) + + # Determine new levels after reduction + new_levels = [] + if wrapper.reduce and not keepdim: + for i, level in enumerate(info.levels): + if not seen[i]: + new_levels.append(level) + else: + new_levels = info.levels[:] + + # Create dimension indices for the original function + if len(dim_indices) == 1: + py_indices: Any = dim_indices[0] + else: + py_indices = tuple(dim_indices) + + # Update arguments + new_args = list(args) + new_kwargs = kwargs.copy() + assert info.tensor is not None + new_args[0] = handle_from_tensor(info.tensor) + + # Update dimension argument + if wrapper.dim_name in new_kwargs: + new_kwargs[wrapper.dim_name] = py_indices + else: + dim_idx = wrapper.dim_offset + 1 + if dim_idx < len(new_args): + new_args = list(new_args) + new_args[dim_idx] = py_indices + + # Call original function + result = wrapper.orig(*new_args, **new_kwargs) + + # Wrap results + def wrap_result(obj: Any) -> Any: + if isinstance(obj, torch.Tensor): + from . import Tensor + + return Tensor.from_positional(obj, new_levels, info.has_device) + return obj + + return tree_map(wrap_result, result) + + +def _wrap( + orig: Callable, + dim_offset: Optional[int] = None, + keepdim_offset: Optional[int] = None, + dim_name: Optional[str] = None, + single_dim: Optional[bool] = None, + reduce: Optional[bool] = None, +) -> Callable: + """ + Wrap a PyTorch function to support first-class dimensions. + + Args: + orig: Original function to wrap + dim_offset: Offset for dimension argument (default: 0) + keepdim_offset: Offset for keepdim argument (default: 1) + dim_name: Name of dimension parameter (default: "dim") + single_dim: Whether function takes single dimension (default: False) + reduce: Whether function reduces dimensions (default: True) + """ + dim_name = dim_name or "dim" + + wrapper = WrappedOperator(orig, patched_dim_method, dim_name) + + if dim_offset is not None: + wrapper.dim_offset = dim_offset + if keepdim_offset is not None: + wrapper.keepdim_offset = keepdim_offset + if single_dim is not None: + wrapper.single_dim = single_dim + if reduce is not None: + wrapper.reduce = reduce + + return wrapper.function() + + +def call_torch_function( + wrapper: WrappedOperator, + func: Callable, + types: tuple, + args: tuple = (), + kwargs: Optional[dict] = None, +) -> Any: + """ + Handle __torch_function__ calls for wrapped operators. + """ + if kwargs is None: + kwargs = {} + + # Import here to avoid circular imports + from . import _Tensor + + # Use the torch function mechanism from _Tensor + return _Tensor.__torch_function__(func, types, args, kwargs) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/magic_trace.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/magic_trace.py new file mode 100644 index 0000000000000000000000000000000000000000..d3be42cd5514c587b31ab8009e55d5e363bbd6c5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/magic_trace.py @@ -0,0 +1,47 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +import os +import signal +import subprocess +from collections.abc import Generator +from contextlib import contextmanager + + +@contextmanager +def magic_trace( + output: str = "trace.fxt", magic_trace_cache: str = "/tmp/magic-trace" +) -> Generator[None, None, None]: + pid = os.getpid() + if not os.path.exists(magic_trace_cache): + print(f"Downloading magic_trace to: {magic_trace_cache}") + subprocess.run( + [ + "wget", + "-O", + magic_trace_cache, + "-q", + "https://github.com/janestreet/magic-trace/releases/download/v1.0.2/magic-trace", + ] + ) + subprocess.run(["chmod", "+x", magic_trace_cache]) + args = [magic_trace_cache, "attach", "-pid", str(pid), "-o", output] + p = subprocess.Popen(args, stderr=subprocess.PIPE, encoding="utf-8") + assert p.stderr is not None + while True: + x = p.stderr.readline() + print(x) + if "Attached" in x: + break + try: + yield + finally: + p.send_signal(signal.SIGINT) + r = p.wait() + if p.stderr is not None: + print(p.stderr.read()) + p.stderr.close() + if r != 0: + raise ValueError(f"magic_trace exited abnormally: {r}") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/op_properties.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/op_properties.py new file mode 100644 index 0000000000000000000000000000000000000000..01313f71f030d58ce76c15c7f8516c4a0bdcf48a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/op_properties.py @@ -0,0 +1,312 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +import torch + + +# pointwise operators can go through a faster pathway + +tensor_magic_methods = ["add", ""] +pointwise_magic_methods_with_reverse = ( + "add", + "sub", + "mul", + "floordiv", + "div", + "truediv", + "mod", + "pow", + "lshift", + "rshift", + "and", + "or", + "xor", +) +pointwise_magic_methods = ( + *(x for m in pointwise_magic_methods_with_reverse for x in (m, "r" + m)), + "eq", + "gt", + "le", + "lt", + "ge", + "gt", + "ne", + "neg", + "pos", + "abs", + "invert", + "iadd", + "isub", + "imul", + "ifloordiv", + "idiv", + "itruediv", + "imod", + "ipow", + "ilshift", + "irshift", + "iand", + "ior", + "ixor", + "int", + "long", + "float", + "complex", +) + +pointwise_methods = (*(f"__{m}__" for m in pointwise_magic_methods),) + +pointwise = ( + *(getattr(torch.Tensor, m) for m in pointwise_methods), + torch.nn.functional.dropout, + torch.where, + torch.Tensor.abs, + torch.abs, + torch.Tensor.acos, + torch.acos, + torch.Tensor.acosh, + torch.acosh, + torch.Tensor.add, + torch.add, + torch.Tensor.addcdiv, + torch.addcdiv, + torch.Tensor.addcmul, + torch.addcmul, + torch.Tensor.addr, + torch.addr, + torch.Tensor.angle, + torch.angle, + torch.Tensor.asin, + torch.asin, + torch.Tensor.asinh, + torch.asinh, + torch.Tensor.atan, + torch.atan, + torch.Tensor.atan2, + torch.atan2, + torch.Tensor.atanh, + torch.atanh, + torch.Tensor.bitwise_and, + torch.bitwise_and, + torch.Tensor.bitwise_left_shift, + torch.bitwise_left_shift, + torch.Tensor.bitwise_not, + torch.bitwise_not, + torch.Tensor.bitwise_or, + torch.bitwise_or, + torch.Tensor.bitwise_right_shift, + torch.bitwise_right_shift, + torch.Tensor.bitwise_xor, + torch.bitwise_xor, + torch.Tensor.ceil, + torch.ceil, + torch.celu, + torch.nn.functional.celu, + torch.Tensor.clamp, + torch.clamp, + torch.Tensor.clamp_max, + torch.clamp_max, + torch.Tensor.clamp_min, + torch.clamp_min, + torch.Tensor.copysign, + torch.copysign, + torch.Tensor.cos, + torch.cos, + torch.Tensor.cosh, + torch.cosh, + torch.Tensor.deg2rad, + torch.deg2rad, + torch.Tensor.digamma, + torch.digamma, + torch.Tensor.div, + torch.div, + torch.dropout, + torch.nn.functional.dropout, + torch.nn.functional.elu, + torch.Tensor.eq, + torch.eq, + torch.Tensor.erf, + torch.erf, + torch.Tensor.erfc, + torch.erfc, + torch.Tensor.erfinv, + torch.erfinv, + torch.Tensor.exp, + torch.exp, + torch.Tensor.exp2, + torch.exp2, + torch.Tensor.expm1, + torch.expm1, + torch.feature_dropout, + torch.Tensor.float_power, + torch.float_power, + torch.Tensor.floor, + torch.floor, + torch.Tensor.floor_divide, + torch.floor_divide, + torch.Tensor.fmod, + torch.fmod, + torch.Tensor.frac, + torch.frac, + torch.Tensor.frexp, + torch.frexp, + torch.Tensor.gcd, + torch.gcd, + torch.Tensor.ge, + torch.ge, + torch.nn.functional.gelu, + torch.nn.functional.glu, + torch.Tensor.gt, + torch.gt, + torch.Tensor.hardshrink, + torch.hardshrink, + torch.nn.functional.hardshrink, + torch.nn.functional.hardsigmoid, + torch.nn.functional.hardswish, + torch.nn.functional.hardtanh, + torch.Tensor.heaviside, + torch.heaviside, + torch.Tensor.hypot, + torch.hypot, + torch.Tensor.i0, + torch.i0, + torch.Tensor.igamma, + torch.igamma, + torch.Tensor.igammac, + torch.igammac, + torch.Tensor.isclose, + torch.isclose, + torch.Tensor.isfinite, + torch.isfinite, + torch.Tensor.isinf, + torch.isinf, + torch.Tensor.isnan, + torch.isnan, + torch.Tensor.isneginf, + torch.isneginf, + torch.Tensor.isposinf, + torch.isposinf, + torch.Tensor.isreal, + torch.isreal, + torch.Tensor.kron, + torch.kron, + torch.Tensor.lcm, + torch.lcm, + torch.Tensor.ldexp, + torch.ldexp, + torch.Tensor.le, + torch.le, + torch.nn.functional.leaky_relu, + torch.Tensor.lerp, + torch.lerp, + torch.Tensor.lgamma, + torch.lgamma, + torch.Tensor.log, + torch.log, + torch.Tensor.log10, + torch.log10, + torch.Tensor.log1p, + torch.log1p, + torch.Tensor.log2, + torch.log2, + torch.nn.functional.logsigmoid, + torch.Tensor.logical_and, + torch.logical_and, + torch.Tensor.logical_not, + torch.logical_not, + torch.Tensor.logical_or, + torch.logical_or, + torch.Tensor.logical_xor, + torch.logical_xor, + torch.Tensor.logit, + torch.logit, + torch.Tensor.lt, + torch.lt, + torch.Tensor.maximum, + torch.maximum, + torch.Tensor.minimum, + torch.minimum, + torch.nn.functional.mish, + torch.Tensor.mvlgamma, + torch.mvlgamma, + torch.Tensor.nan_to_num, + torch.nan_to_num, + torch.Tensor.ne, + torch.ne, + torch.Tensor.neg, + torch.neg, + torch.Tensor.nextafter, + torch.nextafter, + torch.Tensor.outer, + torch.outer, + torch.polar, + torch.Tensor.polygamma, + torch.polygamma, + torch.Tensor.positive, + torch.positive, + torch.Tensor.pow, + torch.pow, + torch.Tensor.prelu, + torch.prelu, + torch.nn.functional.prelu, + torch.Tensor.rad2deg, + torch.rad2deg, + torch.Tensor.reciprocal, + torch.reciprocal, + torch.Tensor.relu, + torch.relu, + torch.nn.functional.relu, + torch.nn.functional.relu6, + torch.Tensor.remainder, + torch.remainder, + torch.Tensor.round, + torch.round, + torch.rrelu, + torch.nn.functional.rrelu, + torch.Tensor.rsqrt, + torch.rsqrt, + torch.rsub, + torch.selu, + torch.nn.functional.selu, + torch.Tensor.sgn, + torch.sgn, + torch.Tensor.sigmoid, + torch.sigmoid, + torch.nn.functional.sigmoid, + torch.Tensor.sign, + torch.sign, + torch.Tensor.signbit, + torch.signbit, + torch.nn.functional.silu, + torch.Tensor.sin, + torch.sin, + torch.Tensor.sinc, + torch.sinc, + torch.Tensor.sinh, + torch.sinh, + torch.nn.functional.softplus, + torch.nn.functional.softshrink, + torch.Tensor.sqrt, + torch.sqrt, + torch.Tensor.square, + torch.square, + torch.Tensor.sub, + torch.sub, + torch.Tensor.tan, + torch.tan, + torch.Tensor.tanh, + torch.tanh, + torch.nn.functional.tanh, + torch.threshold, + torch.nn.functional.threshold, + torch.trapz, + torch.Tensor.true_divide, + torch.true_divide, + torch.Tensor.trunc, + torch.trunc, + torch.Tensor.xlogy, + torch.xlogy, + torch.rand_like, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/wrap_type.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/wrap_type.py new file mode 100644 index 0000000000000000000000000000000000000000..5020e756ce6c68ee78aeb43ae787739a46125cc6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/dim/wrap_type.py @@ -0,0 +1,71 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +import functools +from collections.abc import Callable +from types import ( + BuiltinMethodType, + FunctionType, + GetSetDescriptorType, + MethodDescriptorType, + WrapperDescriptorType, +) +from typing import Any + + +FUNC_TYPES = ( + FunctionType, + MethodDescriptorType, + BuiltinMethodType, + WrapperDescriptorType, +) +PROPERTY_TYPES = (GetSetDescriptorType, property) + + +def _py_wrap_method(orig: Callable, __torch_function__: Callable) -> Callable: + def impl(*args: Any, **kwargs: Any) -> Any: + return __torch_function__(orig, None, args, kwargs) + + # Copy metadata using functools.update_wrapper for just __name__ and __doc__ + functools.update_wrapper(impl, orig, assigned=("__name__", "__doc__"), updated=()) + + return impl + + +def wrap_type(to_patch: Any, pattern: type, __torch_function__: Callable) -> None: + wrap_method = _py_wrap_method + + all: dict[str, Any] = {} + for t in reversed(pattern.mro()[:-1]): # skip object + all.update(t.__dict__) + + def wrap_attr(orig: Any) -> property: + return property(wrap_method(orig.__get__, __torch_function__)) + + for name, obj in all.items(): + if name in ( + "__dict__", + "__new__", + "__init__", + "__repr__", + "__weakref__", + "__doc__", + "__module__", + "__dir__", + ): + continue + + # skip things that have been overloaded + # things that come from object like `__eq__` still need to be patched, however. + if hasattr(to_patch, name) and getattr(to_patch, name) is not getattr( + object, name, None + ): + continue + + if isinstance(obj, FUNC_TYPES): + setattr(to_patch, name, wrap_method(obj, __torch_function__)) + elif isinstance(obj, PROPERTY_TYPES): + setattr(to_patch, name, wrap_attr(obj)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d7ac34f7a3722010fc0fde97fd1cd72e76fa88b7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__init__.py @@ -0,0 +1,4 @@ +from .rearrange import rearrange + + +__all__ = ["rearrange"] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..687771d81def42d1911661aefcd61b813f7b9246 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/_parsing.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/_parsing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c5ceeba3432dda2a6ff81b6e83f9185aec02829 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/_parsing.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/rearrange.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/rearrange.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09f7e0bec49d108c2aad0fbe55c07a2cea85dbc1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/__pycache__/rearrange.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/_parsing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/_parsing.py new file mode 100644 index 0000000000000000000000000000000000000000..2352ea932426271fdc16f660abb4308ea9b3c924 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/_parsing.py @@ -0,0 +1,308 @@ +"""Adapted from https://github.com/arogozhnikov/einops/blob/36c7bb16e57d6e57f8f3050f9e07abdf3f00469f/einops/parsing.py. + +MIT License + +Copyright (c) 2018 Alex Rogozhnikov + +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. +""" + +from __future__ import annotations + +import keyword +import warnings +from typing import Optional, TYPE_CHECKING, Union + + +if TYPE_CHECKING: + from collections.abc import Collection, Mapping + + +_ellipsis: str = "\u2026" # NB, this is a single unicode symbol. String is used as it is not a list, but can be iterated + + +class AnonymousAxis: + """Used by `ParsedExpression` to represent an axis with a size (> 1), but no associated identifier. + + Note: Different instances of this class are not equal to each other, even if they have the same value. + """ + + def __init__(self, value: str) -> None: + self.value = int(value) + if self.value < 1: + raise ValueError( + f"Anonymous axis should have positive length, not {self.value}" + ) + + def __repr__(self) -> str: + return f"{self.value}-axis" + + +class ParsedExpression: + """Structure containing information about one side of an `einops`-style pattern (e.g. 'b c (h w)').""" + + def __init__( + self, + expression: str, + *, + allow_underscore: bool = False, + allow_duplicates: bool = False, + ) -> None: + """Parse the expression and store relevant metadata. + + Args: + expression (str): the `einops`-pattern to parse + allow_underscore (bool): whether to allow axis identifier names to begin with an underscore + allow_duplicates (bool): whether to allow an identifier to appear more than once in the expression + """ + self.has_ellipsis: bool = False + self.has_ellipsis_parenthesized: Optional[bool] = None + self.identifiers: set[Union[str, AnonymousAxis]] = set() + # that's axes like 2, 3, 4 or 5. Axes with size 1 are exceptional and replaced with empty composition + self.has_non_unitary_anonymous_axes: bool = False + # composition keeps structure of composite axes, see how different corner cases are handled in tests + self.composition: list[Union[list[Union[str, AnonymousAxis]], str]] = [] + if "." in expression: + if "..." not in expression: + raise ValueError( + "Expression may contain dots only inside ellipsis (...)" + ) + if str.count(expression, "...") != 1 or str.count(expression, ".") != 3: + raise ValueError( + "Expression may contain dots only inside ellipsis (...); only one ellipsis for tensor " + ) + expression = expression.replace("...", _ellipsis) + self.has_ellipsis = True + + bracket_group: Optional[list[Union[str, AnonymousAxis]]] = None + + def add_axis_name(x: str) -> None: + if x in self.identifiers: + if not (allow_underscore and x == "_") and not allow_duplicates: + raise ValueError( + f"Indexing expression contains duplicate dimension '{x}'" + ) + if x == _ellipsis: + self.identifiers.add(_ellipsis) + if bracket_group is None: + self.composition.append(_ellipsis) + self.has_ellipsis_parenthesized = False + else: + bracket_group.append(_ellipsis) + self.has_ellipsis_parenthesized = True + else: + is_number = str.isdecimal(x) + if is_number and int(x) == 1: + # handling the case of anonymous axis of length 1 + if bracket_group is None: + self.composition.append([]) + else: + pass # no need to think about 1s inside parenthesis + return + is_axis_name, reason = self.check_axis_name_return_reason( + x, allow_underscore=allow_underscore + ) + if not (is_number or is_axis_name): + raise ValueError(f"Invalid axis identifier: {x}\n{reason}") + axis_name: Union[str, AnonymousAxis] = ( + AnonymousAxis(x) if is_number else x + ) + self.identifiers.add(axis_name) + if is_number: + self.has_non_unitary_anonymous_axes = True + if bracket_group is None: + self.composition.append([axis_name]) + else: + bracket_group.append(axis_name) + + current_identifier = None + for char in expression: + if char in "() ": + if current_identifier is not None: + add_axis_name(current_identifier) + current_identifier = None + if char == "(": + if bracket_group is not None: + raise ValueError( + "Axis composition is one-level (brackets inside brackets not allowed)" + ) + bracket_group = [] + elif char == ")": + if bracket_group is None: + raise ValueError("Brackets are not balanced") + self.composition.append(bracket_group) + bracket_group = None + elif str.isalnum(char) or char in ["_", _ellipsis]: + if current_identifier is None: + current_identifier = char + else: + current_identifier += char + else: + raise ValueError(f"Unknown character '{char}'") + + if bracket_group is not None: + raise ValueError(f"Imbalanced parentheses in expression: '{expression}'") + if current_identifier is not None: + add_axis_name(current_identifier) + + @staticmethod + def check_axis_name_return_reason( + name: str, allow_underscore: bool = False + ) -> tuple[bool, str]: + """Check if the given axis name is valid, and a message explaining why if not. + + Valid axes names are python identifiers except keywords, and should not start or end with an underscore. + + Args: + name (str): the axis name to check + allow_underscore (bool): whether axis names are allowed to start with an underscore + + Returns: + tuple[bool, str]: whether the axis name is valid, a message explaining why if not + """ + if not str.isidentifier(name): + return False, "not a valid python identifier" + elif name[0] == "_" or name[-1] == "_": + if name == "_" and allow_underscore: + return True, "" + return False, "axis name should should not start or end with underscore" + else: + if keyword.iskeyword(name): + warnings.warn( + f"It is discouraged to use axes names that are keywords: {name}", + RuntimeWarning, + ) + if name in ["axis"]: + warnings.warn( + "It is discouraged to use 'axis' as an axis name and will raise an error in future", + FutureWarning, + ) + return True, "" + + @staticmethod + def check_axis_name(name: str) -> bool: + """Check if the name is a valid axis name. + + Args: + name (str): the axis name to check + + Returns: + bool: whether the axis name is valid + """ + is_valid, _ = ParsedExpression.check_axis_name_return_reason(name) + return is_valid + + +def parse_pattern( + pattern: str, axes_lengths: Mapping[str, int] +) -> tuple[ParsedExpression, ParsedExpression]: + """Parse an `einops`-style pattern into a left-hand side and right-hand side `ParsedExpression` object. + + Args: + pattern (str): the `einops`-style rearrangement pattern + axes_lengths (Mapping[str, int]): any additional length specifications for dimensions + + Returns: + tuple[ParsedExpression, ParsedExpression]: a tuple containing the left-hand side and right-hand side expressions + """ + # adapted from einops.einops._prepare_transformation_recipe + # https://github.com/arogozhnikov/einops/blob/230ac1526c1f42c9e1f7373912c7f8047496df11/einops/einops.py + try: + left_str, right_str = pattern.split("->") + except ValueError: + raise ValueError("Pattern must contain a single '->' separator") from None + + if _ellipsis in axes_lengths: + raise ValueError(f"'{_ellipsis}' is not an allowed axis identifier") + + left = ParsedExpression(left_str) + right = ParsedExpression(right_str) + + if not left.has_ellipsis and right.has_ellipsis: + raise ValueError( + f"Ellipsis found in right side, but not left side of a pattern {pattern}" + ) + if left.has_ellipsis and left.has_ellipsis_parenthesized: + raise ValueError( + f"Ellipsis is parenthesis in the left side is not allowed: {pattern}" + ) + + return left, right + + +def validate_rearrange_expressions( + left: ParsedExpression, right: ParsedExpression, axes_lengths: Mapping[str, int] +) -> None: + """Perform expression validations that are specific to the `rearrange` operation. + + Args: + left (ParsedExpression): left-hand side expression + right (ParsedExpression): right-hand side expression + axes_lengths (Mapping[str, int]): any additional length specifications for dimensions + """ + for length in axes_lengths.values(): + if (length_type := type(length)) is not int: + raise TypeError( + f"rearrange axis lengths must be integers, got: {length_type}" + ) + + if left.has_non_unitary_anonymous_axes or right.has_non_unitary_anonymous_axes: + raise ValueError("rearrange only supports unnamed axes of size 1") + + difference = set.symmetric_difference(left.identifiers, right.identifiers) + if len(difference) > 0: + raise ValueError( + f"Identifiers only on one side of rearrange expression (should be on both): {difference}" + ) + + unmatched_axes = axes_lengths.keys() - left.identifiers + if len(unmatched_axes) > 0: + raise ValueError( + f"Identifiers not found in rearrange expression: {unmatched_axes}" + ) + + +def comma_separate(collection: Collection[Union[str, Collection[str]]]) -> str: + """Convert a collection of strings representing first class dims into a comma-separated string. + + Args: + collection (Collection[Union[str, Collection[str]]]): the collection of strings to convert + + Returns: + str: the comma-separated string + + Examples: + >>> comma_separate(("d0",)) + 'd0' + + >>> comma_separate(("d0", "d1", "d2", "d3")) + 'd0, d1, d2, d3' + + >>> comma_separate([("d1", "d4")]) + '(d1, d4)' + + >>> comma_separate([("d0",), (), ("d1",), ("d2",), ("d3", "d4")]) + '(d0,), (), (d1,), (d2,), (d3, d4)' + """ + return ", ".join( + item + if isinstance(item, str) + else f"({comma_separate(item)}{',' if len(item) == 1 else ''})" + for item in collection + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/rearrange.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/rearrange.py new file mode 100644 index 0000000000000000000000000000000000000000..21e3bfaad4d8351053d29883a228b837947d1924 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/einops/rearrange.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import functools +from typing import TYPE_CHECKING, Union + +import torch +from functorch.dim import dims # noqa: F401 + +from ._parsing import ( + _ellipsis, + AnonymousAxis, + comma_separate, + parse_pattern, + validate_rearrange_expressions, +) + + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + +__all__ = ["rearrange"] + + +@functools.lru_cache(256) +def _create_rearrange_callable( + tensor_ndim: int, pattern: str, **axes_lengths: int +) -> Callable[[torch.Tensor], torch.Tensor]: + r"""Translate an `einops`-style pattern into a callable that performs the rearrange using first-class dimensions. + + Since the an equivalent result is computed for tensors with the same number of dimensions, with the same pattern and + specified axes lengths, this function can be memoized. + + Args: + tensor_ndim (int): the number of dimensions in the tensor to rearrange + pattern (str): the `einops`-style rearrangement pattern + axes_lengths (int): any additional length specifications for dimensions + + Returns: + Callable[[torch.Tensor], torch.Tensor]: a callable that performs the rearrangement + """ + left, right = parse_pattern(pattern, axes_lengths) + validate_rearrange_expressions(left, right, axes_lengths) + + n_anon_dims = sum(not dim for dim in left.composition) + if left.has_ellipsis: + n_ellipsis_dims = tensor_ndim - (len(left.composition) - 1) + n_named_dims = len(left.identifiers) - 1 + + if (pattern_ndim := n_anon_dims + n_named_dims) > tensor_ndim: + raise ValueError( + f"Number of dimensions in pattern ({pattern_ndim}) must be less than or equal to the number of " + f"dimensions in the tensor ({tensor_ndim})" + ) + else: + n_ellipsis_dims = 0 + n_named_dims = len(left.identifiers) + + if (pattern_ndim := len(left.composition)) != tensor_ndim: + raise ValueError( + f"Number of dimensions in pattern ({pattern_ndim}) must be equal to the number of dimensions in " + f"the tensor ({tensor_ndim})" + ) + n_dims = n_named_dims + n_ellipsis_dims + n_anon_dims + + if n_dims == 0: + # an identity rearrangement on a 0-dimension tensor + return lambda tensor: tensor + + first_class_dims: tuple[str, ...] = tuple(f"d{i}" for i in range(n_dims)) + identifier_dim_map: dict[Union[str, AnonymousAxis], tuple[str, ...]] = {} + anon_axes: list[AnonymousAxis] = [] + + # map the left-hand side identifiers to strings representing first class dims + dims_i = 0 + for dimension in left.composition: + if isinstance(dimension, list): + for identifier in dimension: + # non-unitary anon axes are not allowed in rearrange & unitary anon axes are represented as empty lists + assert isinstance(identifier, str) + identifier_dim_map[identifier] = (first_class_dims[dims_i],) + dims_i += 1 + if not dimension: + # unitary anonymous axis + anon_axis = AnonymousAxis("1") + identifier_dim_map[anon_axis] = (first_class_dims[dims_i],) + anon_axes.append(anon_axis) + dimension.append(anon_axis) + dims_i += 1 + elif dimension == _ellipsis: + identifier = _ellipsis + identifier_dim_map[identifier] = tuple( + first_class_dims[dims_i + j] for j in range(n_ellipsis_dims) + ) + dims_i += n_ellipsis_dims + else: + raise ValueError(f"Unexpected dimension: {dimension}") + + def composition_to_dims( + composition: Sequence[Union[list[Union[str, AnonymousAxis]], str]], + ) -> list[Union[str, tuple[str, ...]]]: + """Convert a `ParsedExpression.composition` into a `Tensor.__getitem__` index of strings representing first + class dims.""" + dim_composition: list[Union[str, tuple[str, ...]]] = [] + for dimension in composition: + if isinstance(dimension, list): + dim_composition.append( + tuple( + dim + for identifier in dimension + for dim in identifier_dim_map[identifier] + ) + ) + elif dimension == _ellipsis: + dim_composition.extend(identifier_dim_map[_ellipsis]) + else: + raise ValueError(f"Unexpected dimension: {dimension}") + return dim_composition + + left_dims = composition_to_dims(left.composition) + right_dims = composition_to_dims(right.composition) + anon_dims = tuple(identifier_dim_map[axis][0] for axis in anon_axes) + specified_lengths = tuple( + (identifier_dim_map[axis][0], length) for axis, length in axes_lengths.items() + ) + + custom_rearrange_callable_name = "do_rearrange" + custom_rearrange_callable_code = ( + ( + f"def {custom_rearrange_callable_name}(tensor):\n" + f" {comma_separate(first_class_dims)} = dims({n_dims})\n" + ) + + ( + "".join( + f" {dim}.size = {length}\n" for (dim, length) in specified_lengths + ) + if specified_lengths + else "" + ) + + f" tensor = tensor[{comma_separate(left_dims)}].order({comma_separate(right_dims)})\n" + + ( + f" return tensor.sum({comma_separate([anon_dims])}, keepdim=False)\n" + if anon_dims + else " return tensor\n" + ) + ) + + exec(custom_rearrange_callable_code) + return locals()[custom_rearrange_callable_name] + + +def rearrange( + tensor: Union[torch.Tensor, list[torch.Tensor], tuple[torch.Tensor, ...]], + pattern: str, + **axes_lengths: int, +) -> torch.Tensor: + r"""A native implementation of `einops.rearrange`, a reader-friendly smart element reordering for multidimensional + tensors. This operation includes functionality of transpose (axes permutation), reshape (view), squeeze, unsqueeze, + stack, concatenate and other operations. + + See: https://einops.rocks/api/rearrange/ + + Args: + tensor (Tensor or sequence of Tensor): the tensor(s) to rearrange + pattern (str): the rearrangement pattern + axes_lengths (int): any additional length specifications for dimensions + + Returns: + Tensor: the rearranged tensor + + Examples: + >>> # suppose we have a set of 32 images in "h w c" format (height-width-channel) + >>> images = torch.randn((32, 30, 40, 3)) + + >>> # stack along first (batch) axis, output is a single array + >>> rearrange(images, "b h w c -> b h w c").shape + torch.Size([32, 30, 40, 3]) + + >>> # concatenate images along height (vertical axis), 960 = 32 * 30 + >>> rearrange(images, "b h w c -> (b h) w c").shape + torch.Size([960, 40, 3]) + + >>> # concatenated images along horizontal axis, 1280 = 32 * 40 + >>> rearrange(images, "b h w c -> h (b w) c").shape + torch.Size([30, 1280, 3]) + + >>> # reordered axes to "b c h w" format for deep learning + >>> rearrange(images, "b h w c -> b c h w").shape + torch.Size([32, 3, 30, 40]) + + >>> # flattened each image into a vector, 3600 = 30 * 40 * 3 + >>> rearrange(images, "b h w c -> b (c h w)").shape + torch.Size([32, 3600]) + + >>> # split each image into 4 smaller (top-left, top-right, bottom-left, bottom-right), 128 = 32 * 2 * 2 + >>> rearrange(images, "b (h1 h) (w1 w) c -> (b h1 w1) h w c", h1=2, w1=2).shape + torch.Size([128, 15, 20, 3]) + + >>> # space-to-depth operation + >>> rearrange(images, "b (h h1) (w w1) c -> b h w (c h1 w1)", h1=2, w1=2).shape + torch.Size([32, 15, 20, 12]) + """ + if not isinstance(tensor, torch.Tensor): + tensor = torch.stack(tensor) + + rearrange_callable = _create_rearrange_callable( + tensor.ndim, pattern, **axes_lengths + ) + + return rearrange_callable(tensor) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0500fc2c29d35fc0edd0c106337fdc4d6845f2e4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__init__.py @@ -0,0 +1,5 @@ +# PyTorch forward-mode is not mature yet +from torch._functorch.apis import chunk_vmap +from torch._functorch.batch_norm_replacement import replace_all_batch_norm_modules_ +from torch._functorch.eager_transforms import hessian, jacfwd, jvp +from torch.func import functionalize diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8219c3b05656a25f99ef0d29b52b870698e045e1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/control_flow.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/control_flow.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7da47eb1bc749a98c7c510110eb250c95ecb701a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/control_flow.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/ops.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/ops.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82a4c94281c9db6fc072bd7d75f5f614f008fbee Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/__pycache__/ops.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/control_flow.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/control_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..c2b4d52271e7f8c5851b5ef961ea7e90d8a01408 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/control_flow.py @@ -0,0 +1,6 @@ +from torch import cond # noqa: F401 +from torch._higher_order_ops.map import ( # noqa: F401 + _stack_pytree, + _unstack_pytree, + map, +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/ops.py b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/ops.py new file mode 100644 index 0000000000000000000000000000000000000000..7a502ef2b002cd824e7b67d08fccac872b313110 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/functorch/experimental/ops.py @@ -0,0 +1 @@ +from torch._ops import HigherOrderOperator # noqa: F401 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/licenses/LICENSE b/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/licenses/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/sboms/hf_xet.cyclonedx.json b/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/sboms/hf_xet.cyclonedx.json new file mode 100644 index 0000000000000000000000000000000000000000..0d428a16fade54a583c7ee17489a129e7828e423 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/hf_xet-1.3.2.dist-info/sboms/hf_xet.cyclonedx.json @@ -0,0 +1,12019 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "serialNumber": "urn:uuid:8142c7e6-0d78-4fdc-8071-fe9a0e46fe4a", + "metadata": { + "timestamp": "2026-02-27T17:17:38.240074353Z", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cargo-cyclonedx", + "version": "0.5.7" + } + ], + "component": { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/hf_xet#1.3.2", + "name": "hf_xet", + "version": "1.3.2", + "scope": "required", + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/hf_xet@1.3.2?download_url=file://.", + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/hf_xet#1.3.2 bin-target-0", + "name": "hf_xet", + "version": "1.3.2", + "purl": "pkg:cargo/hf_xet@1.3.2?download_url=file://.#src/lib.rs" + } + ] + }, + "properties": [ + { + "name": "cdx:rustc:sbom:target:all_targets", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "name": "cas_client", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/cas_client@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/cas_client" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/cas_object#0.1.0", + "name": "cas_object", + "version": "0.1.0", + "scope": "required", + "purl": "pkg:cargo/cas_object@0.1.0?download_url=file:///home/runner/work/xet-core/xet-core/cas_object" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/cas_types#0.1.0", + "name": "cas_types", + "version": "0.1.0", + "scope": "required", + "purl": "pkg:cargo/cas_types@0.1.0?download_url=file:///home/runner/work/xet-core/xet-core/cas_types" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/data#0.14.5", + "name": "data", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/data@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/data" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/deduplication#0.14.5", + "name": "deduplication", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/deduplication@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/deduplication" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "name": "error_printer", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/error_printer@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/error_printer" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/file_reconstruction#0.14.5", + "name": "file_reconstruction", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/file_reconstruction@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/file_reconstruction" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/file_utils#0.14.2", + "name": "file_utils", + "version": "0.14.2", + "scope": "required", + "purl": "pkg:cargo/file_utils@0.14.2?download_url=file:///home/runner/work/xet-core/xet-core/file_utils" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/hub_client#0.1.0", + "name": "hub_client", + "version": "0.1.0", + "scope": "required", + "purl": "pkg:cargo/hub_client@0.1.0?download_url=file:///home/runner/work/xet-core/xet-core/hub_client" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "name": "mdb_shard", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/mdb_shard@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/mdb_shard" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "name": "merklehash", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/merklehash@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/merklehash" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "name": "progress_tracking", + "version": "0.1.0", + "scope": "required", + "purl": "pkg:cargo/progress_tracking@0.1.0?download_url=file:///home/runner/work/xet-core/xet-core/progress_tracking" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "name": "utils", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/utils@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/utils" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/xet_config#0.14.5", + "name": "xet_config", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/xet_config@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/xet_config" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/xet_logging#0.14.5", + "name": "xet_logging", + "version": "0.14.5", + "scope": "required", + "purl": "pkg:cargo/xet_logging@0.14.5?download_url=file:///home/runner/work/xet-core/xet-core/xet_logging" + }, + { + "type": "library", + "bom-ref": "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0", + "name": "xet_runtime", + "version": "0.1.0", + "scope": "required", + "purl": "pkg:cargo/xet_runtime@0.1.0?download_url=file:///home/runner/work/xet-core/xet-core/xet_runtime" + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "author": "Andrew Gallant ", + "name": "aho-corasick", + "version": "1.1.4", + "description": "Fast multiple substring searching.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/aho-corasick@1.1.4", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/BurntSushi/aho-corasick" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/aho-corasick" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "name": "anstream", + "version": "0.6.21", + "description": "IO stream adapters for writing colored text that will gracefully degrade according to your terminal's capabilities.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstream@0.6.21", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "name": "anstyle-parse", + "version": "0.2.7", + "description": "Parse ANSI Style Escapes", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle-parse@0.2.7", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "name": "anstyle-query", + "version": "1.1.5", + "description": "Look up colored console capabilities", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle-query@1.1.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "name": "anstyle", + "version": "1.0.13", + "description": "ANSI text styling", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anstyle@1.0.13", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "author": "David Tolnay ", + "name": "anyhow", + "version": "1.0.101", + "description": "Flexible concrete Error type built on std::error::Error", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/anyhow@1.0.101", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/anyhow" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/anyhow" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#approx@0.5.1", + "author": "Brendan Zabarauskas ", + "name": "approx", + "version": "0.5.1", + "description": "Approximate floating point equality comparisons and assertions.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/approx@0.5.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/approx" + }, + { + "type": "website", + "url": "https://github.com/brendanzab/approx" + }, + { + "type": "vcs", + "url": "https://github.com/brendanzab/approx" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "author": "David Roundy ", + "name": "arrayref", + "version": "0.3.9", + "description": "Macros to take array references of slices", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause" + } + ], + "purl": "pkg:cargo/arrayref@0.3.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/arrayref" + }, + { + "type": "vcs", + "url": "https://github.com/droundy/arrayref" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "author": "bluss", + "name": "arrayvec", + "version": "0.7.6", + "description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/arrayvec@0.7.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/arrayvec/" + }, + { + "type": "vcs", + "url": "https://github.com/bluss/arrayvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "author": "David Tolnay ", + "name": "async-trait", + "version": "0.1.89", + "description": "Type erasure for async trait methods", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/async-trait@0.1.89", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/async-trait" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/async-trait" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2", + "author": "Stjepan Glavina , Contributors to futures-rs", + "name": "atomic-waker", + "version": "1.1.2", + "description": "A synchronization primitive for task wakeup", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/atomic-waker@1.1.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/smol-rs/atomic-waker" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "author": "Josh Stone ", + "name": "autocfg", + "version": "1.5.0", + "description": "Automatic cfg for Rust compiler features", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/autocfg@1.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/autocfg/" + }, + { + "type": "vcs", + "url": "https://github.com/cuviper/autocfg" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aws-lc-rs@1.15.4", + "author": "AWS-LibCrypto", + "name": "aws-lc-rs", + "version": "1.15.4", + "description": "aws-lc-rs is a cryptographic library using AWS-LC for its cryptographic operations. This library strives to be API-compatible with the popular Rust library named ring.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" + } + ], + "licenses": [ + { + "expression": "ISC AND (Apache-2.0 OR ISC)" + } + ], + "purl": "pkg:cargo/aws-lc-rs@1.15.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crate/aws-lc-rs" + }, + { + "type": "website", + "url": "https://github.com/aws/aws-lc-rs" + }, + { + "type": "other", + "url": "aws_lc_rs_1_15_4_sys" + }, + { + "type": "vcs", + "url": "https://github.com/aws/aws-lc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aws-lc-sys@0.37.0", + "author": "AWS-LC", + "name": "aws-lc-sys", + "version": "0.37.0", + "description": "AWS-LC is a general-purpose cryptographic library maintained by the AWS Cryptography team for AWS and their customers. It іs based on code from the Google BoringSSL project and the OpenSSL project.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" + } + ], + "licenses": [ + { + "expression": "ISC AND (Apache-2.0 OR ISC) AND OpenSSL" + } + ], + "purl": "pkg:cargo/aws-lc-sys@0.37.0", + "externalReferences": [ + { + "type": "other", + "url": "aws_lc_0_37_0" + }, + { + "type": "vcs", + "url": "https://github.com/aws/aws-lc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#axum-core@0.5.6", + "name": "axum-core", + "version": "0.5.6", + "description": "Core types and traits for axum", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/axum-core@0.5.6", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/tokio-rs/axum" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/axum" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#axum@0.8.8", + "name": "axum", + "version": "0.8.8", + "description": "Web framework that focuses on ergonomics and modularity", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/axum@0.8.8", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/tokio-rs/axum" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/axum" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "author": "Marshall Pierce ", + "name": "base64", + "version": "0.22.1", + "description": "encodes and decodes base64 as bytes or utf8", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/base64@0.22.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/base64" + }, + { + "type": "vcs", + "url": "https://github.com/marshallpierce/rust-base64" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bincode@1.3.3", + "author": "Ty Overby , Francesco Mazzoli , David Tolnay , Zoey Riordan ", + "name": "bincode", + "version": "1.3.3", + "description": "A binary serialization / deserialization strategy that uses Serde for transforming structs into bytes and vice versa!", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/bincode@1.3.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bincode" + }, + { + "type": "vcs", + "url": "https://github.com/servo/bincode" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0", + "author": "The Rust Project Developers", + "name": "bitflags", + "version": "2.10.0", + "description": "A macro to generate structures which behave like bitflags. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bitflags@2.10.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bitflags" + }, + { + "type": "website", + "url": "https://github.com/bitflags/bitflags" + }, + { + "type": "vcs", + "url": "https://github.com/bitflags/bitflags" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "author": "Jack O'Connor , Samuel Neves", + "name": "blake3", + "version": "1.8.3", + "description": "the BLAKE3 hash function", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" + } + ], + "licenses": [ + { + "expression": "CC0-1.0 OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/blake3@1.8.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/blake3" + }, + { + "type": "vcs", + "url": "https://github.com/BLAKE3-team/BLAKE3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4", + "author": "RustCrypto Developers", + "name": "block-buffer", + "version": "0.10.4", + "description": "Buffer type for block processing of data", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/block-buffer@0.10.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/block-buffer" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bstr@1.12.1", + "author": "Andrew Gallant ", + "name": "bstr", + "version": "1.12.1", + "description": "A string type that is not required to be valid UTF-8.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/bstr@1.12.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/bstr" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/bstr" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/bstr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.25.0", + "author": "Lokathor ", + "name": "bytemuck", + "version": "1.25.0", + "description": "A crate for mucking around with piles of bytes.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + } + ], + "licenses": [ + { + "expression": "Zlib OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/bytemuck@1.25.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Lokathor/bytemuck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0", + "author": "Andrew Gallant ", + "name": "byteorder", + "version": "1.5.0", + "description": "Library for reading/writing numbers in big-endian and little-endian.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/byteorder@1.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/byteorder" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/byteorder" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/byteorder" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "author": "Carl Lerche , Sean McArthur ", + "name": "bytes", + "version": "1.11.1", + "description": "Types and traits for working with bytes", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/bytes@1.11.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/tokio-rs/bytes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "author": "Alex Crichton ", + "name": "cc", + "version": "1.2.55", + "description": "A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code. ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cc@1.2.55", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cc" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/cc-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@0.1.10", + "author": "Alex Crichton ", + "name": "cfg-if", + "version": "0.1.10", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@0.1.10", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cfg-if" + }, + { + "type": "website", + "url": "https://github.com/alexcrichton/cfg-if" + }, + { + "type": "vcs", + "url": "https://github.com/alexcrichton/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "author": "Alex Crichton ", + "name": "cfg-if", + "version": "1.0.4", + "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cfg-if@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/cfg-if" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1", + "author": "Zicklag ", + "name": "cfg_aliases", + "version": "0.2.1", + "description": "A tiny utility to help save you a lot of effort with long winded `#[cfg()]` checks.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/cfg_aliases@0.2.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cfg_aliases" + }, + { + "type": "website", + "url": "https://github.com/katharostech/cfg_aliases" + }, + { + "type": "vcs", + "url": "https://github.com/katharostech/cfg_aliases" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "name": "chrono", + "version": "0.4.43", + "description": "Date and time library for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/chrono@0.4.43", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/chrono/" + }, + { + "type": "website", + "url": "https://github.com/chronotope/chrono" + }, + { + "type": "vcs", + "url": "https://github.com/chronotope/chrono" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "name": "clap", + "version": "4.5.57", + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap@4.5.57", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.57", + "name": "clap_builder", + "version": "4.5.57", + "description": "A simple to use, efficient, and full-featured Command Line Argument Parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_builder@4.5.57", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55", + "name": "clap_derive", + "version": "4.5.55", + "description": "Parse command line argument by defining a struct, derive crate.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_derive@4.5.55", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.7", + "name": "clap_lex", + "version": "0.7.7", + "description": "Minimal, flexible command line parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/clap_lex@0.7.7", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/clap-rs/clap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cmake@0.1.57", + "author": "Alex Crichton ", + "name": "cmake", + "version": "0.1.57", + "description": "A build dependency for running `cmake` to build a native library ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cmake@0.1.57", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cmake" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/cmake-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cmake-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "name": "colorchoice", + "version": "1.0.4", + "description": "Global override of color control", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/colorchoice@1.0.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-cli/anstyle.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#colored@3.1.1", + "author": "Thomas Wickham ", + "name": "colored", + "version": "3.1.1", + "description": "The most simple way to add colors in your terminal", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" + } + ], + "licenses": [ + { + "expression": "MPL-2.0" + } + ], + "purl": "pkg:cargo/colored@3.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/mackwic/colored" + }, + { + "type": "vcs", + "url": "https://github.com/mackwic/colored" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#const-str@1.1.0", + "author": "Nugine ", + "name": "const-str", + "version": "1.1.0", + "description": "compile-time string operations", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/const-str@1.1.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Nugine/const-str" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#const_panic@0.2.15", + "author": "rodrimati1992 ", + "name": "const_panic", + "version": "0.2.15", + "description": "const panic with formatting", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" + } + ], + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:cargo/const_panic@0.2.15", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rodrimati1992/const_panic/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "author": "Cesar Eduardo Barros ", + "name": "constant_time_eq", + "version": "0.4.2", + "description": "Compares two equal-sized byte strings in constant time.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + } + ], + "licenses": [ + { + "expression": "CC0-1.0 OR MIT-0 OR Apache-2.0" + } + ], + "purl": "pkg:cargo/constant_time_eq@0.4.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/constant_time_eq" + }, + { + "type": "vcs", + "url": "https://github.com/cesarb/constant_time_eq" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#countio@0.3.0", + "author": "Oleh Martsokha ", + "name": "countio", + "version": "0.3.0", + "description": "Byte counting for std::io::{Read, Write, Seek} and its async variants from futures and tokio. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b9702aee5d1d744c01d82f6915644f950f898e014903385464c773b96fefdecb" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/countio@0.3.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/countio" + }, + { + "type": "website", + "url": "https://github.com/spire-rs/countio" + }, + { + "type": "vcs", + "url": "https://github.com/spire-rs/countio" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "author": "RustCrypto Developers", + "name": "cpufeatures", + "version": "0.2.17", + "description": "Lightweight runtime CPU feature detection for aarch64, loongarch64, and x86/x86_64 targets, with no_std support and support for mobile targets including Android and iOS ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/cpufeatures@0.2.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/cpufeatures" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-channel@0.5.15", + "name": "crossbeam-channel", + "version": "0.5.15", + "description": "Multi-producer multi-consumer channels for message passing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/crossbeam-channel@0.5.15", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-channel" + }, + { + "type": "vcs", + "url": "https://github.com/crossbeam-rs/crossbeam" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.12", + "name": "crossbeam-queue", + "version": "0.3.12", + "description": "Concurrent queues", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/crossbeam-queue@0.3.12", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-queue" + }, + { + "type": "vcs", + "url": "https://github.com/crossbeam-rs/crossbeam" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21", + "name": "crossbeam-utils", + "version": "0.8.21", + "description": "Utilities for concurrent programming", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/crossbeam-utils@0.8.21", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-utils" + }, + { + "type": "vcs", + "url": "https://github.com/crossbeam-rs/crossbeam" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7", + "author": "RustCrypto Developers", + "name": "crypto-common", + "version": "0.1.7", + "description": "Common cryptographic traits", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/crypto-common@0.1.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crypto-common" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/traits" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#csv-core@0.1.13", + "author": "Andrew Gallant ", + "name": "csv-core", + "version": "0.1.13", + "description": "Bare bones CSV parsing with no_std support.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/csv-core@0.1.13", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/csv-core" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/rust-csv" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/rust-csv" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#csv@1.4.0", + "author": "Andrew Gallant ", + "name": "csv", + "version": "1.4.0", + "description": "Fast CSV parsing with support for serde.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/csv@1.4.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/csv" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/rust-csv" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/rust-csv" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ctor-proc-macro@0.0.7", + "author": "Matt Mastracci ", + "name": "ctor-proc-macro", + "version": "0.0.7", + "description": "proc-macro support for the ctor crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/ctor-proc-macro@0.0.7", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mmastrac/rust-ctor" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ctor@0.6.3", + "author": "Matt Mastracci ", + "name": "ctor", + "version": "0.6.3", + "description": "__attribute__((constructor)) for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/ctor@0.6.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mmastrac/rust-ctor" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.5", + "author": "Jacob Pratt ", + "name": "deranged", + "version": "0.5.5", + "description": "Ranged integers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/deranged@0.5.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/jhpratt/deranged" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#derivative@2.2.0", + "author": "mcarton ", + "name": "derivative", + "version": "2.2.0", + "description": "A set of alternative `derive` attributes for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/derivative@2.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://mcarton.github.io/rust-derivative/" + }, + { + "type": "vcs", + "url": "https://github.com/mcarton/rust-derivative" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7", + "author": "RustCrypto Developers", + "name": "digest", + "version": "0.10.7", + "description": "Traits for cryptographic hash functions and message authentication codes", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/digest@0.10.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/digest" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/traits" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.5.0", + "author": "Simon Ochsenreither ", + "name": "dirs-sys", + "version": "0.5.0", + "description": "System-level helper functions for the dirs and directories crates.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/dirs-sys@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dirs-dev/dirs-sys-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0", + "author": "Simon Ochsenreither ", + "name": "dirs", + "version": "6.0.0", + "description": "A tiny low-level library that provides platform-specific standard locations of directories for config, cache and other data on Linux, Windows, macOS and Redox by leveraging the mechanisms defined by the XDG base/user directory specifications on Linux, the Known Folder API on Windows, and the Standard Directory guidelines on macOS.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/dirs@6.0.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/soc/dirs-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "author": "Jane Lusby ", + "name": "displaydoc", + "version": "0.2.5", + "description": "A derive macro for implementing the display Trait via a doc comment and string interpolation ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/displaydoc@0.2.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/displaydoc" + }, + { + "type": "website", + "url": "https://github.com/yaahc/displaydoc" + }, + { + "type": "vcs", + "url": "https://github.com/yaahc/displaydoc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#doxygen-rs@0.4.2", + "name": "doxygen-rs", + "version": "0.4.2", + "description": "Transform Doxygen to Rustdoc", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" + } + ], + "licenses": [ + { + "expression": "BSD-3-Clause" + } + ], + "purl": "pkg:cargo/doxygen-rs@0.4.2", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/Techie-Pi/doxygen-rs/" + }, + { + "type": "vcs", + "url": "https://github.com/Techie-Pi/doxygen-rs/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#dtor-proc-macro@0.0.6", + "author": "Matt Mastracci ", + "name": "dtor-proc-macro", + "version": "0.0.6", + "description": "proc-macro support for the dtor crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/dtor-proc-macro@0.0.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mmastrac/rust-ctor" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#dtor@0.1.1", + "author": "Matt Mastracci ", + "name": "dtor", + "version": "0.1.1", + "description": "__attribute__((destructor)) for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/dtor@0.1.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/mmastrac/rust-ctor" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5", + "author": "Kornel ", + "name": "dunce", + "version": "1.0.5", + "description": "Normalize Windows paths to the most compatible format, avoiding UNC where possible", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + } + ], + "licenses": [ + { + "expression": "CC0-1.0 OR MIT-0 OR Apache-2.0" + } + ], + "purl": "pkg:cargo/dunce@1.0.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/dunce" + }, + { + "type": "website", + "url": "https://lib.rs/crates/dunce" + }, + { + "type": "vcs", + "url": "https://gitlab.com/kornelski/dunce" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#duration-str@0.19.0", + "author": "baoyachi ", + "name": "duration-str", + "version": "0.19.0", + "description": "duration string parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "12494809f9915b6132014cc259c4e204ab53ab6c6dd2225672703b5359267d82" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/duration-str@0.19.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/baoyachi/duration-str" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#either@1.15.0", + "author": "bluss", + "name": "either", + "version": "1.15.0", + "description": "The enum `Either` with variants `Left` and `Right` is a general purpose sum type with two cases. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/either@1.15.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/either/1/" + }, + { + "type": "vcs", + "url": "https://github.com/rayon-rs/either" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "name": "equivalent", + "version": "1.0.2", + "description": "Traits for key comparison in maps.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/equivalent@1.0.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/indexmap-rs/equivalent" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "author": "Chris Wong , Dan Gohman ", + "name": "errno", + "version": "0.3.14", + "description": "Cross-platform interface to the `errno` variable.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/errno@0.3.14", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/errno" + }, + { + "type": "vcs", + "url": "https://github.com/lambda-fairy/rust-errno" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "author": "Stjepan Glavina ", + "name": "fastrand", + "version": "2.3.0", + "description": "A simple and fast random number generator", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/fastrand@2.3.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/smol-rs/fastrand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "name": "find-msvc-tools", + "version": "0.1.9", + "description": "Find windows-specific tools, read MSVC versions from the registry and from COM interfaces", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/find-msvc-tools@0.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/find-msvc-tools" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/cc-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7", + "author": "Alex Crichton ", + "name": "fnv", + "version": "1.0.7", + "description": "Fowler–Noll–Vo hash function", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/fnv@1.0.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://doc.servo.org/fnv/" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-fnv" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "author": "The rust-url developers", + "name": "form_urlencoded", + "version": "1.2.2", + "description": "Parser and serializer for the application/x-www-form-urlencoded syntax, as used by HTML forms.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/form_urlencoded@1.2.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#fs_extra@1.3.0", + "author": "Denis Kurilenko ", + "name": "fs_extra", + "version": "1.3.0", + "description": "Expanding std::fs and std::io. Recursively copy folders with information about process and much more.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/fs_extra@1.3.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/fs_extra" + }, + { + "type": "website", + "url": "https://github.com/webdesus/fs_extra" + }, + { + "type": "vcs", + "url": "https://github.com/webdesus/fs_extra" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "name": "futures-channel", + "version": "0.3.31", + "description": "Channels for asynchronous communication using futures-rs. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-channel@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "name": "futures-core", + "version": "0.3.31", + "description": "The core traits and types in for the `futures` library. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-core@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31", + "name": "futures-executor", + "version": "0.3.31", + "description": "Executors for asynchronous tasks based on the futures-rs library. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-executor@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31", + "name": "futures-io", + "version": "0.3.31", + "description": "The `AsyncRead`, `AsyncWrite`, `AsyncSeek`, and `AsyncBufRead` traits for the futures-rs library. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-io@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31", + "name": "futures-macro", + "version": "0.3.31", + "description": "The futures-rs procedural macro implementations. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-macro@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "name": "futures-sink", + "version": "0.3.31", + "description": "The asynchronous `Sink` trait for the futures-rs library. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-sink@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31", + "name": "futures-task", + "version": "0.3.31", + "description": "Tools for working with tasks. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-task@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "name": "futures-util", + "version": "0.3.31", + "description": "Common utilities and extension traits for the futures-rs library. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures-util@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "name": "futures", + "version": "0.3.31", + "description": "An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/futures@0.3.31", + "externalReferences": [ + { + "type": "website", + "url": "https://rust-lang.github.io/futures-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/futures-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#gearhash@0.1.3", + "author": "Sam Rijs ", + "name": "gearhash", + "version": "0.1.3", + "description": "Fast, SIMD-accelerated hash function for content-defined chunking", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c8cf82cf76cd16485e56295a1377c775ce708c9f1a0be6b029076d60a245d213" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/gearhash@0.1.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/srijs/rust-gearhash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7", + "author": "Bartłomiej Kamiński , Aaron Trent ", + "name": "generic-array", + "version": "0.14.7", + "description": "Generic types implementing functionality of arrays", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/generic-array@0.14.7", + "externalReferences": [ + { + "type": "documentation", + "url": "http://fizyk20.github.io/generic-array/generic_array/" + }, + { + "type": "vcs", + "url": "https://github.com/fizyk20/generic-array.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "author": "The Rand Project Developers", + "name": "getrandom", + "version": "0.2.17", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.2.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4", + "author": "The Rand Project Developers", + "name": "getrandom", + "version": "0.3.4", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.3.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "author": "The Rand Project Developers", + "name": "getrandom", + "version": "0.4.1", + "description": "A small cross-platform library for retrieving random data from system source", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/getrandom@0.4.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/getrandom" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/getrandom" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#git-version-macro@0.3.9", + "author": "David Roundy , Maarten de Vries , Mara Bos ", + "name": "git-version-macro", + "version": "0.3.9", + "description": "Internal macro crate for git-version.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause" + } + ], + "purl": "pkg:cargo/git-version-macro@0.3.9", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/fusion-engineering/rust-git-version" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#git-version@0.3.9", + "author": "Mara Bos , Maarten de Vries , David Roundy ", + "name": "git-version", + "version": "0.3.9", + "description": "Compile the git version (tag name, or hash otherwise) and dirty state into your program.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause" + } + ], + "purl": "pkg:cargo/git-version@0.3.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/git-version/" + }, + { + "type": "vcs", + "url": "https://github.com/fusion-engineering/rust-git-version" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#h2@0.4.13", + "author": "Carl Lerche , Sean McArthur ", + "name": "h2", + "version": "0.4.13", + "description": "An HTTP/2 client and server", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/h2@0.4.13", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/h2" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/h2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#half@2.7.1", + "author": "Kathryn Long ", + "name": "half", + "version": "2.7.1", + "description": "Half-precision floating point f16 and bf16 types for Rust implementing the IEEE 754-2008 standard binary16 and bfloat16 types.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/half@2.7.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/VoidStarKat/half-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "author": "Amanieu d'Antras ", + "name": "hashbrown", + "version": "0.16.1", + "description": "A Rust port of Google's SwissTable hash map", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/hashbrown@0.16.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/hashbrown" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#headers-core@0.3.0", + "author": "Sean McArthur ", + "name": "headers-core", + "version": "0.3.0", + "description": "typed HTTP headers core trait", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/headers-core@0.3.0", + "externalReferences": [ + { + "type": "website", + "url": "https://hyper.rs" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/headers" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#headers@0.4.1", + "author": "Sean McArthur ", + "name": "headers", + "version": "0.4.1", + "description": "typed HTTP headers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/headers@0.4.1", + "externalReferences": [ + { + "type": "website", + "url": "https://hyper.rs" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/headers" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heapify@0.2.0", + "name": "heapify", + "version": "0.2.0", + "description": "Convenience functions to turn slices into max-heaps.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heapify@0.2.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/ethereal-sheep/heapify" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "name": "heck", + "version": "0.5.0", + "description": "heck is a case conversion library.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/heck@0.5.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/withoutboats/heck" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heed-traits@0.20.0", + "author": "Kerollmops ", + "name": "heed-traits", + "version": "0.20.0", + "description": "The traits used inside of the fully typed LMDB wrapper, heed", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/heed-traits@0.20.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Kerollmops/heed" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heed-types@0.21.0", + "author": "Kerollmops ", + "name": "heed-types", + "version": "0.21.0", + "description": "The types used with the fully typed LMDB wrapper, heed", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/heed-types@0.21.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Kerollmops/heed" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heed@0.22.0", + "author": "Kerollmops ", + "name": "heed", + "version": "0.22.0", + "description": "A fully typed LMDB (mdb.master) wrapper with minimum overhead", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6a56c94661ddfb51aa9cdfbf102cfcc340aa69267f95ebccc4af08d7c530d393" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/heed@0.22.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Kerollmops/heed" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "author": "Carl Lerche , Lucio Franco , Sean McArthur ", + "name": "http-body-util", + "version": "0.1.3", + "description": "Combinators and adapters for HTTP request or response bodies. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/http-body-util@0.1.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/http-body-util" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/http-body" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "author": "Carl Lerche , Lucio Franco , Sean McArthur ", + "name": "http-body", + "version": "1.0.1", + "description": "Trait representing an asynchronous, streaming, HTTP request or response body. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/http-body@1.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/http-body" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/http-body" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "author": "Alex Crichton , Carl Lerche , Sean McArthur ", + "name": "http", + "version": "1.4.0", + "description": "A set of types for representing HTTP requests and responses. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/http@1.4.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/http" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/http" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "author": "Sean McArthur ", + "name": "httparse", + "version": "1.10.1", + "description": "A tiny, safe, speedy, zero-copy HTTP/1.x parser.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/httparse@1.10.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/httparse" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/httparse" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "author": "Pyfisch ", + "name": "httpdate", + "version": "1.0.3", + "description": "HTTP date parsing and formatting", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/httpdate@1.0.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/pyfisch/httpdate" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hyper-rustls@0.27.7", + "name": "hyper-rustls", + "version": "0.27.7", + "description": "Rustls+hyper integration for pure rust HTTPS", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR ISC OR MIT" + } + ], + "purl": "pkg:cargo/hyper-rustls@0.27.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/hyper-rustls/" + }, + { + "type": "website", + "url": "https://github.com/rustls/hyper-rustls" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/hyper-rustls" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "author": "Sean McArthur ", + "name": "hyper-util", + "version": "0.1.20", + "description": "hyper utilities", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/hyper-util@0.1.20", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/hyper-util" + }, + { + "type": "website", + "url": "https://hyper.rs" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/hyper-util" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "author": "Sean McArthur ", + "name": "hyper", + "version": "1.8.1", + "description": "A protective and efficient HTTP library for all.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/hyper@1.8.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/hyper" + }, + { + "type": "website", + "url": "https://hyper.rs" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/hyper" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "author": "Andrew Straw , René Kijewski , Ryan Lopopolo ", + "name": "iana-time-zone", + "version": "0.1.65", + "description": "get the IANA time zone for the current system", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/iana-time-zone@0.1.65", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/strawlab/iana-time-zone" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.1.1", + "author": "The ICU4X Project Developers", + "name": "icu_collections", + "version": "2.1.1", + "description": "Collection of API for use in ICU libraries.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_collections@2.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locale_core@2.1.1", + "author": "The ICU4X Project Developers", + "name": "icu_locale_core", + "version": "2.1.1", + "description": "API for managing Unicode Language and Locale Identifiers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_locale_core@2.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1", + "author": "The ICU4X Project Developers", + "name": "icu_normalizer", + "version": "2.1.1", + "description": "API for normalizing text into Unicode Normalization Forms", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_normalizer@2.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1", + "author": "The ICU4X Project Developers", + "name": "icu_normalizer_data", + "version": "2.1.1", + "description": "Data for the icu_normalizer crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_normalizer_data@2.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2", + "author": "The ICU4X Project Developers", + "name": "icu_properties", + "version": "2.1.2", + "description": "Definitions for Unicode properties", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_properties@2.1.2", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2", + "author": "The ICU4X Project Developers", + "name": "icu_properties_data", + "version": "2.1.2", + "description": "Data for the icu_properties crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_properties_data@2.1.2", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1", + "author": "The ICU4X Project Developers", + "name": "icu_provider", + "version": "2.1.1", + "description": "Trait and struct definitions for the ICU data provider", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/icu_provider@2.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "author": "The rust-url developers", + "name": "idna", + "version": "1.1.0", + "description": "IDNA (Internationalizing Domain Names in Applications) and Punycode.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/idna@1.1.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1", + "author": "The rust-url developers", + "name": "idna_adapter", + "version": "1.2.1", + "description": "Back end adapter for idna", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/idna_adapter@1.2.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/idna_adapter/latest/idna_adapter/" + }, + { + "type": "website", + "url": "https://docs.rs/crate/idna_adapter/latest" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/idna_adapter" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "name": "indexmap", + "version": "2.13.0", + "description": "A hash table with consistent order and fast iteration.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/indexmap@2.13.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/indexmap/" + }, + { + "type": "vcs", + "url": "https://github.com/indexmap-rs/indexmap" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#indoc@2.0.7", + "author": "David Tolnay ", + "name": "indoc", + "version": "2.0.7", + "description": "Indented document literals", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/indoc@2.0.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/indoc" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/indoc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ipnet@2.11.0", + "author": "Kris Price ", + "name": "ipnet", + "version": "2.11.0", + "description": "Provides types and useful methods for working with IPv4 and IPv6 network addresses, commonly called IP prefixes. The new `IpNet`, `Ipv4Net`, and `Ipv6Net` types build on the existing `IpAddr`, `Ipv4Addr`, and `Ipv6Addr` types already provided in Rust's standard library and align to their design to stay consistent. The module also provides useful traits that extend `Ipv4Addr` and `Ipv6Addr` with methods for `Add`, `Sub`, `BitAnd`, and `BitOr` operations. The module only uses stable feature so it is guaranteed to compile using the stable toolchain.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/ipnet@2.11.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/ipnet" + }, + { + "type": "vcs", + "url": "https://github.com/krisprice/ipnet" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#iri-string@0.7.10", + "author": "YOSHIOKA Takuma ", + "name": "iri-string", + "version": "0.7.10", + "description": "IRI as string types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/iri-string@0.7.10", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/lo48576/iri-string" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "name": "is_terminal_polyfill", + "version": "1.70.2", + "description": "Polyfill for `is_terminal` stdlib feature for use with older MSRVs", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/is_terminal_polyfill@1.70.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/polyfill-rs/is_terminal_polyfill" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0", + "author": "bluss", + "name": "itertools", + "version": "0.14.0", + "description": "Extra iterator adaptors, iterator methods, free functions, and macros.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itertools@0.14.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itertools/" + }, + { + "type": "vcs", + "url": "https://github.com/rust-itertools/itertools" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "author": "David Tolnay ", + "name": "itoa", + "version": "1.0.17", + "description": "Fast integer primitive to string conversion", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/itoa@1.0.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/itoa" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/itoa" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#jobserver@0.1.34", + "author": "Alex Crichton ", + "name": "jobserver", + "version": "0.1.34", + "description": "An implementation of the GNU Make jobserver for Rust. ", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/jobserver@0.1.34", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/jobserver" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/jobserver-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/jobserver-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#konst@0.4.3", + "author": "rodrimati1992 ", + "name": "konst", + "version": "0.4.3", + "description": "Const equivalents of std features: comparison, destructuring, iteration, and parsing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" + } + ], + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:cargo/konst@0.4.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/konst/" + }, + { + "type": "vcs", + "url": "https://github.com/rodrimati1992/konst/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#konst_proc_macros@0.4.1", + "author": "rodrimati1992 ", + "name": "konst_proc_macros", + "version": "0.4.1", + "description": "Implementation detail of the `konst` crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" + } + ], + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:cargo/konst_proc_macros@0.4.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/konst/" + }, + { + "type": "vcs", + "url": "https://github.com/rodrimati1992/konst/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "author": "Marvin Löbel ", + "name": "lazy_static", + "version": "1.5.0", + "description": "A macro for declaring lazily evaluated statics in Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lazy_static@1.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/lazy_static" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang-nursery/lazy-static.rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "author": "The Rust Project Developers", + "name": "libc", + "version": "0.2.181", + "description": "Raw FFI bindings to platform libraries like libc.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/libc@0.2.181", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/libc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libm@0.2.16", + "author": "Alex Crichton , Amanieu d'Antras , Jorge Aparicio , Trevor Gross ", + "name": "libm", + "version": "0.2.16", + "description": "libm in pure Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/libm@0.2.16", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/compiler-builtins" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0", + "author": "Dan Gohman ", + "name": "linux-raw-sys", + "version": "0.11.0", + "description": "Generated bindings for Linux's userspace API", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/linux-raw-sys@0.11.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/linux-raw-sys" + }, + { + "type": "vcs", + "url": "https://github.com/sunfishcode/linux-raw-sys" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.1", + "author": "The ICU4X Project Developers", + "name": "litemap", + "version": "0.8.1", + "description": "A key-value Map implementation based on a flat, sorted Vec.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/litemap@0.8.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/litemap" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lmdb-master-sys@0.2.5", + "author": "Kerollmops , Dan Burkert , Victor Porof ", + "name": "lmdb-master-sys", + "version": "0.2.5", + "description": "Rust bindings for liblmdb on the mdb.master branch.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "864808e0b19fb6dd3b70ba94ee671b82fce17554cf80aeb0a155c65bb08027df" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/lmdb-master-sys@0.2.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/lmdb-master-sys" + }, + { + "type": "vcs", + "url": "https://github.com/meilisearch/heed/tree/main/lmdb-master-sys" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14", + "author": "Amanieu d'Antras ", + "name": "lock_api", + "version": "0.4.14", + "description": "Wrappers to create fully-featured Mutex and RwLock types. Compatible with no_std.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/lock_api@0.4.14", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Amanieu/parking_lot" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "author": "The Rust Project Developers", + "name": "log", + "version": "0.4.29", + "description": "A lightweight logging facade for Rust ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/log@0.4.29", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/log" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/log" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lru-slab@0.1.2", + "author": "Benjamin Saunders ", + "name": "lru-slab", + "version": "0.1.2", + "description": "Pre-allocated storage with constant-time LRU tracking", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0 OR Zlib" + } + ], + "purl": "pkg:cargo/lru-slab@0.1.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Ralith/lru-slab" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#lz4_flex@0.12.0", + "author": "Pascal Seitz , Arthur Silva , ticki ", + "name": "lz4_flex", + "version": "0.12.0", + "description": "Fastest LZ4 implementation in Rust, no unsafe by default.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/lz4_flex@0.12.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pseitz/lz4_flex" + }, + { + "type": "vcs", + "url": "https://github.com/pseitz/lz4_flex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "author": "Eliza Weisman ", + "name": "matchers", + "version": "0.2.0", + "description": "Regex matching on character and byte streams. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/matchers@0.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/matchers/" + }, + { + "type": "website", + "url": "https://github.com/hawkw/matchers" + }, + { + "type": "vcs", + "url": "https://github.com/hawkw/matchers" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#matchit@0.8.4", + "author": "Ibraheem Ahmed ", + "name": "matchit", + "version": "0.8.4", + "description": "A high performance, zero-copy URL router.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + } + ], + "licenses": [ + { + "expression": "MIT AND BSD-3-Clause" + } + ], + "purl": "pkg:cargo/matchit@0.8.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/ibraheemdev/matchit" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#matrixmultiply@0.3.10", + "author": "bluss, R. Janis Goldschmidt", + "name": "matrixmultiply", + "version": "0.3.10", + "description": "General matrix multiplication for f32 and f64 matrices. Operates on matrices with general layout (they can use arbitrary row and column stride). Detects and uses AVX or SSE2 on x86 platforms transparently for higher performance. Uses a microkernel strategy, so that the implementation is easy to parallelize and optimize. Supports multithreading.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/matrixmultiply@0.3.10", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/matrixmultiply/" + }, + { + "type": "vcs", + "url": "https://github.com/bluss/matrixmultiply/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "author": "Andrew Gallant , bluss", + "name": "memchr", + "version": "2.8.0", + "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/memchr@2.8.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/memchr/" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/memchr" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/memchr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1", + "author": "Gilad Naaman ", + "name": "memoffset", + "version": "0.9.1", + "description": "offset_of functionality for Rust structs.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/memoffset@0.9.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Gilnaa/memoffset" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "author": "Sean McArthur ", + "name": "mime", + "version": "0.3.17", + "description": "Strongly Typed Mimes", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/mime@0.3.17", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/mime" + }, + { + "type": "vcs", + "url": "https://github.com/hyperium/mime" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5", + "author": "Austin Bonander ", + "name": "mime_guess", + "version": "2.0.5", + "description": "A simple crate for detection of a file's MIME type by its extension.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/mime_guess@2.0.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/mime_guess/" + }, + { + "type": "vcs", + "url": "https://github.com/abonander/mime_guess" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#mio@1.1.1", + "author": "Carl Lerche , Thomas de Zeeuw , Tokio Contributors ", + "name": "mio", + "version": "1.1.1", + "description": "Lightweight non-blocking I/O.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/mio@1.1.1", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/tokio-rs/mio" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/mio" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "author": "Thom Chiovoloni ", + "name": "more-asserts", + "version": "0.3.1", + "description": "Small library providing additional assert_* and debug_assert_* macros.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT OR Apache-2.0 OR CC0-1.0" + } + ], + "purl": "pkg:cargo/more-asserts@0.3.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/more-asserts" + }, + { + "type": "website", + "url": "https://github.com/thomcc/rust-more-asserts" + }, + { + "type": "vcs", + "url": "https://github.com/thomcc/rust-more-asserts" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#nalgebra@0.33.2", + "author": "Sébastien Crozet ", + "name": "nalgebra", + "version": "0.33.2", + "description": "General-purpose linear algebra library with transformations and statically-sized or dynamically-sized matrices.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/nalgebra@0.33.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://www.nalgebra.org/docs" + }, + { + "type": "website", + "url": "https://nalgebra.org" + }, + { + "type": "vcs", + "url": "https://github.com/dimforge/nalgebra" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "author": "ogham@bsago.me, Ryan Scheel (Havvy) , Josh Triplett , The Nushell Project Developers", + "name": "nu-ansi-term", + "version": "0.50.3", + "description": "Library for ANSI terminal colors and styles (bold, underline)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/nu-ansi-term@0.50.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/nushell/nu-ansi-term" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "author": "The Rust Project Developers", + "name": "num-bigint", + "version": "0.4.6", + "description": "Big integer implementation for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-bigint@0.4.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-bigint" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-bigint" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-bigint" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-complex@0.4.6", + "author": "The Rust Project Developers", + "name": "num-complex", + "version": "0.4.6", + "description": "Complex numbers implementation for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-complex@0.4.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-complex" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-complex" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-complex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-conv@0.2.0", + "author": "Jacob Pratt ", + "name": "num-conv", + "version": "0.2.0", + "description": "`num_conv` is a crate to convert between integer types without using `as` casts. This provides better certainty when refactoring, makes the exact behavior of code more explicit, and allows using turbofish syntax. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-conv@0.2.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/jhpratt/num-conv" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "author": "The Rust Project Developers", + "name": "num-integer", + "version": "0.1.46", + "description": "Integer traits and functions", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-integer@0.1.46", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-integer" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-integer" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-integer" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.2", + "author": "The Rust Project Developers", + "name": "num-rational", + "version": "0.4.2", + "description": "Rational numbers implementation for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-rational@0.4.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-rational" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-rational" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-rational" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "author": "The Rust Project Developers", + "name": "num-traits", + "version": "0.2.19", + "description": "Numeric traits for generic mathematics", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/num-traits@0.2.19", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/num-traits" + }, + { + "type": "website", + "url": "https://github.com/rust-num/num-traits" + }, + { + "type": "vcs", + "url": "https://github.com/rust-num/num-traits" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "author": "Aleksey Kladov ", + "name": "once_cell", + "version": "1.21.3", + "description": "Single assignment cells and lazy values.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/once_cell@1.21.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/once_cell" + }, + { + "type": "vcs", + "url": "https://github.com/matklad/once_cell" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#oneshot@0.1.13", + "author": "Linus Färnstrand ", + "name": "oneshot", + "version": "0.1.13", + "description": "Oneshot spsc channel with (potentially) lock-free non-blocking send, and a receiver supporting both thread blocking receive operations as well as Future based async polling. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/oneshot@0.1.13", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/faern/oneshot" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.2.1", + "author": "Alex Crichton ", + "name": "openssl-probe", + "version": "0.2.1", + "description": "A library for helping to find system-wide trust anchor (\"root\") certificate locations based on paths typically used by `openssl`. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/openssl-probe@0.2.1", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/rustls/openssl-probe" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/openssl-probe" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0", + "author": "Simon Ochsenreither ", + "name": "option-ext", + "version": "0.2.0", + "description": "Extends `Option` with additional operations", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + } + ], + "licenses": [ + { + "expression": "MPL-2.0" + } + ], + "purl": "pkg:cargo/option-ext@0.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/option-ext/" + }, + { + "type": "website", + "url": "https://github.com/soc/option-ext" + }, + { + "type": "vcs", + "url": "https://github.com/soc/option-ext.git" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#os_str_bytes@6.6.1", + "author": "dylni", + "name": "os_str_bytes", + "version": "6.6.1", + "description": "Convert between byte sequences and platform-native strings ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/os_str_bytes@6.6.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dylni/os_str_bytes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#page_size@0.6.0", + "author": "Philip Woods ", + "name": "page_size", + "version": "0.6.0", + "description": "Provides an easy, fast, cross-platform way to retrieve the memory page size", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/page_size@0.6.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/page_size/" + }, + { + "type": "website", + "url": "https://github.com/Elzair/page_size_rs" + }, + { + "type": "vcs", + "url": "https://github.com/Elzair/page_size_rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5", + "author": "Amanieu d'Antras ", + "name": "parking_lot", + "version": "0.12.5", + "description": "More compact and efficient implementations of the standard synchronization primitives.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/parking_lot@0.12.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Amanieu/parking_lot" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12", + "author": "Amanieu d'Antras ", + "name": "parking_lot_core", + "version": "0.9.12", + "description": "An advanced API for creating custom synchronization primitives.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/parking_lot_core@0.9.12", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Amanieu/parking_lot" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#paste@1.0.15", + "author": "David Tolnay ", + "name": "paste", + "version": "1.0.15", + "description": "Macros for all your token pasting needs", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/paste@1.0.15", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/paste" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/paste" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "author": "The rust-url developers", + "name": "percent-encoding", + "version": "2.3.2", + "description": "Percent encoding and decoding", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/percent-encoding@2.3.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/servo/rust-url/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3", + "author": "Steven Fackler ", + "name": "phf", + "version": "0.11.3", + "description": "Runtime support for perfect hash function data structures", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/phf@0.11.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-phf/rust-phf" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.3", + "author": "Steven Fackler ", + "name": "phf_generator", + "version": "0.11.3", + "description": "PHF generation logic", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/phf_generator@0.11.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-phf/rust-phf" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.11.3", + "author": "Steven Fackler ", + "name": "phf_macros", + "version": "0.11.3", + "description": "Macros to generate types in the phf crate", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/phf_macros@0.11.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-phf/rust-phf" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3", + "author": "Steven Fackler ", + "name": "phf_shared", + "version": "0.11.3", + "description": "Support code shared by PHF libraries", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/phf_shared@0.11.3", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-phf/rust-phf" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.10", + "name": "pin-project-internal", + "version": "1.1.10", + "description": "Implementation detail of the `pin-project` crate. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/pin-project-internal@1.1.10", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/pin-project" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "name": "pin-project-lite", + "version": "0.2.16", + "description": "A lightweight version of pin-project written with declarative macros. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/pin-project-lite@0.2.16", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/pin-project-lite" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10", + "name": "pin-project", + "version": "1.1.10", + "description": "A crate for safe and ergonomic pin-projection. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/pin-project@1.1.10", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/taiki-e/pin-project" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0", + "author": "Josef Brandl ", + "name": "pin-utils", + "version": "0.1.0", + "description": "Utilities for pinning ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pin-utils@0.1.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/pin-utils" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang-nursery/pin-utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.4", + "author": "The ICU4X Project Developers", + "name": "potential_utf", + "version": "0.1.4", + "description": "Unvalidated string and character types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/potential_utf@0.1.4", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0", + "author": "Jacob Pratt ", + "name": "powerfmt", + "version": "0.2.0", + "description": " `powerfmt` is a library that provides utilities for formatting values. This crate makes it significantly easier to support filling to a minimum width with alignment, avoid heap allocation, and avoid repetitive calculations. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/powerfmt@0.2.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/jhpratt/powerfmt" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21", + "author": "The CryptoCorrosion Contributors", + "name": "ppv-lite86", + "version": "0.2.21", + "description": "Cross-platform cryptography-oriented low-level SIMD library.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/ppv-lite86@0.2.21", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/cryptocorrosion/cryptocorrosion" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "author": "David Tolnay , Alex Crichton ", + "name": "proc-macro2", + "version": "1.0.106", + "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/proc-macro2@1.0.106", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/proc-macro2" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/proc-macro2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#prometheus@0.14.0", + "author": "overvenus@gmail.com, siddontang@gmail.com, vistaswx@gmail.com", + "name": "prometheus", + "version": "0.14.0", + "description": "Prometheus instrumentation library for Rust applications.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/prometheus@0.14.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/prometheus" + }, + { + "type": "website", + "url": "https://github.com/tikv/rust-prometheus" + }, + { + "type": "vcs", + "url": "https://github.com/tikv/rust-prometheus" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#protobuf-support@3.7.2", + "author": "Stepan Koltsov ", + "name": "protobuf-support", + "version": "3.7.2", + "description": "Code supporting protobuf implementation. None of code in this crate is public API. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/protobuf-support@3.7.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://github.com/stepancheg/rust-protobuf/blob/master/README.md" + }, + { + "type": "website", + "url": "https://github.com/stepancheg/rust-protobuf/" + }, + { + "type": "vcs", + "url": "https://github.com/stepancheg/rust-protobuf/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#protobuf@3.7.2", + "author": "Stepan Koltsov ", + "name": "protobuf", + "version": "3.7.2", + "description": "Rust implementation of Google protocol buffers ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/protobuf@3.7.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://github.com/stepancheg/rust-protobuf/blob/master/README.md" + }, + { + "type": "website", + "url": "https://github.com/stepancheg/rust-protobuf/" + }, + { + "type": "vcs", + "url": "https://github.com/stepancheg/rust-protobuf/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.26.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-build-config", + "version": "0.26.0", + "description": "Build configuration for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-build-config@0.26.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.26.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-ffi", + "version": "0.26.0", + "description": "Python-API bindings for the PyO3 ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-ffi@0.26.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "other", + "url": "python" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.26.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros-backend", + "version": "0.26.0", + "description": "Code generation for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros-backend@0.26.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.26.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3-macros", + "version": "0.26.0", + "description": "Proc macros for PyO3 package", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3-macros@0.26.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.26.0", + "author": "PyO3 Project and Contributors ", + "name": "pyo3", + "version": "0.26.0", + "description": "Bindings to Python interpreter", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/pyo3@0.26.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/crate/pyo3/" + }, + { + "type": "website", + "url": "https://github.com/pyo3/pyo3" + }, + { + "type": "vcs", + "url": "https://github.com/pyo3/pyo3" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quinn-proto@0.11.13", + "name": "quinn-proto", + "version": "0.11.13", + "description": "State machine for the QUIC transport protocol", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quinn-proto@0.11.13", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/quinn-rs/quinn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quinn-udp@0.5.14", + "name": "quinn-udp", + "version": "0.5.14", + "description": "UDP sockets with ECN information for the QUIC transport protocol", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quinn-udp@0.5.14", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/quinn-rs/quinn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quinn@0.11.9", + "name": "quinn", + "version": "0.11.9", + "description": "Versatile QUIC transport protocol implementation", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quinn@0.11.9", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/quinn-rs/quinn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "author": "David Tolnay ", + "name": "quote", + "version": "1.0.44", + "description": "Quasi-quoting macro quote!(...)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/quote@1.0.44", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/quote/" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/quote" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5", + "author": "The Rand Project Developers, The Rust Project Developers", + "name": "rand", + "version": "0.8.5", + "description": "Random number generators and other randomness functionality. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand@0.8.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "author": "The Rand Project Developers, The Rust Project Developers", + "name": "rand", + "version": "0.9.2", + "description": "Random number generators and other randomness functionality. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand@0.9.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1", + "author": "The Rand Project Developers, The Rust Project Developers, The CryptoCorrosion Contributors", + "name": "rand_chacha", + "version": "0.3.1", + "description": "ChaCha random number generator ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand_chacha@0.3.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand_chacha" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.9.0", + "author": "The Rand Project Developers, The Rust Project Developers, The CryptoCorrosion Contributors", + "name": "rand_chacha", + "version": "0.9.0", + "description": "ChaCha random number generator ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand_chacha@0.9.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand_chacha" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4", + "author": "The Rand Project Developers, The Rust Project Developers", + "name": "rand_core", + "version": "0.6.4", + "description": "Core random number generator traits and tools for implementation. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand_core@0.6.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand_core" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.9.5", + "author": "The Rand Project Developers, The Rust Project Developers", + "name": "rand_core", + "version": "0.9.5", + "description": "Core random number generator traits and tools for implementation. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand_core@0.9.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand_core" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rand_distr@0.4.3", + "author": "The Rand Project Developers", + "name": "rand_distr", + "version": "0.4.3", + "description": "Sampling from random number distributions ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rand_distr@0.4.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rand_distr" + }, + { + "type": "website", + "url": "https://rust-random.github.io/book" + }, + { + "type": "vcs", + "url": "https://github.com/rust-random/rand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rawpointer@0.2.1", + "author": "bluss", + "name": "rawpointer", + "version": "0.2.1", + "description": "Extra methods for raw pointers and `NonNull`. For example `.post_inc()` and `.pre_dec()` (c.f. `ptr++` and `--ptr`), `offset` and `add` for `NonNull`, and the function `ptrdistance`. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rawpointer@0.2.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rawpointer/" + }, + { + "type": "vcs", + "url": "https://github.com/bluss/rawpointer/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex-automata", + "version": "0.4.14", + "description": "Automata construction and matching using regular expressions.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-automata@0.4.14", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-automata" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex/tree/master/regex-automata" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex-syntax", + "version": "0.8.9", + "description": "A regular expression parser.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex-syntax@0.8.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex-syntax" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex/tree/master/regex-syntax" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "author": "The Rust Project Developers, Andrew Gallant ", + "name": "regex", + "version": "1.12.3", + "description": "An implementation of regular expressions for Rust. This implementation uses finite automata and guarantees linear time matching on all inputs. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/regex@1.12.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/regex" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/regex" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/regex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest-middleware@0.5.1", + "author": "Rodrigo Gryzinski ", + "name": "reqwest-middleware", + "version": "0.5.1", + "description": "Wrapper around reqwest to allow for client middleware chains.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/reqwest-middleware@0.5.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/TrueLayer/reqwest-middleware" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest-retry@0.9.1", + "author": "Rodrigo Gryzinski ", + "name": "reqwest-retry", + "version": "0.9.1", + "description": "Retry middleware for reqwest.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "fe2412db2af7d2268e7a5406be0431f37d9eb67ff390f35b395716f5f06c2eaa" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/reqwest-retry@0.9.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/TrueLayer/reqwest-middleware" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "author": "Sean McArthur ", + "name": "reqwest", + "version": "0.13.2", + "description": "higher level HTTP client library", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/reqwest@0.13.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/reqwest" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/reqwest" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#retry-policies@0.5.1", + "author": "Luca Palmieri ", + "name": "retry-policies", + "version": "0.5.1", + "description": "A collection of plug-and-play retry policies for Rust projects.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "46a4bd6027df676bcb752d3724db0ea3c0c5fc1dd0376fec51ac7dcaf9cc69be" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/retry-policies@0.5.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/TrueLayer/retry-policies" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "name": "ring", + "version": "0.17.14", + "description": "An experiment.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 AND ISC" + } + ], + "purl": "pkg:cargo/ring@0.17.14", + "externalReferences": [ + { + "type": "other", + "url": "ring_core_0_17_14_" + }, + { + "type": "vcs", + "url": "https://github.com/briansmith/ring" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0", + "author": "Paul Mason ", + "name": "rust_decimal", + "version": "1.40.0", + "description": "Decimal number implementation written in pure Rust suitable for financial and fixed-precision calculations.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/rust_decimal@1.40.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rust_decimal/" + }, + { + "type": "vcs", + "url": "https://github.com/paupino/rust-decimal" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustc-hash@2.1.1", + "author": "The Rust Project Developers", + "name": "rustc-hash", + "version": "2.1.1", + "description": "A speedy, non-cryptographic hashing algorithm used by rustc", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/rustc-hash@2.1.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rust-lang/rustc-hash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3", + "author": "Dan Gohman , Jakub Konka ", + "name": "rustix", + "version": "1.1.3", + "description": "Safe Rust bindings to POSIX/Unix/Linux/Winsock-like syscalls", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/rustix@1.1.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustix" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/rustix" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-native-certs@0.8.3", + "name": "rustls-native-certs", + "version": "0.8.3", + "description": "rustls-native-certs allows rustls to use the platform native certificate store", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR ISC OR MIT" + } + ], + "purl": "pkg:cargo/rustls-native-certs@0.8.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/rustls/rustls-native-certs" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/rustls-native-certs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "name": "rustls-pki-types", + "version": "1.14.0", + "description": "Shared types for the rustls PKI ecosystem", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rustls-pki-types@1.14.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustls-pki-types" + }, + { + "type": "website", + "url": "https://github.com/rustls/pki-types" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/pki-types" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-platform-verifier@0.6.2", + "name": "rustls-platform-verifier", + "version": "0.6.2", + "description": "rustls-platform-verifier supports verifying TLS certificates in rustls with the operating system verifier", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rustls-platform-verifier@0.6.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rustls/rustls-platform-verifier" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "name": "rustls-webpki", + "version": "0.103.9", + "description": "Web PKI X.509 Certificate Verification.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" + } + ], + "licenses": [ + { + "expression": "ISC" + } + ], + "purl": "pkg:cargo/rustls-webpki@0.103.9", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/rustls/webpki" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "name": "rustls", + "version": "0.23.36", + "description": "Rustls is a modern TLS library written in Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR ISC OR MIT" + } + ], + "purl": "pkg:cargo/rustls@0.23.36", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/rustls/rustls" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/rustls" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22", + "author": "David Tolnay ", + "name": "rustversion", + "version": "1.0.22", + "description": "Conditional compilation according to rustc compiler version", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/rustversion@1.0.22", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/rustversion" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/rustversion" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.23", + "author": "David Tolnay ", + "name": "ryu", + "version": "1.0.23", + "description": "Fast floating point to string conversion", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR BSL-1.0" + } + ], + "purl": "pkg:cargo/ryu@1.0.23", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/ryu" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/ryu" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#safe-transmute@0.11.3", + "author": "наб , Eduardo Pinho , Lukas Kalbertodt , Philipp Tessenow , Marijn Suijten ", + "name": "safe-transmute", + "version": "0.11.3", + "description": "A safeguarded transmute() for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/safe-transmute@0.11.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://rawcdn.githack.com/nabijaczleweli/safe-transmute-rs/doc/safe_transmute/index.html" + }, + { + "type": "vcs", + "url": "https://github.com/nabijaczleweli/safe-transmute-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#safe_arch@0.7.4", + "author": "Lokathor ", + "name": "safe_arch", + "version": "0.7.4", + "description": "Crate that exposes `core::arch` safely via `#[cfg()]`.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" + } + ], + "licenses": [ + { + "expression": "Zlib OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/safe_arch@0.7.4", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Lokathor/safe_arch" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6", + "author": "Andrew Gallant ", + "name": "same-file", + "version": "1.0.6", + "description": "A simple crate for determining whether two file paths point to the same file. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/same-file@1.0.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/same-file" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/same-file" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/same-file" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1", + "author": "Alex Crichton ", + "name": "scoped-tls", + "version": "1.0.1", + "description": "Library implementation of the standard library's old `scoped_thread_local!` macro for providing scoped access to thread local storage (TLS) so any type can be stored into TLS. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/scoped-tls@1.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/scoped-tls" + }, + { + "type": "website", + "url": "https://github.com/alexcrichton/scoped-tls" + }, + { + "type": "vcs", + "url": "https://github.com/alexcrichton/scoped-tls" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0", + "author": "bluss", + "name": "scopeguard", + "version": "1.2.0", + "description": "A RAII scope guard that will run a given closure when it goes out of scope, even if the code between panics (assuming unwinding panic). Defines the macros `defer!`, `defer_on_unwind!`, `defer_on_success!` as shorthands for guards with one of the implemented strategies. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/scopeguard@1.2.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/scopeguard/" + }, + { + "type": "vcs", + "url": "https://github.com/bluss/scopeguard" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde", + "version": "1.0.228", + "description": "A generic serialization/deserialization framework", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_core", + "version": "1.0.228", + "description": "Serde traits only, with no support for derive -- use the `serde` crate instead", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_core@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_core" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_derive", + "version": "1.0.228", + "description": "Macros 1.1 implementation of #[derive(Serialize, Deserialize)]", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_derive@1.0.228", + "externalReferences": [ + { + "type": "documentation", + "url": "https://serde.rs/derive.html" + }, + { + "type": "website", + "url": "https://serde.rs" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/serde" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "author": "Erick Tryzelaar , David Tolnay ", + "name": "serde_json", + "version": "1.0.149", + "description": "A JSON serialization file format", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_json@1.0.149", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_json" + }, + { + "type": "vcs", + "url": "https://github.com/serde-rs/json" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_path_to_error@0.1.20", + "author": "David Tolnay ", + "name": "serde_path_to_error", + "version": "0.1.20", + "description": "Path to the element that failed to deserialize", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_path_to_error@0.1.20", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_path_to_error" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/path-to-error" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_repr@0.1.20", + "author": "David Tolnay ", + "name": "serde_repr", + "version": "0.1.20", + "description": "Derive Serialize and Deserialize that delegates to the underlying repr of a C-like enum.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_repr@0.1.20", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_repr" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/serde-repr" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1", + "author": "Anthony Ramine ", + "name": "serde_urlencoded", + "version": "0.7.1", + "description": "`x-www-form-urlencoded` meets Serde", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/serde_urlencoded@0.7.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/serde_urlencoded/0.7.1/serde_urlencoded/" + }, + { + "type": "vcs", + "url": "https://github.com/nox/serde_urlencoded" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6", + "author": "RustCrypto Developers", + "name": "sha1", + "version": "0.10.6", + "description": "SHA-1 hash function", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/sha1@0.10.6", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sha1" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/hashes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sha2-asm@0.6.4", + "author": "RustCrypto Developers", + "name": "sha2-asm", + "version": "0.6.4", + "description": "Assembly implementation of SHA-2 compression functions", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b845214d6175804686b2bd482bcffe96651bb2d1200742b712003504a2dac1ab" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/sha2-asm@0.6.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sha2-asm" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/asm-hashes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9", + "author": "RustCrypto Developers", + "name": "sha2", + "version": "0.10.9", + "description": "Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/sha2@0.10.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sha2" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/hashes" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "author": "Eliza Weisman ", + "name": "sharded-slab", + "version": "0.1.7", + "description": "A lock-free concurrent slab. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/sharded-slab@0.1.7", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sharded-slab/" + }, + { + "type": "website", + "url": "https://github.com/hawkw/sharded-slab" + }, + { + "type": "vcs", + "url": "https://github.com/hawkw/sharded-slab" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#shellexpand@3.1.1", + "author": "Vladimir Matveev , Ian Jackson ", + "name": "shellexpand", + "version": "3.1.1", + "description": "Shell-like expansions in strings", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/shellexpand@3.1.1", + "externalReferences": [ + { + "type": "documentation", + "url": "http://docs.rs/shellexpand/" + }, + { + "type": "vcs", + "url": "https://gitlab.com/ijackson/rust-shellexpand" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0", + "author": "comex , Fenhl , Adrian Taylor , Alex Touchet , Daniel Parks , Garrett Berg ", + "name": "shlex", + "version": "1.3.0", + "description": "Split a string into shell words, like Python's shlex.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/shlex@1.3.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/comex/rust-shlex" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.8", + "author": "Michal 'vorner' Vaner , Masaki Hara ", + "name": "signal-hook-registry", + "version": "1.4.8", + "description": "Backend crate for signal-hook", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/signal-hook-registry@1.4.8", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/signal-hook-registry" + }, + { + "type": "vcs", + "url": "https://github.com/vorner/signal-hook" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18", + "author": "Michal 'vorner' Vaner , Thomas Himmelstoss ", + "name": "signal-hook", + "version": "0.3.18", + "description": "Unix signal handling", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/signal-hook@0.3.18", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/signal-hook" + }, + { + "type": "vcs", + "url": "https://github.com/vorner/signal-hook" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#simba@0.9.1", + "author": "sebcrozet ", + "name": "simba", + "version": "0.9.1", + "description": "SIMD algebra for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/simba@0.9.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/simba" + }, + { + "type": "vcs", + "url": "https://github.com/dimforge/simba" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#siphasher@1.0.2", + "author": "Frank Denis ", + "name": "siphasher", + "version": "1.0.2", + "description": "SipHash-2-4, SipHash-1-3 and 128-bit variants in pure Rust", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/siphasher@1.0.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/siphasher" + }, + { + "type": "website", + "url": "https://docs.rs/siphasher" + }, + { + "type": "vcs", + "url": "https://github.com/jedisct1/rust-siphash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12", + "author": "Carl Lerche ", + "name": "slab", + "version": "0.4.12", + "description": "Pre-allocated storage for a uniform data type", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/slab@0.4.12", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/tokio-rs/slab" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "author": "The Servo Project Developers", + "name": "smallvec", + "version": "1.15.1", + "description": "'Small vector' optimization: store up to a small number of items on the stack", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/smallvec@1.15.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/smallvec/" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-smallvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "author": "Alex Crichton , Thomas de Zeeuw ", + "name": "socket2", + "version": "0.6.2", + "description": "Utilities for handling networking sockets with a maximal amount of configuration possible intended. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/socket2@0.6.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/socket2" + }, + { + "type": "website", + "url": "https://github.com/rust-lang/socket2" + }, + { + "type": "vcs", + "url": "https://github.com/rust-lang/socket2" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.1", + "author": "Robert Grosse ", + "name": "stable_deref_trait", + "version": "1.2.1", + "description": "An unsafe marker trait for types like Box and Rc that dereference to a stable address even when moved, and hence can be used with libraries such as owning_ref and rental. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/stable_deref_trait@1.2.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/stable_deref_trait/1.2.1/stable_deref_trait" + }, + { + "type": "vcs", + "url": "https://github.com/storyyeller/stable_deref_trait" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0", + "author": "Nikolai Vazquez", + "name": "static_assertions", + "version": "1.1.0", + "description": "Compile-time assertions to ensure that invariants are met.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/static_assertions@1.1.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/static_assertions/" + }, + { + "type": "website", + "url": "https://github.com/nvzqz/static-assertions-rs" + }, + { + "type": "vcs", + "url": "https://github.com/nvzqz/static-assertions-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#statrs@0.18.0", + "author": "Michael Ma", + "name": "statrs", + "version": "0.18.0", + "description": "Statistical computing library for Rust", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/statrs@0.18.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/statrs-dev/statrs" + }, + { + "type": "vcs", + "url": "https://github.com/statrs-dev/statrs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1", + "author": "Danny Guo , maxbachmann ", + "name": "strsim", + "version": "0.11.1", + "description": "Implementations of string similarity metrics. Includes Hamming, Levenshtein, OSA, Damerau-Levenshtein, Jaro, Jaro-Winkler, and Sørensen-Dice. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/strsim@0.11.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/strsim/" + }, + { + "type": "website", + "url": "https://github.com/rapidfuzz/strsim-rs" + }, + { + "type": "vcs", + "url": "https://github.com/rapidfuzz/strsim-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "author": "Isis Lovecruft , Henry de Valence ", + "name": "subtle", + "version": "2.6.1", + "description": "Pure-Rust traits and utilities for constant-time cryptographic implementations.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + } + ], + "licenses": [ + { + "expression": "BSD-3-Clause" + } + ], + "purl": "pkg:cargo/subtle@2.6.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/subtle" + }, + { + "type": "website", + "url": "https://dalek.rs/" + }, + { + "type": "vcs", + "url": "https://github.com/dalek-cryptography/subtle" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109", + "author": "David Tolnay ", + "name": "syn", + "version": "1.0.109", + "description": "Parser for Rust source code", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@1.0.109", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114", + "author": "David Tolnay ", + "name": "syn", + "version": "2.0.114", + "description": "Parser for Rust source code", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/syn@2.0.114", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/syn" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/syn" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "author": "Actyx AG ", + "name": "sync_wrapper", + "version": "1.0.2", + "description": "A tool for enlisting the compiler's help in proving the absence of concurrency", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + } + ], + "licenses": [ + { + "expression": "Apache-2.0" + } + ], + "purl": "pkg:cargo/sync_wrapper@1.0.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/sync_wrapper" + }, + { + "type": "website", + "url": "https://docs.rs/sync_wrapper" + }, + { + "type": "vcs", + "url": "https://github.com/Actyx/sync_wrapper" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#synchronoise@1.0.1", + "author": "QuietMisdreavus ", + "name": "synchronoise", + "version": "1.0.1", + "description": "Synchronization primitives that build upon the standard library", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/synchronoise@1.0.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/synchronoise/" + }, + { + "type": "vcs", + "url": "https://github.com/QuietMisdreavus/synchronoise" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2", + "author": "Nika Layzell ", + "name": "synstructure", + "version": "0.13.2", + "description": "Helper methods and macros for custom derives", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/synstructure@0.13.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/synstructure" + }, + { + "type": "vcs", + "url": "https://github.com/mystor/synstructure" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#sysinfo@0.38.1", + "author": "Guillaume Gomez ", + "name": "sysinfo", + "version": "0.38.1", + "description": "Library to get system information such as processes, CPUs, disks, components and networks", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5792d209c2eac902426c0c4a166c9f72147db453af548cf9bf3242644c4d4fe3" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/sysinfo@0.38.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/GuillaumeGomez/sysinfo" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4", + "author": "Dan Gohman ", + "name": "target-lexicon", + "version": "0.13.4", + "description": "LLVM target triple types", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:cargo/target-lexicon@0.13.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/target-lexicon/" + }, + { + "type": "vcs", + "url": "https://github.com/bytecodealliance/target-lexicon" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "author": "Steven Allen , The Rust Project Developers, Ashley Mannix , Jason White ", + "name": "tempfile", + "version": "3.25.0", + "description": "A library for managing temporary files and directories.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/tempfile@3.25.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tempfile" + }, + { + "type": "website", + "url": "https://stebalien.com/projects/tempfile-rs/" + }, + { + "type": "vcs", + "url": "https://github.com/Stebalien/tempfile" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.69", + "author": "David Tolnay ", + "name": "thiserror-impl", + "version": "1.0.69", + "description": "Implementation detail of the `thiserror` crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror-impl@1.0.69", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18", + "author": "David Tolnay ", + "name": "thiserror-impl", + "version": "2.0.18", + "description": "Implementation detail of the `thiserror` crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror-impl@2.0.18", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69", + "author": "David Tolnay ", + "name": "thiserror", + "version": "1.0.69", + "description": "derive(Error)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror@1.0.69", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/thiserror" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "author": "David Tolnay ", + "name": "thiserror", + "version": "2.0.18", + "description": "derive(Error)", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thiserror@2.0.18", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/thiserror" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/thiserror" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "author": "Amanieu d'Antras ", + "name": "thread_local", + "version": "1.1.9", + "description": "Per-object thread-local storage", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/thread_local@1.1.9", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/thread_local/" + }, + { + "type": "vcs", + "url": "https://github.com/Amanieu/thread_local-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.8", + "author": "Jacob Pratt , Time contributors", + "name": "time-core", + "version": "0.1.8", + "description": "This crate is an implementation detail and should not be relied upon directly.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/time-core@0.1.8", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/time-rs/time" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.27", + "author": "Jacob Pratt , Time contributors", + "name": "time-macros", + "version": "0.2.27", + "description": " Procedural macros for the time crate. This crate is an implementation detail and should not be relied upon directly. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/time-macros@0.2.27", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/time-rs/time" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#time@0.3.47", + "author": "Jacob Pratt , Time contributors", + "name": "time", + "version": "0.3.47", + "description": "Date and time library. Fully interoperable with the standard library. Mostly compatible with #![no_std].", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/time@0.3.47", + "externalReferences": [ + { + "type": "website", + "url": "https://time-rs.github.io" + }, + { + "type": "vcs", + "url": "https://github.com/time-rs/time" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.2", + "author": "The ICU4X Project Developers", + "name": "tinystr", + "version": "0.8.2", + "description": "A small ASCII-only bounded length string representation.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/tinystr@0.8.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.10.0", + "author": "Lokathor ", + "name": "tinyvec", + "version": "1.10.0", + "description": "`tinyvec` provides 100% safe vec-like data structures.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" + } + ], + "licenses": [ + { + "expression": "Zlib OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/tinyvec@1.10.0", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Lokathor/tinyvec" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1", + "author": "Soveu ", + "name": "tinyvec_macros", + "version": "0.1.1", + "description": "Some macros for tiny containers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0 OR Zlib" + } + ], + "purl": "pkg:cargo/tinyvec_macros@0.1.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Soveu/tinyvec_macros" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.6.0", + "author": "Tokio Contributors ", + "name": "tokio-macros", + "version": "2.6.0", + "description": "Tokio's proc macros. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tokio-macros@2.6.0", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tokio" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-retry@0.3.0", + "author": "Sam Rijs ", + "name": "tokio-retry", + "version": "0.3.0", + "description": "Extensible, asynchronous retry behaviours for futures/tokio", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tokio-retry@0.3.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tokio-retry" + }, + { + "type": "vcs", + "url": "https://github.com/srijs/rust-tokio-retry" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4", + "name": "tokio-rustls", + "version": "0.26.4", + "description": "Asynchronous TLS/SSL streams for Tokio using Rustls.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/tokio-rustls@0.26.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tokio-rustls" + }, + { + "type": "website", + "url": "https://github.com/rustls/tokio-rustls" + }, + { + "type": "vcs", + "url": "https://github.com/rustls/tokio-rustls" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "author": "Tokio Contributors ", + "name": "tokio-util", + "version": "0.7.18", + "description": "Additional utilities for working with Tokio. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tokio-util@0.7.18", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tokio" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "author": "Tokio Contributors ", + "name": "tokio", + "version": "1.49.0", + "description": "An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tokio@1.49.0", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tokio" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tower-http@0.6.8", + "author": "Tower Maintainers ", + "name": "tower-http", + "version": "0.6.8", + "description": "Tower middleware and utilities for HTTP clients and servers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tower-http@0.6.8", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/tower-rs/tower-http" + }, + { + "type": "vcs", + "url": "https://github.com/tower-rs/tower-http" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "author": "Tower Maintainers ", + "name": "tower-layer", + "version": "0.3.3", + "description": "Decorates a `Service` to allow easy composition between `Service`s. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tower-layer@0.3.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tower-layer/0.3.3" + }, + { + "type": "website", + "url": "https://github.com/tower-rs/tower" + }, + { + "type": "vcs", + "url": "https://github.com/tower-rs/tower" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "author": "Tower Maintainers ", + "name": "tower-service", + "version": "0.3.3", + "description": "Trait representing an asynchronous, request / response based, client or server. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tower-service@0.3.3", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/tower-service/0.3.3" + }, + { + "type": "website", + "url": "https://github.com/tower-rs/tower" + }, + { + "type": "vcs", + "url": "https://github.com/tower-rs/tower" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3", + "author": "Tower Maintainers ", + "name": "tower", + "version": "0.5.3", + "description": "Tower is a library of modular and reusable components for building robust clients and servers. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tower@0.5.3", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/tower-rs/tower" + }, + { + "type": "vcs", + "url": "https://github.com/tower-rs/tower" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-appender@0.2.4", + "author": "Zeki Sherif , Tokio Contributors ", + "name": "tracing-appender", + "version": "0.2.4", + "description": "Provides utilities for file appenders and making non-blocking writers. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-appender@0.2.4", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "author": "Tokio Contributors , Eliza Weisman , David Barsky ", + "name": "tracing-attributes", + "version": "0.1.31", + "description": "Procedural macro attributes for automatically instrumenting functions. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-attributes@0.1.31", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "author": "Tokio Contributors ", + "name": "tracing-core", + "version": "0.1.36", + "description": "Core primitives for application-level tracing. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-core@0.1.36", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "author": "Tokio Contributors ", + "name": "tracing-log", + "version": "0.2.0", + "description": "Provides compatibility between `tracing` and the `log` crate. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-log@0.2.0", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-serde@0.2.0", + "author": "Tokio Contributors ", + "name": "tracing-serde", + "version": "0.2.0", + "description": "A compatibility layer for serializing trace data with `serde` ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-serde@0.2.0", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "author": "Eliza Weisman , David Barsky , Tokio Contributors ", + "name": "tracing-subscriber", + "version": "0.3.22", + "description": "Utilities for implementing and composing `tracing` subscribers. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing-subscriber@0.3.22", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "author": "Eliza Weisman , Tokio Contributors ", + "name": "tracing", + "version": "0.1.44", + "description": "Application-level tracing for Rust. ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/tracing@0.1.44", + "externalReferences": [ + { + "type": "website", + "url": "https://tokio.rs" + }, + { + "type": "vcs", + "url": "https://github.com/tokio-rs/tracing" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5", + "author": "Sean McArthur ", + "name": "try-lock", + "version": "0.2.5", + "description": "A lightweight atomic lock.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/try-lock@0.2.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/try-lock" + }, + { + "type": "website", + "url": "https://github.com/seanmonstar/try-lock" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/try-lock" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#twox-hash@2.1.2", + "author": "Jake Goulding ", + "name": "twox-hash", + "version": "2.1.2", + "description": "A Rust implementation of the XXHash and XXH3 algorithms", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/twox-hash@2.1.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/twox-hash/" + }, + { + "type": "vcs", + "url": "https://github.com/shepmaster/twox-hash" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0", + "author": "Paho Lurie-Gregg , Andre Bogus ", + "name": "typenum", + "version": "1.19.0", + "description": "Typenum is a Rust library for type-level numbers evaluated at compile time. It currently supports bits, unsigned integers, and signed integers. It also provides a type-level array of type-level numbers, but its implementation is incomplete.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/typenum@1.19.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/typenum" + }, + { + "type": "vcs", + "url": "https://github.com/paholg/typenum" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#typewit@1.14.2", + "author": "rodrimati1992 ", + "name": "typewit", + "version": "1.14.2", + "description": "type-witness-based abstractions, mostly for emulating polymorphism in const fns", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" + } + ], + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:cargo/typewit@1.14.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/typewit/" + }, + { + "type": "vcs", + "url": "https://github.com/rodrimati1992/typewit/" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ulid@1.2.1", + "author": "dylanhart ", + "name": "ulid", + "version": "1.2.1", + "description": "a Universally Unique Lexicographically Sortable Identifier implementation", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/ulid@1.2.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/dylanhart/ulid-rs" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicase@2.9.0", + "author": "Sean McArthur ", + "name": "unicase", + "version": "2.9.0", + "description": "A case-insensitive wrapper around strings.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/unicase@2.9.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicase" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/unicase" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.23", + "author": "David Tolnay ", + "name": "unicode-ident", + "version": "1.0.23", + "description": "Determine whether characters have the XID_Start or XID_Continue properties according to Unicode Standard Annex #31", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + } + ], + "licenses": [ + { + "expression": "(MIT OR Apache-2.0) AND Unicode-3.0" + } + ], + "purl": "pkg:cargo/unicode-ident@1.0.23", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unicode-ident" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/unicode-ident" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#unindent@0.2.4", + "author": "David Tolnay ", + "name": "unindent", + "version": "0.2.4", + "description": "Remove a column of leading whitespace from a string", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/unindent@0.2.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/unindent" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/indoc" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0", + "author": "Brian Smith ", + "name": "untrusted", + "version": "0.9.0", + "description": "Safe, fast, zero-panic, zero-crashing, zero-allocation parsing of untrusted inputs in Rust.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + } + ], + "licenses": [ + { + "expression": "ISC" + } + ], + "purl": "pkg:cargo/untrusted@0.9.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://briansmith.org/rustdoc/untrusted/" + }, + { + "type": "vcs", + "url": "https://github.com/briansmith/untrusted" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "author": "The rust-url developers", + "name": "url", + "version": "2.5.8", + "description": "URL library for Rust, based on the WHATWG URL Standard", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/url@2.5.8", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/url" + }, + { + "type": "vcs", + "url": "https://github.com/servo/rust-url" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3", + "author": "Kornel , Bertram Truong ", + "name": "urlencoding", + "version": "2.1.3", + "description": "A Rust library for doing URL percentage encoding.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/urlencoding@2.1.3", + "externalReferences": [ + { + "type": "website", + "url": "https://lib.rs/urlencoding" + }, + { + "type": "vcs", + "url": "https://github.com/kornelski/rust_urlencoding" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4", + "author": "Henri Sivonen ", + "name": "utf8_iter", + "version": "1.0.4", + "description": "Iterator by char over potentially-invalid UTF-8 in &[u8]", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/utf8_iter@1.0.4", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/utf8_iter/" + }, + { + "type": "website", + "url": "https://docs.rs/utf8_iter/" + }, + { + "type": "vcs", + "url": "https://github.com/hsivonen/utf8_iter" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2", + "author": "Joe Wilm , Christian Duerr ", + "name": "utf8parse", + "version": "0.2.2", + "description": "Table-driven UTF-8 parser", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/utf8parse@0.2.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/utf8parse/" + }, + { + "type": "vcs", + "url": "https://github.com/alacritty/vte" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#uuid@1.20.0", + "author": "Ashley Mannix, Dylan DPC, Hunar Roop Kahlon", + "name": "uuid", + "version": "1.20.0", + "description": "A library to generate and parse UUIDs.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/uuid@1.20.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/uuid" + }, + { + "type": "website", + "url": "https://github.com/uuid-rs/uuid" + }, + { + "type": "vcs", + "url": "https://github.com/uuid-rs/uuid" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5", + "author": "Sergio Benitez ", + "name": "version_check", + "version": "0.9.5", + "description": "Tiny crate to check the version of the installed/running rustc.", + "scope": "excluded", + "hashes": [ + { + "alg": "SHA-256", + "content": "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + } + ], + "licenses": [ + { + "expression": "MIT OR Apache-2.0" + } + ], + "purl": "pkg:cargo/version_check@0.9.5", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/version_check/" + }, + { + "type": "vcs", + "url": "https://github.com/SergioBenitez/version_check" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0", + "author": "Andrew Gallant ", + "name": "walkdir", + "version": "2.5.0", + "description": "Recursively walk a directory.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" + } + ], + "licenses": [ + { + "expression": "Unlicense OR MIT" + } + ], + "purl": "pkg:cargo/walkdir@2.5.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/walkdir/" + }, + { + "type": "website", + "url": "https://github.com/BurntSushi/walkdir" + }, + { + "type": "vcs", + "url": "https://github.com/BurntSushi/walkdir" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#want@0.3.1", + "author": "Sean McArthur ", + "name": "want", + "version": "0.3.1", + "description": "Detect when another Future wants a result.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/want@0.3.1", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/want" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/want" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#warp@0.4.2", + "author": "Sean McArthur ", + "name": "warp", + "version": "0.4.2", + "description": "serve the web at warp speeds", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/warp@0.4.2", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/warp" + }, + { + "type": "vcs", + "url": "https://github.com/seanmonstar/warp" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#whoami@2.1.0", + "name": "whoami", + "version": "2.1.0", + "description": "Rust library for getting information about the current user and environment", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "8fae98cf96deed1b7572272dfc777713c249ae40aa1cf8862e091e8b745f5361" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR BSL-1.0 OR MIT" + } + ], + "purl": "pkg:cargo/whoami@2.1.0", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/whoami" + }, + { + "type": "website", + "url": "https://github.com/ardaku/whoami/releases" + }, + { + "type": "vcs", + "url": "https://github.com/ardaku/whoami" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#wide@0.7.33", + "author": "Lokathor ", + "name": "wide", + "version": "0.7.33", + "description": "A crate to help you go wide.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" + } + ], + "licenses": [ + { + "expression": "Zlib OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/wide@0.7.33", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/Lokathor/wide" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14", + "name": "winnow", + "version": "0.7.14", + "description": "A byte-oriented, zero-copy, parser combinators library", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/winnow@0.7.14", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/winnow-rs/winnow" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2", + "author": "The ICU4X Project Developers", + "name": "writeable", + "version": "0.6.2", + "description": "A more efficient alternative to fmt::Display", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/writeable@0.6.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.8.1", + "author": "Manish Goregaokar ", + "name": "yoke-derive", + "version": "0.8.1", + "description": "Custom derive for the yoke crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/yoke-derive@0.8.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "author": "Manish Goregaokar ", + "name": "yoke", + "version": "0.8.1", + "description": "Abstraction allowing borrowed data to be carried along with the backing data it borrows from", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/yoke@0.8.1", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.39", + "author": "Joshua Liebow-Feeser , Jack Wrenn ", + "name": "zerocopy-derive", + "version": "0.8.39", + "description": "Custom derive for traits from the zerocopy crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zerocopy-derive@0.8.39", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/google/zerocopy" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.39", + "author": "Joshua Liebow-Feeser , Jack Wrenn ", + "name": "zerocopy", + "version": "0.8.39", + "description": "Zerocopy makes zero-cost memory manipulation effortless. We write \"unsafe\" so you don't have to.", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" + } + ], + "licenses": [ + { + "expression": "BSD-2-Clause OR Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zerocopy@0.8.39", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/google/zerocopy" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.6", + "author": "Manish Goregaokar ", + "name": "zerofrom-derive", + "version": "0.1.6", + "description": "Custom derive for the zerofrom crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerofrom-derive@0.1.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6", + "author": "Manish Goregaokar ", + "name": "zerofrom", + "version": "0.1.6", + "description": "ZeroFrom trait for constructing", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerofrom@0.1.6", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2", + "author": "The RustCrypto Project Developers", + "name": "zeroize", + "version": "1.8.2", + "description": "Securely clear secrets from memory with a simple trait built on stable Rust primitives which guarantee memory is zeroed using an operation will not be 'optimized away' by the compiler. Uses a portable pure Rust implementation that works everywhere, even WASM! ", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + } + ], + "licenses": [ + { + "expression": "Apache-2.0 OR MIT" + } + ], + "purl": "pkg:cargo/zeroize@1.8.2", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/RustCrypto/utils/tree/master/zeroize" + }, + { + "type": "vcs", + "url": "https://github.com/RustCrypto/utils" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3", + "author": "The ICU4X Project Developers", + "name": "zerotrie", + "version": "0.2.3", + "description": "A data structure that efficiently maps strings to integers", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerotrie@0.2.3", + "externalReferences": [ + { + "type": "website", + "url": "https://icu4x.unicode.org" + }, + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.11.2", + "author": "Manish Goregaokar ", + "name": "zerovec-derive", + "version": "0.11.2", + "description": "Custom derive for the zerovec crate", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerovec-derive@0.11.2", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5", + "author": "The ICU4X Project Developers", + "name": "zerovec", + "version": "0.11.5", + "description": "Zero-copy vector backed by a byte array", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" + } + ], + "licenses": [ + { + "expression": "Unicode-3.0" + } + ], + "purl": "pkg:cargo/zerovec@0.11.5", + "externalReferences": [ + { + "type": "vcs", + "url": "https://github.com/unicode-org/icu4x" + } + ] + }, + { + "type": "library", + "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.20", + "author": "David Tolnay ", + "name": "zmij", + "version": "1.0.20", + "description": "A double-to-string conversion algorithm based on Schubfach and yy", + "scope": "required", + "hashes": [ + { + "alg": "SHA-256", + "content": "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" + } + ], + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:cargo/zmij@1.0.20", + "externalReferences": [ + { + "type": "documentation", + "url": "https://docs.rs/zmij" + }, + { + "type": "vcs", + "url": "https://github.com/dtolnay/zmij" + } + ] + } + ], + "dependencies": [ + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#axum@0.8.8", + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "path+file:///home/runner/work/xet-core/xet-core/cas_object#0.1.0", + "path+file:///home/runner/work/xet-core/xet-core/cas_types#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "path+file:///home/runner/work/xet-core/xet-core/deduplication#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/file_utils#0.14.2", + "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#heed@0.22.0", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "registry+https://github.com/rust-lang/crates.io-index#reqwest-middleware@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#reqwest-retry@0.9.1", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#statrs@0.18.0", + "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-retry@0.3.0", + "registry+https://github.com/rust-lang/crates.io-index#tower-http@0.6.8", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#warp@0.4.2", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/cas_object#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "registry+https://github.com/rust-lang/crates.io-index#countio@0.3.0", + "registry+https://github.com/rust-lang/crates.io-index#csv@1.4.0", + "path+file:///home/runner/work/xet-core/xet-core/deduplication#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#half@2.7.1", + "registry+https://github.com/rust-lang/crates.io-index#lz4_flex@0.12.0", + "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/cas_types#0.1.0", + "dependsOn": [ + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_repr@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/data#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/cas_object#0.1.0", + "path+file:///home/runner/work/xet-core/xet-core/cas_types#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "path+file:///home/runner/work/xet-core/xet-core/deduplication#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/file_reconstruction#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "path+file:///home/runner/work/xet-core/xet-core/hub_client#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#prometheus@0.14.0", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9", + "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#ulid@1.2.1", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/deduplication#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#gearhash@0.1.3", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/file_reconstruction#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/cas_types#0.1.0", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_config#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/file_utils#0.14.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#colored@3.1.1", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#whoami@2.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/hf_xet#1.3.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "path+file:///home/runner/work/xet-core/xet-core/data#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_config#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_logging#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/hub_client#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "path+file:///home/runner/work/xet-core/xet-core/cas_client#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "registry+https://github.com/rust-lang/crates.io-index#reqwest-middleware@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/mdb_shard#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#heapify@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#uuid@1.20.0", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.25.0", + "registry+https://github.com/rust-lang/crates.io-index#heed@0.22.0", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#safe-transmute@0.11.3", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/progress_tracking#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#bincode@1.3.3", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "registry+https://github.com/rust-lang/crates.io-index#ctor@0.6.3", + "registry+https://github.com/rust-lang/crates.io-index#derivative@2.2.0", + "registry+https://github.com/rust-lang/crates.io-index#duration-str@0.19.0", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "path+file:///home/runner/work/xet-core/xet-core/merklehash#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#shellexpand@3.1.1", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/xet_config#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#const-str@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#konst@0.4.3", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/xet_logging#0.14.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#git-version@0.3.9", + "registry+https://github.com/rust-lang/crates.io-index#sysinfo@0.38.1", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#tracing-appender@0.2.4", + "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0" + ] + }, + { + "ref": "path+file:///home/runner/work/xet-core/xet-core/xet_runtime#0.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0", + "path+file:///home/runner/work/xet-core/xet-core/error_printer#0.14.5", + "registry+https://github.com/rust-lang/crates.io-index#oneshot@0.1.13", + "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "path+file:///home/runner/work/xet-core/xet-core/utils#0.14.5", + "path+file:///home/runner/work/xet-core/xet-core/xet_config#0.14.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.1.5", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#approx@0.5.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#aws-lc-rs@1.15.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aws-lc-sys@0.37.0", + "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#aws-lc-sys@0.37.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "registry+https://github.com/rust-lang/crates.io-index#cmake@0.1.57", + "registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5", + "registry+https://github.com/rust-lang/crates.io-index#fs_extra@1.3.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#axum-core@0.5.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#axum@0.8.8", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#axum-core@0.5.6", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#matchit@0.8.4", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#serde_path_to_error@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1", + "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bincode@1.3.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#blake3@1.8.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#arrayref@0.3.9", + "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bstr@1.12.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.25.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "registry+https://github.com/rust-lang/crates.io-index#jobserver@0.1.34", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@0.1.10", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap@4.5.57", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.57", + "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.5.57", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.21", + "registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.13", + "registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.7", + "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.5.55", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.7.7", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cmake@0.1.57", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#colored@3.1.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#const-str@1.1.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#const_panic@0.2.15", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#typewit@1.14.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#constant_time_eq@0.4.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#countio@0.3.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-channel@0.5.15", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.12", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.21", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7", + "registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#csv-core@0.1.13", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#csv@1.4.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#csv-core@0.1.13", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.23", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ctor-proc-macro@0.0.7", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ctor@0.6.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ctor-proc-macro@0.0.7", + "registry+https://github.com/rust-lang/crates.io-index#dtor@0.1.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#derivative@2.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4", + "registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#dirs-sys@0.5.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#doxygen-rs@0.4.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#dtor-proc-macro@0.0.6", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#dtor@0.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#dtor-proc-macro@0.0.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#dunce@1.0.5", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#duration-str@0.19.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.43", + "registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#time@0.3.47", + "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#either@1.15.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#find-msvc-tools@0.1.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#fs_extra@1.3.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#gearhash@0.1.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@0.1.10" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0", + "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#git-version-macro@0.3.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#git-version@0.3.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#git-version-macro@0.3.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#h2@0.4.13", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#half@2.7.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.39" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#headers-core@0.3.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#headers@0.4.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#headers-core@0.3.0", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heapify@0.2.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heed-traits@0.20.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heed-types@0.21.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bincode@1.3.3", + "registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#heed-traits@0.20.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#heed@0.22.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0", + "registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#heed-traits@0.20.0", + "registry+https://github.com/rust-lang/crates.io-index#heed-types@0.21.0", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#lmdb-master-sys@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#page_size@0.6.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#synchronoise@1.0.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hyper-rustls@0.27.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#ipnet@2.11.0", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#h2@0.4.13", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#httparse@1.10.1", + "registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#want@0.3.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.65", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.4", + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_locale_core@2.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.2", + "registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer_data@2.1.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#icu_collections@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_locale_core@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2", + "registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_properties_data@2.1.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#icu_provider@2.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#icu_locale_core@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6", + "registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#idna_adapter@1.2.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#icu_normalizer@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#icu_properties@2.1.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.13.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.16.1", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#indoc@2.0.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ipnet@2.11.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#iri-string@0.7.10", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#is_terminal_polyfill@1.70.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itertools@0.14.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#either@1.15.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#jobserver@0.1.34", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#konst@0.4.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#const_panic@0.2.15", + "registry+https://github.com/rust-lang/crates.io-index#konst_proc_macros@0.4.1", + "registry+https://github.com/rust-lang/crates.io-index#typewit@1.14.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#konst_proc_macros@0.4.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#libm@0.2.16", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#litemap@0.8.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lmdb-master-sys@0.2.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "registry+https://github.com/rust-lang/crates.io-index#doxygen-rs@0.4.2", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lru-slab@0.1.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#lz4_flex@0.12.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#twox-hash@2.1.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#matchit@0.8.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#matrixmultiply@0.3.10", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#rawpointer@0.2.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "registry+https://github.com/rust-lang/crates.io-index#unicase@2.9.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#mio@1.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#more-asserts@0.3.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#nalgebra@0.33.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#approx@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#matrixmultiply@0.3.10", + "registry+https://github.com/rust-lang/crates.io-index#num-complex@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.2", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5", + "registry+https://github.com/rust-lang/crates.io-index#rand_distr@0.4.3", + "registry+https://github.com/rust-lang/crates.io-index#simba@0.9.1", + "registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-complex@0.4.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-conv@0.2.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.46", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#libm@0.2.16" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#oneshot@0.1.13", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.2.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#option-ext@0.2.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#os_str_bytes@6.6.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#page_size@0.6.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.14", + "registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.12", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#paste@1.0.15", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.11.3", + "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#phf_macros@0.11.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.3", + "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#siphasher@1.0.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.10", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.10" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#potential_utf@0.1.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.39" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.23" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#prometheus@0.14.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7", + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.5", + "registry+https://github.com/rust-lang/crates.io-index#protobuf@3.7.2", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#protobuf-support@3.7.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#protobuf@3.7.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#protobuf-support@3.7.2", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.26.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.26.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.26.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.26.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0", + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.26.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.26.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#indoc@2.0.7", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.1", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.26.0", + "registry+https://github.com/rust-lang/crates.io-index#unindent@0.2.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quinn-proto@0.11.13", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aws-lc-rs@1.15.4", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#lru-slab@0.1.2", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "registry+https://github.com/rust-lang/crates.io-index#rustc-hash@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.10.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quinn-udp@0.5.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quinn@0.11.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#cfg_aliases@0.2.1", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#quinn-proto@0.11.13", + "registry+https://github.com/rust-lang/crates.io-index#quinn-udp@0.5.14", + "registry+https://github.com/rust-lang/crates.io-index#rustc-hash@2.1.1", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1", + "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.9.0", + "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.9.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21", + "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.9.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21", + "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.9.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand_core@0.9.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rand_distr@0.4.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rawpointer@0.2.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#regex@1.12.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest-middleware@0.5.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest-retry@0.9.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.101", + "registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.89", + "registry+https://github.com/rust-lang/crates.io-index#futures@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "registry+https://github.com/rust-lang/crates.io-index#reqwest-middleware@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#retry-policies@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#reqwest@0.13.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#base64@0.22.1", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#hyper-rustls@0.27.7", + "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#quinn@0.11.9", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#rustls-platform-verifier@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4", + "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-http@0.6.8", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#retry-policies@0.5.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55", + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17", + "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rust_decimal@1.40.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.6", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustc-hash@2.1.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0", + "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.11.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-native-certs@0.8.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.2.1", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-platform-verifier@0.6.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#rustls-native-certs@0.8.3", + "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aws-lc-rs@1.15.4", + "registry+https://github.com/rust-lang/crates.io-index#ring@0.17.14", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#aws-lc-rs@1.15.4", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#rustls-pki-types@1.14.0", + "registry+https://github.com/rust-lang/crates.io-index#rustls-webpki@0.103.9", + "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#rustversion@1.0.22", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.23", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#safe-transmute@0.11.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#safe_arch@0.7.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.25.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.228", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.20" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_path_to_error@0.1.20", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_repr@0.1.20", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.23", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sha2-asm@0.6.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cc@1.2.55" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4", + "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17", + "registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7", + "registry+https://github.com/rust-lang/crates.io-index#sha2-asm@0.6.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.5.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#shellexpand@3.1.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bstr@1.12.1", + "registry+https://github.com/rust-lang/crates.io-index#dirs@6.0.0", + "registry+https://github.com/rust-lang/crates.io-index#os_str_bytes@6.6.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#shlex@1.3.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.8", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#errno@0.3.14", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#signal-hook@0.3.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.8" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#simba@0.9.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#approx@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#num-complex@0.4.6", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#paste@1.0.15", + "registry+https://github.com/rust-lang/crates.io-index#wide@0.7.33" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#siphasher@1.0.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#slab@0.4.12", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#static_assertions@1.1.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#statrs@0.18.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#approx@0.5.1", + "registry+https://github.com/rust-lang/crates.io-index#nalgebra@0.33.2", + "registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#strsim@0.11.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#subtle@2.6.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.23" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.23" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#synchronoise@1.0.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.12" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#sysinfo@0.38.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.13.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tempfile@3.25.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#fastrand@2.3.0", + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.1", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#rustix@1.1.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.69", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.69", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.69" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.18" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.8", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.27", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#num-conv@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.8" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#time@0.3.47", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#deranged@0.5.5", + "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17", + "registry+https://github.com/rust-lang/crates.io-index#num-conv@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.8", + "registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.27" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tinystr@0.8.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.10.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.6.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-retry@0.3.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10", + "registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-rustls@0.26.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rustls@0.23.36", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.181", + "registry+https://github.com/rust-lang/crates.io-index#mio@1.1.1", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#socket2@0.6.2", + "registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.6.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tower-http@0.6.8", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0", + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#iri-string@0.7.10", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tower@0.5.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#sync_wrapper@1.0.2", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tower-layer@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-appender@0.2.4", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#crossbeam-channel@0.5.15", + "registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.18", + "registry+https://github.com/rust-lang/crates.io-index#time@0.3.47", + "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-serde@0.2.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing-subscriber@0.3.22", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#matchers@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#nu-ansi-term@0.50.3", + "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3", + "registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.14", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#sharded-slab@0.1.7", + "registry+https://github.com/rust-lang/crates.io-index#smallvec@1.15.1", + "registry+https://github.com/rust-lang/crates.io-index#thread_local@1.1.9", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36", + "registry+https://github.com/rust-lang/crates.io-index#tracing-log@0.2.0", + "registry+https://github.com/rust-lang/crates.io-index#tracing-serde@0.2.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.16", + "registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.31", + "registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.36" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#twox-hash@2.1.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#typenum@1.19.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#typewit@1.14.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#ulid@1.2.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#rand@0.9.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicase@2.9.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.23", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#unindent@0.2.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#untrusted@0.9.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#url@2.5.8", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.2", + "registry+https://github.com/rust-lang/crates.io-index#idna@1.1.0", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf8_iter@1.0.4", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#uuid@1.20.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.3.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.5", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#walkdir@2.5.0", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#want@0.3.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#warp@0.4.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytes@1.11.1", + "registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.31", + "registry+https://github.com/rust-lang/crates.io-index#headers@0.4.1", + "registry+https://github.com/rust-lang/crates.io-index#http@1.4.0", + "registry+https://github.com/rust-lang/crates.io-index#http-body@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#http-body-util@0.1.3", + "registry+https://github.com/rust-lang/crates.io-index#hyper@1.8.1", + "registry+https://github.com/rust-lang/crates.io-index#hyper-util@0.1.20", + "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29", + "registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17", + "registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.5", + "registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.2", + "registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.10", + "registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1", + "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.228", + "registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.149", + "registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1", + "registry+https://github.com/rust-lang/crates.io-index#tokio@1.49.0", + "registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.18", + "registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.3", + "registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.44" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#whoami@2.1.0", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#wide@0.7.33", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.25.0", + "registry+https://github.com/rust-lang/crates.io-index#safe_arch@0.7.4" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#winnow@0.7.14", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.0" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#writeable@0.6.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.8.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114", + "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#stable_deref_trait@1.2.1", + "registry+https://github.com/rust-lang/crates.io-index#yoke-derive@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.39", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.39", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.8.39" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114", + "registry+https://github.com/rust-lang/crates.io-index#synstructure@0.13.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#zerofrom-derive@0.1.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zeroize@1.8.2", + "dependsOn": [] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerotrie@0.2.3", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.5", + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.11.2", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106", + "registry+https://github.com/rust-lang/crates.io-index#quote@1.0.44", + "registry+https://github.com/rust-lang/crates.io-index#syn@2.0.114" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zerovec@0.11.5", + "dependsOn": [ + "registry+https://github.com/rust-lang/crates.io-index#yoke@0.8.1", + "registry+https://github.com/rust-lang/crates.io-index#zerofrom@0.1.6", + "registry+https://github.com/rust-lang/crates.io-index#zerovec-derive@0.11.2" + ] + }, + { + "ref": "registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.20", + "dependsOn": [] + } + ] +} \ No newline at end of file diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5aacc7cf7143a91539b983843b603609adf3d94 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/conftest.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/conftest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49ac18cd52d7b88004d3494561bc5d9f7bd65e3e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/conftest.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c53f9e2e4723c844cf807637908c092b1e750e5e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert_matrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert_matrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f9ec732ed3c8949bdfb0ba63891eaf33fd85301 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/convert_matrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/exception.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/exception.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..286ac38b4b89f3afc2befb776cea7670e304d20d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/exception.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/lazy_imports.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/lazy_imports.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed9e73adcca7f78b9004cfdc8d84e1a0284d33e1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/lazy_imports.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/relabel.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/relabel.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bf3b86142a59a6dd4feb4a4465ca5a467c5246e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/__pycache__/relabel.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6439d56fe1364cf9b08e2a20affe4362cb56cbe8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/__init__.py @@ -0,0 +1,134 @@ +from networkx.algorithms.assortativity import * +from networkx.algorithms.asteroidal import * +from networkx.algorithms.boundary import * +from networkx.algorithms.broadcasting import * +from networkx.algorithms.bridges import * +from networkx.algorithms.chains import * +from networkx.algorithms.centrality import * +from networkx.algorithms.chordal import * +from networkx.algorithms.cluster import * +from networkx.algorithms.clique import * +from networkx.algorithms.communicability_alg import * +from networkx.algorithms.components import * +from networkx.algorithms.coloring import * +from networkx.algorithms.core import * +from networkx.algorithms.covering import * +from networkx.algorithms.cycles import * +from networkx.algorithms.cuts import * +from networkx.algorithms.d_separation import * +from networkx.algorithms.dag import * +from networkx.algorithms.distance_measures import * +from networkx.algorithms.distance_regular import * +from networkx.algorithms.dominance import * +from networkx.algorithms.dominating import * +from networkx.algorithms.efficiency_measures import * +from networkx.algorithms.euler import * +from networkx.algorithms.graphical import * +from networkx.algorithms.hierarchy import * +from networkx.algorithms.hybrid import * +from networkx.algorithms.link_analysis import * +from networkx.algorithms.link_prediction import * +from networkx.algorithms.lowest_common_ancestors import * +from networkx.algorithms.isolate import * +from networkx.algorithms.matching import * +from networkx.algorithms.minors import * +from networkx.algorithms.mis import * +from networkx.algorithms.moral import * +from networkx.algorithms.non_randomness import * +from networkx.algorithms.operators import * +from networkx.algorithms.planarity import * +from networkx.algorithms.planar_drawing import * +from networkx.algorithms.polynomials import * +from networkx.algorithms.perfect_graph import * +from networkx.algorithms.reciprocity import * +from networkx.algorithms.regular import * +from networkx.algorithms.richclub import * +from networkx.algorithms.shortest_paths import * +from networkx.algorithms.similarity import * +from networkx.algorithms.graph_hashing import * +from networkx.algorithms.simple_paths import * +from networkx.algorithms.smallworld import * +from networkx.algorithms.smetric import * +from networkx.algorithms.structuralholes import * +from networkx.algorithms.sparsifiers import * +from networkx.algorithms.summarization import * +from networkx.algorithms.swap import * +from networkx.algorithms.time_dependent import * +from networkx.algorithms.traversal import * +from networkx.algorithms.triads import * +from networkx.algorithms.vitality import * +from networkx.algorithms.voronoi import * +from networkx.algorithms.walks import * +from networkx.algorithms.wiener import * + +# Make certain subpackages available to the user as direct imports from +# the `networkx` namespace. +from networkx.algorithms import approximation +from networkx.algorithms import assortativity +from networkx.algorithms import bipartite +from networkx.algorithms import node_classification +from networkx.algorithms import centrality +from networkx.algorithms import chordal +from networkx.algorithms import cluster +from networkx.algorithms import clique +from networkx.algorithms import components +from networkx.algorithms import connectivity +from networkx.algorithms import community +from networkx.algorithms import coloring +from networkx.algorithms import flow +from networkx.algorithms import isomorphism +from networkx.algorithms import link_analysis +from networkx.algorithms import lowest_common_ancestors +from networkx.algorithms import operators +from networkx.algorithms import shortest_paths +from networkx.algorithms import tournament +from networkx.algorithms import traversal +from networkx.algorithms import tree + +# Make certain functions from some of the previous subpackages available +# to the user as direct imports from the `networkx` namespace. +from networkx.algorithms.bipartite import complete_bipartite_graph +from networkx.algorithms.bipartite import is_bipartite +from networkx.algorithms.bipartite import projected_graph +from networkx.algorithms.connectivity import all_pairs_node_connectivity +from networkx.algorithms.connectivity import all_node_cuts +from networkx.algorithms.connectivity import average_node_connectivity +from networkx.algorithms.connectivity import edge_connectivity +from networkx.algorithms.connectivity import edge_disjoint_paths +from networkx.algorithms.connectivity import k_components +from networkx.algorithms.connectivity import k_edge_components +from networkx.algorithms.connectivity import k_edge_subgraphs +from networkx.algorithms.connectivity import k_edge_augmentation +from networkx.algorithms.connectivity import is_k_edge_connected +from networkx.algorithms.connectivity import minimum_edge_cut +from networkx.algorithms.connectivity import minimum_node_cut +from networkx.algorithms.connectivity import node_connectivity +from networkx.algorithms.connectivity import node_disjoint_paths +from networkx.algorithms.connectivity import stoer_wagner +from networkx.algorithms.flow import capacity_scaling +from networkx.algorithms.flow import cost_of_flow +from networkx.algorithms.flow import gomory_hu_tree +from networkx.algorithms.flow import max_flow_min_cost +from networkx.algorithms.flow import maximum_flow +from networkx.algorithms.flow import maximum_flow_value +from networkx.algorithms.flow import min_cost_flow +from networkx.algorithms.flow import min_cost_flow_cost +from networkx.algorithms.flow import minimum_cut +from networkx.algorithms.flow import minimum_cut_value +from networkx.algorithms.flow import network_simplex +from networkx.algorithms.isomorphism import could_be_isomorphic +from networkx.algorithms.isomorphism import fast_could_be_isomorphic +from networkx.algorithms.isomorphism import faster_could_be_isomorphic +from networkx.algorithms.isomorphism import is_isomorphic +from networkx.algorithms.isomorphism.vf2pp import * +from networkx.algorithms.tree.branchings import maximum_branching +from networkx.algorithms.tree.branchings import maximum_spanning_arborescence +from networkx.algorithms.tree.branchings import minimum_branching +from networkx.algorithms.tree.branchings import minimum_spanning_arborescence +from networkx.algorithms.tree.branchings import ArborescenceIterator +from networkx.algorithms.tree.coding import * +from networkx.algorithms.tree.decomposition import * +from networkx.algorithms.tree.mst import * +from networkx.algorithms.tree.operations import * +from networkx.algorithms.tree.recognition import * +from networkx.algorithms.tournament import is_tournament diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4d9888609cbc43d4ba2121fcd0feda0985d1aebd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/__init__.py @@ -0,0 +1,5 @@ +from networkx.algorithms.assortativity.connectivity import * +from networkx.algorithms.assortativity.correlation import * +from networkx.algorithms.assortativity.mixing import * +from networkx.algorithms.assortativity.neighbor_degree import * +from networkx.algorithms.assortativity.pairs import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..c3fde0da68a1990da29ced6996620d709c52c13d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/connectivity.py @@ -0,0 +1,122 @@ +from collections import defaultdict + +import networkx as nx + +__all__ = ["average_degree_connectivity"] + + +@nx._dispatchable(edge_attrs="weight") +def average_degree_connectivity( + G, source="in+out", target="in+out", nodes=None, weight=None +): + r"""Compute the average degree connectivity of graph. + + The average degree connectivity is the average nearest neighbor degree of + nodes with degree k. For weighted graphs, an analogous measure can + be computed using the weighted average neighbors degree defined in + [1]_, for a node `i`, as + + .. math:: + + k_{nn,i}^{w} = \frac{1}{s_i} \sum_{j \in N(i)} w_{ij} k_j + + where `s_i` is the weighted degree of node `i`, + `w_{ij}` is the weight of the edge that links `i` and `j`, + and `N(i)` are the neighbors of node `i`. + + Parameters + ---------- + G : NetworkX graph + + source : "in"|"out"|"in+out" (default:"in+out") + Directed graphs only. Use "in"- or "out"-degree for source node. + + target : "in"|"out"|"in+out" (default:"in+out" + Directed graphs only. Use "in"- or "out"-degree for target node. + + nodes : list or iterable (optional) + Compute neighbor connectivity for these nodes. The default is all + nodes. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used as a weight. + If None, then each edge has weight 1. + + Returns + ------- + d : dict + A dictionary keyed by degree k with the value of average connectivity. + + Raises + ------ + NetworkXError + If either `source` or `target` are not one of 'in', + 'out', or 'in+out'. + If either `source` or `target` is passed for an undirected graph. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> G.edges[1, 2]["weight"] = 3 + >>> nx.average_degree_connectivity(G) + {1: 2.0, 2: 1.5} + >>> nx.average_degree_connectivity(G, weight="weight") + {1: 2.0, 2: 1.75} + + See Also + -------- + average_neighbor_degree + + References + ---------- + .. [1] A. Barrat, M. Barthélemy, R. Pastor-Satorras, and A. Vespignani, + "The architecture of complex weighted networks". + PNAS 101 (11): 3747–3752 (2004). + """ + # First, determine the type of neighbors and the type of degree to use. + if G.is_directed(): + if source not in ("in", "out", "in+out"): + raise nx.NetworkXError('source must be one of "in", "out", or "in+out"') + if target not in ("in", "out", "in+out"): + raise nx.NetworkXError('target must be one of "in", "out", or "in+out"') + direction = {"out": G.out_degree, "in": G.in_degree, "in+out": G.degree} + neighbor_funcs = { + "out": G.successors, + "in": G.predecessors, + "in+out": G.neighbors, + } + source_degree = direction[source] + target_degree = direction[target] + neighbors = neighbor_funcs[source] + # `reverse` indicates whether to look at the in-edge when + # computing the weight of an edge. + reverse = source == "in" + else: + if source != "in+out" or target != "in+out": + raise nx.NetworkXError( + f"source and target arguments are only supported for directed graphs" + ) + source_degree = G.degree + target_degree = G.degree + neighbors = G.neighbors + reverse = False + dsum = defaultdict(int) + dnorm = defaultdict(int) + # Check if `source_nodes` is actually a single node in the graph. + source_nodes = source_degree(nodes) + if nodes in G: + source_nodes = [(nodes, source_degree(nodes))] + for n, k in source_nodes: + nbrdeg = target_degree(neighbors(n)) + if weight is None: + s = sum(d for n, d in nbrdeg) + else: # weight nbr degree by weight of (n,nbr) edge + if reverse: + s = sum(G[nbr][n].get(weight, 1) * d for nbr, d in nbrdeg) + else: + s = sum(G[n][nbr].get(weight, 1) * d for nbr, d in nbrdeg) + dnorm[k] += source_degree(n, weight=weight) + dsum[k] += s + + # normalize + return {k: avg if dnorm[k] == 0 else avg / dnorm[k] for k, avg in dsum.items()} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..52ae7a12fa9de5705412538fc6bbe873755d9b7a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/correlation.py @@ -0,0 +1,302 @@ +"""Node assortativity coefficients and correlation measures.""" + +import networkx as nx +from networkx.algorithms.assortativity.mixing import ( + attribute_mixing_matrix, + degree_mixing_matrix, +) +from networkx.algorithms.assortativity.pairs import node_degree_xy + +__all__ = [ + "degree_pearson_correlation_coefficient", + "degree_assortativity_coefficient", + "attribute_assortativity_coefficient", + "numeric_assortativity_coefficient", +] + + +@nx._dispatchable(edge_attrs="weight") +def degree_assortativity_coefficient(G, x="out", y="in", weight=None, nodes=None): + """Compute degree assortativity of graph. + + Assortativity measures the similarity of connections + in the graph with respect to the node degree. + + Parameters + ---------- + G : NetworkX graph + + x: string ('in','out') + The degree type for source node (directed graphs only). + + y: string ('in','out') + The degree type for target node (directed graphs only). + + weight: string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + nodes: list or iterable (optional) + Compute degree assortativity only for nodes in container. + The default is all nodes. + + Returns + ------- + r : float + Assortativity of graph by degree. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> r = nx.degree_assortativity_coefficient(G) + >>> print(f"{r:3.1f}") + -0.5 + + See Also + -------- + attribute_assortativity_coefficient + numeric_assortativity_coefficient + degree_mixing_dict + degree_mixing_matrix + + Notes + ----- + This computes Eq. (21) in Ref. [1]_ , where e is the joint + probability distribution (mixing matrix) of the degrees. If G is + directed than the matrix e is the joint probability of the + user-specified degree type for the source and target. + + References + ---------- + .. [1] M. E. J. Newman, Mixing patterns in networks, + Physical Review E, 67 026126, 2003 + .. [2] Foster, J.G., Foster, D.V., Grassberger, P. & Paczuski, M. + Edge direction and the structure of networks, PNAS 107, 10815-20 (2010). + """ + if nodes is None: + nodes = G.nodes + + degrees = None + + if G.is_directed(): + indeg = ( + {d for _, d in G.in_degree(nodes, weight=weight)} + if "in" in (x, y) + else set() + ) + outdeg = ( + {d for _, d in G.out_degree(nodes, weight=weight)} + if "out" in (x, y) + else set() + ) + degrees = set.union(indeg, outdeg) + else: + degrees = {d for _, d in G.degree(nodes, weight=weight)} + + mapping = {d: i for i, d in enumerate(degrees)} + M = degree_mixing_matrix(G, x=x, y=y, nodes=nodes, weight=weight, mapping=mapping) + + return _numeric_ac(M, mapping=mapping) + + +@nx._dispatchable(edge_attrs="weight") +def degree_pearson_correlation_coefficient(G, x="out", y="in", weight=None, nodes=None): + """Compute degree assortativity of graph. + + Assortativity measures the similarity of connections + in the graph with respect to the node degree. + + This is the same as degree_assortativity_coefficient but uses the + potentially faster scipy.stats.pearsonr function. + + Parameters + ---------- + G : NetworkX graph + + x: string ('in','out') + The degree type for source node (directed graphs only). + + y: string ('in','out') + The degree type for target node (directed graphs only). + + weight: string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + nodes: list or iterable (optional) + Compute pearson correlation of degrees only for specified nodes. + The default is all nodes. + + Returns + ------- + r : float + Assortativity of graph by degree. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> r = nx.degree_pearson_correlation_coefficient(G) + >>> print(f"{r:3.1f}") + -0.5 + + Notes + ----- + This calls scipy.stats.pearsonr. + + References + ---------- + .. [1] M. E. J. Newman, Mixing patterns in networks + Physical Review E, 67 026126, 2003 + .. [2] Foster, J.G., Foster, D.V., Grassberger, P. & Paczuski, M. + Edge direction and the structure of networks, PNAS 107, 10815-20 (2010). + """ + import scipy as sp + + xy = node_degree_xy(G, x=x, y=y, nodes=nodes, weight=weight) + x, y = zip(*xy) + return float(sp.stats.pearsonr(x, y)[0]) + + +@nx._dispatchable(node_attrs="attribute") +def attribute_assortativity_coefficient(G, attribute, nodes=None): + """Compute assortativity for node attributes. + + Assortativity measures the similarity of connections + in the graph with respect to the given attribute. + + Parameters + ---------- + G : NetworkX graph + + attribute : string + Node attribute key + + nodes: list or iterable (optional) + Compute attribute assortativity for nodes in container. + The default is all nodes. + + Returns + ------- + r: float + Assortativity of graph for given attribute + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from([0, 1], color="red") + >>> G.add_nodes_from([2, 3], color="blue") + >>> G.add_edges_from([(0, 1), (2, 3)]) + >>> print(nx.attribute_assortativity_coefficient(G, "color")) + 1.0 + + Notes + ----- + This computes Eq. (2) in Ref. [1]_ , (trace(M)-sum(M^2))/(1-sum(M^2)), + where M is the joint probability distribution (mixing matrix) + of the specified attribute. + + References + ---------- + .. [1] M. E. J. Newman, Mixing patterns in networks, + Physical Review E, 67 026126, 2003 + """ + M = attribute_mixing_matrix(G, attribute, nodes) + return attribute_ac(M) + + +@nx._dispatchable(node_attrs="attribute") +def numeric_assortativity_coefficient(G, attribute, nodes=None): + """Compute assortativity for numerical node attributes. + + Assortativity measures the similarity of connections + in the graph with respect to the given numeric attribute. + + Parameters + ---------- + G : NetworkX graph + + attribute : string + Node attribute key. + + nodes: list or iterable (optional) + Compute numeric assortativity only for attributes of nodes in + container. The default is all nodes. + + Returns + ------- + r: float + Assortativity of graph for given attribute + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from([0, 1], size=2) + >>> G.add_nodes_from([2, 3], size=3) + >>> G.add_edges_from([(0, 1), (2, 3)]) + >>> print(nx.numeric_assortativity_coefficient(G, "size")) + 1.0 + + Notes + ----- + This computes Eq. (21) in Ref. [1]_ , which is the Pearson correlation + coefficient of the specified (scalar valued) attribute across edges. + + References + ---------- + .. [1] M. E. J. Newman, Mixing patterns in networks + Physical Review E, 67 026126, 2003 + """ + if nodes is None: + nodes = G.nodes + vals = {G.nodes[n][attribute] for n in nodes} + mapping = {d: i for i, d in enumerate(vals)} + M = attribute_mixing_matrix(G, attribute, nodes, mapping) + return _numeric_ac(M, mapping) + + +def attribute_ac(M): + """Compute assortativity for attribute matrix M. + + Parameters + ---------- + M : numpy.ndarray + 2D ndarray representing the attribute mixing matrix. + + Notes + ----- + This computes Eq. (2) in Ref. [1]_ , (trace(e)-sum(e^2))/(1-sum(e^2)), + where e is the joint probability distribution (mixing matrix) + of the specified attribute. + + References + ---------- + .. [1] M. E. J. Newman, Mixing patterns in networks, + Physical Review E, 67 026126, 2003 + """ + if M.sum() != 1.0: + M = M / M.sum() + s = (M @ M).sum() + t = M.trace() + r = (t - s) / (1 - s) + return float(r) + + +def _numeric_ac(M, mapping): + # M is a 2D numpy array + # numeric assortativity coefficient, pearsonr + import numpy as np + + if M.sum() != 1.0: + M = M / M.sum() + x = np.array(list(mapping.keys())) + y = x # x and y have the same support + idx = list(mapping.values()) + a = M.sum(axis=0) + b = M.sum(axis=1) + vara = (a[idx] * x**2).sum() - ((a[idx] * x).sum()) ** 2 + varb = (b[idx] * y**2).sum() - ((b[idx] * y).sum()) ** 2 + xy = np.outer(x, y) + ab = np.outer(a[idx], b[idx]) + return float((xy * (M - ab)).sum() / np.sqrt(vara * varb)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py new file mode 100644 index 0000000000000000000000000000000000000000..1762d4e56c96624ecb4cccf1f2247f46159a12e4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/mixing.py @@ -0,0 +1,255 @@ +""" +Mixing matrices for node attributes and degree. +""" + +import networkx as nx +from networkx.algorithms.assortativity.pairs import node_attribute_xy, node_degree_xy +from networkx.utils import dict_to_numpy_array + +__all__ = [ + "attribute_mixing_matrix", + "attribute_mixing_dict", + "degree_mixing_matrix", + "degree_mixing_dict", + "mixing_dict", +] + + +@nx._dispatchable(node_attrs="attribute") +def attribute_mixing_dict(G, attribute, nodes=None, normalized=False): + """Returns dictionary representation of mixing matrix for attribute. + + Parameters + ---------- + G : graph + NetworkX graph object. + + attribute : string + Node attribute key. + + nodes: list or iterable (optional) + Unse nodes in container to build the dict. The default is all nodes. + + normalized : bool (default=False) + Return counts if False or probabilities if True. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from([0, 1], color="red") + >>> G.add_nodes_from([2, 3], color="blue") + >>> G.add_edge(1, 3) + >>> d = nx.attribute_mixing_dict(G, "color") + >>> print(d["red"]["blue"]) + 1 + >>> print(d["blue"]["red"]) # d symmetric for undirected graphs + 1 + + Returns + ------- + d : dictionary + Counts or joint probability of occurrence of attribute pairs. + """ + xy_iter = node_attribute_xy(G, attribute, nodes) + return mixing_dict(xy_iter, normalized=normalized) + + +@nx._dispatchable(node_attrs="attribute") +def attribute_mixing_matrix(G, attribute, nodes=None, mapping=None, normalized=True): + """Returns mixing matrix for attribute. + + Parameters + ---------- + G : graph + NetworkX graph object. + + attribute : string + Node attribute key. + + nodes: list or iterable (optional) + Use only nodes in container to build the matrix. The default is + all nodes. + + mapping : dictionary, optional + Mapping from node attribute to integer index in matrix. + If not specified, an arbitrary ordering will be used. + + normalized : bool (default=True) + Return counts if False or probabilities if True. + + Returns + ------- + m: numpy array + Counts or joint probability of occurrence of attribute pairs. + + Notes + ----- + If each node has a unique attribute value, the unnormalized mixing matrix + will be equal to the adjacency matrix. To get a denser mixing matrix, + the rounding can be performed to form groups of nodes with equal values. + For example, the exact height of persons in cm (180.79155222, 163.9080892, + 163.30095355, 167.99016217, 168.21590163, ...) can be rounded to (180, 163, + 163, 168, 168, ...). + + Definitions of attribute mixing matrix vary on whether the matrix + should include rows for attribute values that don't arise. Here we + do not include such empty-rows. But you can force them to appear + by inputting a `mapping` that includes those values. + + Examples + -------- + >>> G = nx.path_graph(3) + >>> gender = {0: "male", 1: "female", 2: "female"} + >>> nx.set_node_attributes(G, gender, "gender") + >>> mapping = {"male": 0, "female": 1} + >>> mix_mat = nx.attribute_mixing_matrix(G, "gender", mapping=mapping) + >>> mix_mat + array([[0. , 0.25], + [0.25, 0.5 ]]) + """ + d = attribute_mixing_dict(G, attribute, nodes) + a = dict_to_numpy_array(d, mapping=mapping) + if normalized: + a = a / a.sum() + return a + + +@nx._dispatchable(edge_attrs="weight") +def degree_mixing_dict(G, x="out", y="in", weight=None, nodes=None, normalized=False): + """Returns dictionary representation of mixing matrix for degree. + + Parameters + ---------- + G : graph + NetworkX graph object. + + x: string ('in','out') + The degree type for source node (directed graphs only). + + y: string ('in','out') + The degree type for target node (directed graphs only). + + weight: string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + normalized : bool (default=False) + Return counts if False or probabilities if True. + + Returns + ------- + d: dictionary + Counts or joint probability of occurrence of degree pairs. + """ + xy_iter = node_degree_xy(G, x=x, y=y, nodes=nodes, weight=weight) + return mixing_dict(xy_iter, normalized=normalized) + + +@nx._dispatchable(edge_attrs="weight") +def degree_mixing_matrix( + G, x="out", y="in", weight=None, nodes=None, normalized=True, mapping=None +): + """Returns mixing matrix for attribute. + + Parameters + ---------- + G : graph + NetworkX graph object. + + x: string ('in','out') + The degree type for source node (directed graphs only). + + y: string ('in','out') + The degree type for target node (directed graphs only). + + nodes: list or iterable (optional) + Build the matrix using only nodes in container. + The default is all nodes. + + weight: string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + normalized : bool (default=True) + Return counts if False or probabilities if True. + + mapping : dictionary, optional + Mapping from node degree to integer index in matrix. + If not specified, an arbitrary ordering will be used. + + Returns + ------- + m: numpy array + Counts, or joint probability, of occurrence of node degree. + + Notes + ----- + Definitions of degree mixing matrix vary on whether the matrix + should include rows for degree values that don't arise. Here we + do not include such empty-rows. But you can force them to appear + by inputting a `mapping` that includes those values. See examples. + + Examples + -------- + >>> G = nx.star_graph(3) + >>> mix_mat = nx.degree_mixing_matrix(G) + >>> mix_mat + array([[0. , 0.5], + [0.5, 0. ]]) + + If you want every possible degree to appear as a row, even if no nodes + have that degree, use `mapping` as follows, + + >>> max_degree = max(deg for n, deg in G.degree) + >>> mapping = {x: x for x in range(max_degree + 1)} # identity mapping + >>> mix_mat = nx.degree_mixing_matrix(G, mapping=mapping) + >>> mix_mat + array([[0. , 0. , 0. , 0. ], + [0. , 0. , 0. , 0.5], + [0. , 0. , 0. , 0. ], + [0. , 0.5, 0. , 0. ]]) + """ + d = degree_mixing_dict(G, x=x, y=y, nodes=nodes, weight=weight) + a = dict_to_numpy_array(d, mapping=mapping) + if normalized: + a = a / a.sum() + return a + + +def mixing_dict(xy, normalized=False): + """Returns a dictionary representation of mixing matrix. + + Parameters + ---------- + xy : list or container of two-tuples + Pairs of (x,y) items. + + attribute : string + Node attribute key + + normalized : bool (default=False) + Return counts if False or probabilities if True. + + Returns + ------- + d: dictionary + Counts or Joint probability of occurrence of values in xy. + """ + d = {} + psum = 0.0 + for x, y in xy: + if x not in d: + d[x] = {} + if y not in d: + d[y] = {} + v = d[x].get(y, 0) + d[x][y] = v + 1 + psum += 1 + + if normalized: + for _, jdict in d.items(): + for j in jdict: + jdict[j] /= psum + return d diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py new file mode 100644 index 0000000000000000000000000000000000000000..6488d041a8bdc93ef3591283781b81bcf7f47dab --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/neighbor_degree.py @@ -0,0 +1,160 @@ +import networkx as nx + +__all__ = ["average_neighbor_degree"] + + +@nx._dispatchable(edge_attrs="weight") +def average_neighbor_degree(G, source="out", target="out", nodes=None, weight=None): + r"""Returns the average degree of the neighborhood of each node. + + In an undirected graph, the neighborhood `N(i)` of node `i` contains the + nodes that are connected to `i` by an edge. + + For directed graphs, `N(i)` is defined according to the parameter `source`: + + - if source is 'in', then `N(i)` consists of predecessors of node `i`. + - if source is 'out', then `N(i)` consists of successors of node `i`. + - if source is 'in+out', then `N(i)` is both predecessors and successors. + + The average neighborhood degree of a node `i` is + + .. math:: + + k_{nn,i} = \frac{1}{|N(i)|} \sum_{j \in N(i)} k_j + + where `N(i)` are the neighbors of node `i` and `k_j` is + the degree of node `j` which belongs to `N(i)`. For weighted + graphs, an analogous measure can be defined [1]_, + + .. math:: + + k_{nn,i}^{w} = \frac{1}{s_i} \sum_{j \in N(i)} w_{ij} k_j + + where `s_i` is the weighted degree of node `i`, `w_{ij}` + is the weight of the edge that links `i` and `j` and + `N(i)` are the neighbors of node `i`. + + + Parameters + ---------- + G : NetworkX graph + + source : string ("in"|"out"|"in+out"), optional (default="out") + Directed graphs only. + Use "in"- or "out"-neighbors of source node. + + target : string ("in"|"out"|"in+out"), optional (default="out") + Directed graphs only. + Use "in"- or "out"-degree for target node. + + nodes : list or iterable, optional (default=G.nodes) + Compute neighbor degree only for specified nodes. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used as a weight. + If None, then each edge has weight 1. + + Returns + ------- + d: dict + A dictionary keyed by node to the average degree of its neighbors. + + Raises + ------ + NetworkXError + If either `source` or `target` are not one of 'in', 'out', or 'in+out'. + If either `source` or `target` is passed for an undirected graph. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> G.edges[0, 1]["weight"] = 5 + >>> G.edges[2, 3]["weight"] = 3 + + >>> nx.average_neighbor_degree(G) + {0: 2.0, 1: 1.5, 2: 1.5, 3: 2.0} + >>> nx.average_neighbor_degree(G, weight="weight") + {0: 2.0, 1: 1.1666666666666667, 2: 1.25, 3: 2.0} + + >>> G = nx.DiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> nx.average_neighbor_degree(G, source="in", target="in") + {0: 0.0, 1: 0.0, 2: 1.0, 3: 1.0} + + >>> nx.average_neighbor_degree(G, source="out", target="out") + {0: 1.0, 1: 1.0, 2: 0.0, 3: 0.0} + + See Also + -------- + average_degree_connectivity + + References + ---------- + .. [1] A. Barrat, M. Barthélemy, R. Pastor-Satorras, and A. Vespignani, + "The architecture of complex weighted networks". + PNAS 101 (11): 3747–3752 (2004). + """ + if G.is_directed(): + if source == "in": + source_degree = G.in_degree + elif source == "out": + source_degree = G.out_degree + elif source == "in+out": + source_degree = G.degree + else: + raise nx.NetworkXError( + f"source argument {source} must be 'in', 'out' or 'in+out'" + ) + + if target == "in": + target_degree = G.in_degree + elif target == "out": + target_degree = G.out_degree + elif target == "in+out": + target_degree = G.degree + else: + raise nx.NetworkXError( + f"target argument {target} must be 'in', 'out' or 'in+out'" + ) + else: + if source != "out" or target != "out": + raise nx.NetworkXError( + f"source and target arguments are only supported for directed graphs" + ) + source_degree = target_degree = G.degree + + # precompute target degrees -- should *not* be weighted degree + t_deg = dict(target_degree()) + + # Set up both predecessor and successor neighbor dicts leaving empty if not needed + G_P = G_S = {n: {} for n in G} + if G.is_directed(): + # "in" or "in+out" cases: G_P contains predecessors + if "in" in source: + G_P = G.pred + # "out" or "in+out" cases: G_S contains successors + if "out" in source: + G_S = G.succ + else: + # undirected leave G_P empty but G_S is the adjacency + G_S = G.adj + + # Main loop: Compute average degree of neighbors + avg = {} + for n, deg in source_degree(nodes, weight=weight): + # handle degree zero average + if deg == 0: + avg[n] = 0.0 + continue + + # we sum over both G_P and G_S, but one of the two is usually empty. + if weight is None: + avg[n] = ( + sum(t_deg[nbr] for nbr in G_S[n]) + sum(t_deg[nbr] for nbr in G_P[n]) + ) / deg + else: + avg[n] = ( + sum(dd.get(weight, 1) * t_deg[nbr] for nbr, dd in G_S[n].items()) + + sum(dd.get(weight, 1) * t_deg[nbr] for nbr, dd in G_P[n].items()) + ) / deg + return avg diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py new file mode 100644 index 0000000000000000000000000000000000000000..ea5fd287545c80dd2ebbb2b253d5ab0ab7480743 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/assortativity/pairs.py @@ -0,0 +1,127 @@ +"""Generators of x-y pairs of node data.""" + +import networkx as nx + +__all__ = ["node_attribute_xy", "node_degree_xy"] + + +@nx._dispatchable(node_attrs="attribute") +def node_attribute_xy(G, attribute, nodes=None): + """Yields 2-tuples of node attribute values for all edges in `G`. + + This generator yields, for each edge in `G` incident to a node in `nodes`, + a 2-tuple of form ``(attribute value, attribute value)`` for the parameter + specified node-attribute. + + Parameters + ---------- + G: NetworkX graph + + attribute: key + The node attribute key. + + nodes: list or iterable (optional) + Use only edges that are incident to specified nodes. + The default is all nodes. + + Yields + ------ + (x, y): 2-tuple + Generates 2-tuple of (attribute, attribute) values. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_node(1, color="red") + >>> G.add_node(2, color="blue") + >>> G.add_node(3, color="green") + >>> G.add_edge(1, 2) + >>> list(nx.node_attribute_xy(G, "color")) + [('red', 'blue')] + + Notes + ----- + For undirected graphs, each edge is produced twice, once for each edge + representation (u, v) and (v, u), with the exception of self-loop edges + which only appear once. + """ + if nodes is None: + nodes = set(G) + else: + nodes = set(nodes) + Gnodes = G.nodes + for u, nbrsdict in G.adjacency(): + if u not in nodes: + continue + uattr = Gnodes[u].get(attribute, None) + if G.is_multigraph(): + for v, keys in nbrsdict.items(): + vattr = Gnodes[v].get(attribute, None) + for _ in keys: + yield (uattr, vattr) + else: + for v in nbrsdict: + vattr = Gnodes[v].get(attribute, None) + yield (uattr, vattr) + + +@nx._dispatchable(edge_attrs="weight") +def node_degree_xy(G, x="out", y="in", weight=None, nodes=None): + """Yields 2-tuples of ``(degree, degree)`` values for edges in `G`. + + This generator yields, for each edge in `G` incident to a node in `nodes`, + a 2-tuple of form ``(degree, degree)``. The node degrees are weighted + when a `weight` attribute is specified. + + Parameters + ---------- + G: NetworkX graph + + x: string ('in','out') + The degree type for source node (directed graphs only). + + y: string ('in','out') + The degree type for target node (directed graphs only). + + weight: string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + nodes: list or iterable (optional) + Use only edges that are adjacency to specified nodes. + The default is all nodes. + + Yields + ------ + (x, y): 2-tuple + Generates 2-tuple of (degree, degree) values. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge(1, 2) + >>> list(nx.node_degree_xy(G, x="out", y="in")) + [(1, 1)] + >>> list(nx.node_degree_xy(G, x="in", y="out")) + [(0, 0)] + + Notes + ----- + For undirected graphs, each edge is produced twice, once for each edge + representation (u, v) and (v, u), with the exception of self-loop edges + which only appear once. + """ + nodes = set(G) if nodes is None else set(nodes) + if G.is_directed(): + direction = {"out": G.out_degree, "in": G.in_degree} + xdeg = direction[x] + ydeg = direction[y] + else: + xdeg = ydeg = G.degree + + for u, degu in xdeg(nodes, weight=weight): + # use G.edges to treat multigraphs correctly + neighbors = (nbr for _, nbr in G.edges(u) if nbr in nodes) + for _, degv in ydeg(neighbors, weight=weight): + yield degu, degv diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/asteroidal.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/asteroidal.py new file mode 100644 index 0000000000000000000000000000000000000000..b308392a48626ade3b964aa544373e29d8a3a22b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/asteroidal.py @@ -0,0 +1,164 @@ +""" +Algorithms for asteroidal triples and asteroidal numbers in graphs. + +An asteroidal triple in a graph G is a set of three non-adjacent vertices +u, v and w such that there exist a path between any two of them that avoids +closed neighborhood of the third. More formally, v_j, v_k belongs to the same +connected component of G - N[v_i], where N[v_i] denotes the closed neighborhood +of v_i. A graph which does not contain any asteroidal triples is called +an AT-free graph. The class of AT-free graphs is a graph class for which +many NP-complete problems are solvable in polynomial time. Amongst them, +independent set and coloring. +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["is_at_free", "find_asteroidal_triple"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def find_asteroidal_triple(G): + r"""Find an asteroidal triple in the given graph. + + An asteroidal triple is a triple of non-adjacent vertices such that + there exists a path between any two of them which avoids the closed + neighborhood of the third. It checks all independent triples of vertices + and whether they are an asteroidal triple or not. This is done with the + help of a data structure called a component structure. + A component structure encodes information about which vertices belongs to + the same connected component when the closed neighborhood of a given vertex + is removed from the graph. The algorithm used to check is the trivial + one, outlined in [1]_, which has a runtime of + :math:`O(|V||\overline{E} + |V||E|)`, where the second term is the + creation of the component structure. + + Parameters + ---------- + G : NetworkX Graph + The graph to check whether is AT-free or not + + Returns + ------- + list or None + An asteroidal triple is returned as a list of nodes. If no asteroidal + triple exists, i.e. the graph is AT-free, then None is returned. + + Notes + ----- + The component structure and the algorithm is described in [1]_. The current + implementation implements the trivial algorithm for simple graphs. + + References + ---------- + .. [1] Ekkehard Köhler, + "Recognizing Graphs without asteroidal triples", + Journal of Discrete Algorithms 2, pages 439-452, 2004. + https://www.sciencedirect.com/science/article/pii/S157086670400019X + """ + V = set(G.nodes) + + if len(V) < 6: + # An asteroidal triple cannot exist in a graph with 5 or less vertices. + return None + + component_structure = create_component_structure(G) + + for u, v in nx.non_edges(G): + u_neighborhood = set(G[u]).union([u]) + v_neighborhood = set(G[v]).union([v]) + union_of_neighborhoods = u_neighborhood.union(v_neighborhood) + for w in V - union_of_neighborhoods: + # Check for each pair of vertices whether they belong to the + # same connected component when the closed neighborhood of the + # third is removed. + if ( + component_structure[u][v] == component_structure[u][w] + and component_structure[v][u] == component_structure[v][w] + and component_structure[w][u] == component_structure[w][v] + ): + return [u, v, w] + return None + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_at_free(G): + """Check if a graph is AT-free. + + The method uses the `find_asteroidal_triple` method to recognize + an AT-free graph. If no asteroidal triple is found the graph is + AT-free and True is returned. If at least one asteroidal triple is + found the graph is not AT-free and False is returned. + + Parameters + ---------- + G : NetworkX Graph + The graph to check whether is AT-free or not. + + Returns + ------- + bool + True if G is AT-free and False otherwise. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)]) + >>> nx.is_at_free(G) + True + + >>> G = nx.cycle_graph(6) + >>> nx.is_at_free(G) + False + """ + return find_asteroidal_triple(G) is None + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def create_component_structure(G): + r"""Create component structure for G. + + A *component structure* is an `nxn` array, denoted `c`, where `n` is + the number of vertices, where each row and column corresponds to a vertex. + + .. math:: + c_{uv} = \begin{cases} 0, if v \in N[u] \\ + k, if v \in component k of G \setminus N[u] \end{cases} + + Where `k` is an arbitrary label for each component. The structure is used + to simplify the detection of asteroidal triples. + + Parameters + ---------- + G : NetworkX Graph + Undirected, simple graph. + + Returns + ------- + component_structure : dictionary + A dictionary of dictionaries, keyed by pairs of vertices. + + """ + V = set(G.nodes) + component_structure = {} + for v in V: + label = 0 + closed_neighborhood = set(G[v]).union({v}) + row_dict = {} + for u in closed_neighborhood: + row_dict[u] = 0 + + G_reduced = G.subgraph(set(G.nodes) - closed_neighborhood) + for cc in nx.connected_components(G_reduced): + label += 1 + for u in cc: + row_dict[u] = label + + component_structure[v] = row_dict + + return component_structure diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/boundary.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/boundary.py new file mode 100644 index 0000000000000000000000000000000000000000..ba05d803037d8812bfff83df5382e8ea942711b2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/boundary.py @@ -0,0 +1,168 @@ +"""Routines to find the boundary of a set of nodes. + +An edge boundary is a set of edges, each of which has exactly one +endpoint in a given set of nodes (or, in the case of directed graphs, +the set of edges whose source node is in the set). + +A node boundary of a set *S* of nodes is the set of (out-)neighbors of +nodes in *S* that are outside *S*. + +""" + +from itertools import chain + +import networkx as nx + +__all__ = ["edge_boundary", "node_boundary"] + + +@nx._dispatchable(edge_attrs={"data": "default"}, preserve_edge_attrs="data") +def edge_boundary(G, nbunch1, nbunch2=None, data=False, keys=False, default=None): + """Returns the edge boundary of `nbunch1`. + + The *edge boundary* of a set *S* with respect to a set *T* is the + set of edges (*u*, *v*) such that *u* is in *S* and *v* is in *T*. + If *T* is not specified, it is assumed to be the set of all nodes + not in *S*. + + Parameters + ---------- + G : NetworkX graph + + nbunch1 : iterable + Iterable of nodes in the graph representing the set of nodes + whose edge boundary will be returned. (This is the set *S* from + the definition above.) + + nbunch2 : iterable + Iterable of nodes representing the target (or "exterior") set of + nodes. (This is the set *T* from the definition above.) If not + specified, this is assumed to be the set of all nodes in `G` + not in `nbunch1`. + + keys : bool + This parameter has the same meaning as in + :meth:`MultiGraph.edges`. + + data : bool or object + This parameter has the same meaning as in + :meth:`MultiGraph.edges`. + + default : object + This parameter has the same meaning as in + :meth:`MultiGraph.edges`. + + Returns + ------- + iterator + An iterator over the edges in the boundary of `nbunch1` with + respect to `nbunch2`. If `keys`, `data`, or `default` + are specified and `G` is a multigraph, then edges are returned + with keys and/or data, as in :meth:`MultiGraph.edges`. + + Examples + -------- + >>> G = nx.wheel_graph(6) + + When nbunch2=None: + + >>> list(nx.edge_boundary(G, (1, 3))) + [(1, 0), (1, 2), (1, 5), (3, 0), (3, 2), (3, 4)] + + When nbunch2 is given: + + >>> list(nx.edge_boundary(G, (1, 3), (2, 0))) + [(1, 0), (1, 2), (3, 0), (3, 2)] + + Notes + ----- + Any element of `nbunch` that is not in the graph `G` will be + ignored. + + `nbunch1` and `nbunch2` are usually meant to be disjoint, but in + the interest of speed and generality, that is not required here. + + """ + nset1 = {n for n in nbunch1 if n in G} + # Here we create an iterator over edges incident to nodes in the set + # `nset1`. The `Graph.edges()` method does not provide a guarantee + # on the orientation of the edges, so our algorithm below must + # handle the case in which exactly one orientation, either (u, v) or + # (v, u), appears in this iterable. + if G.is_multigraph(): + edges = G.edges(nset1, data=data, keys=keys, default=default) + else: + edges = G.edges(nset1, data=data, default=default) + # If `nbunch2` is not provided, then it is assumed to be the set + # complement of `nbunch1`. For the sake of efficiency, this is + # implemented by using the `not in` operator, instead of by creating + # an additional set and using the `in` operator. + if nbunch2 is None: + return (e for e in edges if (e[0] in nset1) ^ (e[1] in nset1)) + nset2 = set(nbunch2) + return ( + e + for e in edges + if (e[0] in nset1 and e[1] in nset2) or (e[1] in nset1 and e[0] in nset2) + ) + + +@nx._dispatchable +def node_boundary(G, nbunch1, nbunch2=None): + """Returns the node boundary of `nbunch1`. + + The *node boundary* of a set *S* with respect to a set *T* is the + set of nodes *v* in *T* such that for some *u* in *S*, there is an + edge joining *u* to *v*. If *T* is not specified, it is assumed to + be the set of all nodes not in *S*. + + Parameters + ---------- + G : NetworkX graph + + nbunch1 : iterable + Iterable of nodes in the graph representing the set of nodes + whose node boundary will be returned. (This is the set *S* from + the definition above.) + + nbunch2 : iterable + Iterable of nodes representing the target (or "exterior") set of + nodes. (This is the set *T* from the definition above.) If not + specified, this is assumed to be the set of all nodes in `G` + not in `nbunch1`. + + Returns + ------- + set + The node boundary of `nbunch1` with respect to `nbunch2`. + + Examples + -------- + >>> G = nx.wheel_graph(6) + + When nbunch2=None: + + >>> list(nx.node_boundary(G, (3, 4))) + [0, 2, 5] + + When nbunch2 is given: + + >>> list(nx.node_boundary(G, (3, 4), (0, 1, 5))) + [0, 5] + + Notes + ----- + Any element of `nbunch` that is not in the graph `G` will be + ignored. + + `nbunch1` and `nbunch2` are usually meant to be disjoint, but in + the interest of speed and generality, that is not required here. + + """ + nset1 = {n for n in nbunch1 if n in G} + bdy = set(chain.from_iterable(G[v] for v in nset1)) - nset1 + # If `nbunch2` is not specified, it is assumed to be the set + # complement of `nbunch1`. + if nbunch2 is not None: + bdy &= set(nbunch2) + return bdy diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/bridges.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/bridges.py new file mode 100644 index 0000000000000000000000000000000000000000..eaa6fd3bd7ef881abf93682315b76dc3b11e40ce --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/bridges.py @@ -0,0 +1,205 @@ +"""Bridge-finding algorithms.""" + +from itertools import chain + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["bridges", "has_bridges", "local_bridges"] + + +@not_implemented_for("directed") +@nx._dispatchable +def bridges(G, root=None): + """Generate all bridges in a graph. + + A *bridge* in a graph is an edge whose removal causes the number of + connected components of the graph to increase. Equivalently, a bridge is an + edge that does not belong to any cycle. Bridges are also known as cut-edges, + isthmuses, or cut arcs. + + Parameters + ---------- + G : undirected graph + + root : node (optional) + A node in the graph `G`. If specified, only the bridges in the + connected component containing this node will be returned. + + Yields + ------ + e : edge + An edge in the graph whose removal disconnects the graph (or + causes the number of connected components to increase). + + Raises + ------ + NodeNotFound + If `root` is not in the graph `G`. + + NetworkXNotImplemented + If `G` is a directed graph. + + Examples + -------- + The barbell graph with parameter zero has a single bridge: + + >>> G = nx.barbell_graph(10, 0) + >>> list(nx.bridges(G)) + [(9, 10)] + + Notes + ----- + This is an implementation of the algorithm described in [1]_. An edge is a + bridge if and only if it is not contained in any chain. Chains are found + using the :func:`networkx.chain_decomposition` function. + + The algorithm described in [1]_ requires a simple graph. If the provided + graph is a multigraph, we convert it to a simple graph and verify that any + bridges discovered by the chain decomposition algorithm are not multi-edges. + + Ignoring polylogarithmic factors, the worst-case time complexity is the + same as the :func:`networkx.chain_decomposition` function, + $O(m + n)$, where $n$ is the number of nodes in the graph and $m$ is + the number of edges. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29#Bridge-Finding_with_Chain_Decompositions + """ + multigraph = G.is_multigraph() + H = nx.Graph(G) if multigraph else G + chains = nx.chain_decomposition(H, root=root) + chain_edges = set(chain.from_iterable(chains)) + if root is not None: + H = H.subgraph(nx.node_connected_component(H, root)).copy() + for u, v in H.edges(): + if (u, v) not in chain_edges and (v, u) not in chain_edges: + if multigraph and len(G[u][v]) > 1: + continue + yield u, v + + +@not_implemented_for("directed") +@nx._dispatchable +def has_bridges(G, root=None): + """Decide whether a graph has any bridges. + + A *bridge* in a graph is an edge whose removal causes the number of + connected components of the graph to increase. + + Parameters + ---------- + G : undirected graph + + root : node (optional) + A node in the graph `G`. If specified, only the bridges in the + connected component containing this node will be considered. + + Returns + ------- + bool + Whether the graph (or the connected component containing `root`) + has any bridges. + + Raises + ------ + NodeNotFound + If `root` is not in the graph `G`. + + NetworkXNotImplemented + If `G` is a directed graph. + + Examples + -------- + The barbell graph with parameter zero has a single bridge:: + + >>> G = nx.barbell_graph(10, 0) + >>> nx.has_bridges(G) + True + + On the other hand, the cycle graph has no bridges:: + + >>> G = nx.cycle_graph(5) + >>> nx.has_bridges(G) + False + + Notes + ----- + This implementation uses the :func:`networkx.bridges` function, so + it shares its worst-case time complexity, $O(m + n)$, ignoring + polylogarithmic factors, where $n$ is the number of nodes in the + graph and $m$ is the number of edges. + + """ + try: + next(bridges(G, root=root)) + except StopIteration: + return False + else: + return True + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def local_bridges(G, with_span=True, weight=None): + """Iterate over local bridges of `G` optionally computing the span + + A *local bridge* is an edge whose endpoints have no common neighbors. + That is, the edge is not part of a triangle in the graph. + + The *span* of a *local bridge* is the shortest path length between + the endpoints if the local bridge is removed. + + Parameters + ---------- + G : undirected graph + + with_span : bool + If True, yield a 3-tuple `(u, v, span)` + + weight : function, string or None (default: None) + If function, used to compute edge weights for the span. + If string, the edge data attribute used in calculating span. + If None, all edges have weight 1. + + Yields + ------ + e : edge + The local bridges as an edge 2-tuple of nodes `(u, v)` or + as a 3-tuple `(u, v, span)` when `with_span is True`. + + Raises + ------ + NetworkXNotImplemented + If `G` is a directed graph or multigraph. + + Examples + -------- + A cycle graph has every edge a local bridge with span N-1. + + >>> G = nx.cycle_graph(9) + >>> (0, 8, 8) in set(nx.local_bridges(G)) + True + """ + if with_span is not True: + for u, v in G.edges: + if not (set(G[u]) & set(G[v])): + yield u, v + else: + wt = nx.weighted._weight_function(G, weight) + for u, v in G.edges: + if not (set(G[u]) & set(G[v])): + enodes = {u, v} + + def hide_edge(n, nbr, d): + if n not in enodes or nbr not in enodes: + return wt(n, nbr, d) + return None + + try: + span = nx.shortest_path_length(G, u, v, weight=hide_edge) + yield u, v, span + except nx.NetworkXNoPath: + yield u, v, float("inf") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/broadcasting.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/broadcasting.py new file mode 100644 index 0000000000000000000000000000000000000000..c2e2718a5a3dc549a09dc72469462f80f8c0af77 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/broadcasting.py @@ -0,0 +1,164 @@ +"""Routines to calculate the broadcast time of certain graphs. + +Broadcasting is an information dissemination problem in which a node in a graph, +called the originator, must distribute a message to all other nodes by placing +a series of calls along the edges of the graph. Once informed, other nodes aid +the originator in distributing the message. + +The broadcasting must be completed as quickly as possible subject to the +following constraints: +- Each call requires one unit of time. +- A node can only participate in one call per unit of time. +- Each call only involves two adjacent nodes: a sender and a receiver. +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "tree_broadcast_center", + "tree_broadcast_time", +] + + +def _get_max_broadcast_value(G, U, v, values): + adj = sorted(set(G.neighbors(v)) & U, key=values.get, reverse=True) + return max(values[u] + i for i, u in enumerate(adj, start=1)) + + +def _get_broadcast_centers(G, v, values, target): + adj = sorted(G.neighbors(v), key=values.get, reverse=True) + j = next(i for i, u in enumerate(adj, start=1) if values[u] + i == target) + return set([v] + adj[:j]) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def tree_broadcast_center(G): + """Return the broadcast center of a tree. + + The broadcast center of a graph `G` denotes the set of nodes having + minimum broadcast time [1]_. This function implements a linear algorithm + for determining the broadcast center of a tree with ``n`` nodes. As a + by-product, it also determines the broadcast time from the broadcast center. + + Parameters + ---------- + G : Graph + The graph should be an undirected tree. + + Returns + ------- + b_T, b_C : (int, set) tuple + Minimum broadcast time of the broadcast center in `G`, set of nodes + in the broadcast center. + + Raises + ------ + NetworkXNotImplemented + If `G` is directed or is a multigraph. + + NotATree + If `G` is not a tree. + + References + ---------- + .. [1] Slater, P.J., Cockayne, E.J., Hedetniemi, S.T, + Information dissemination in trees. SIAM J.Comput. 10(4), 692–701 (1981) + """ + # Assert that the graph G is a tree + if not nx.is_tree(G): + raise nx.NotATree("G is not a tree") + # step 0 + if (n := len(G)) < 3: + return n - 1, set(G) + + # step 1 + U = {node for node, deg in G.degree if deg == 1} + values = {n: 0 for n in U} + T = G.copy() + T.remove_nodes_from(U) + + # step 2 + W = {node for node, deg in T.degree if deg == 1} + values.update((w, G.degree[w] - 1) for w in W) + + # step 3 + while len(T) >= 2: + # step 4 + w = min(W, key=values.get) + v = next(T.neighbors(w)) + + # step 5 + U.add(w) + W.remove(w) + T.remove_node(w) + + # step 6 + if T.degree(v) == 1: + # update t(v) + values.update({v: _get_max_broadcast_value(G, U, v, values)}) + W.add(v) + + # step 7 + v = nx.utils.arbitrary_element(T) + b_T = _get_max_broadcast_value(G, U, v, values) + return b_T, _get_broadcast_centers(G, v, values, b_T) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def tree_broadcast_time(G, node=None): + """Return the minimum broadcast time of a (node in a) tree. + + The minimum broadcast time of a node is defined as the minimum amount + of time required to complete broadcasting starting from that node. + The broadcast time of a graph is the maximum over + all nodes of the minimum broadcast time from that node [1]_. + This function returns the minimum broadcast time of `node`. + If `node` is `None`, the broadcast time for the graph is returned. + + Parameters + ---------- + G : Graph + The graph should be an undirected tree. + + node : node, optional (default=None) + Starting node for the broadcasting. If `None`, the algorithm + returns the broadcast time of the graph instead. + + Returns + ------- + int + Minimum broadcast time of `node` in `G`, or broadcast time of `G` + if no node is provided. + + Raises + ------ + NetworkXNotImplemented + If `G` is directed or is a multigraph. + + NodeNotFound + If `node` is not a node in `G`. + + NotATree + If `G` is not a tree. + + References + ---------- + .. [1] Harutyunyan, H. A. and Li, Z. + "A Simple Construction of Broadcast Graphs." + In Computing and Combinatorics. COCOON 2019 + (Ed. D. Z. Du and C. Tian.) Springer, pp. 240-253, 2019. + """ + if node is not None and node not in G: + err = f"node {node} not in G" + raise nx.NodeNotFound(err) + b_T, b_C = tree_broadcast_center(G) + if node is None: + return b_T + sum(1 for _ in nx.bfs_layers(G, b_C)) - 1 + return b_T + next( + d for d, layer in enumerate(nx.bfs_layers(G, b_C)) if node in layer + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chains.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chains.py new file mode 100644 index 0000000000000000000000000000000000000000..ae342d9c8669acd832a3bdb4fe8eecf3e300464f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chains.py @@ -0,0 +1,172 @@ +"""Functions for finding chains in a graph.""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["chain_decomposition"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def chain_decomposition(G, root=None): + """Returns the chain decomposition of a graph. + + The *chain decomposition* of a graph with respect a depth-first + search tree is a set of cycles or paths derived from the set of + fundamental cycles of the tree in the following manner. Consider + each fundamental cycle with respect to the given tree, represented + as a list of edges beginning with the nontree edge oriented away + from the root of the tree. For each fundamental cycle, if it + overlaps with any previous fundamental cycle, just take the initial + non-overlapping segment, which is a path instead of a cycle. Each + cycle or path is called a *chain*. For more information, see [1]_. + + Parameters + ---------- + G : undirected graph + + root : node (optional) + A node in the graph `G`. If specified, only the chain + decomposition for the connected component containing this node + will be returned. This node indicates the root of the depth-first + search tree. + + Yields + ------ + chain : list + A list of edges representing a chain. There is no guarantee on + the orientation of the edges in each chain (for example, if a + chain includes the edge joining nodes 1 and 2, the chain may + include either (1, 2) or (2, 1)). + + Raises + ------ + NodeNotFound + If `root` is not in the graph `G`. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> list(nx.chain_decomposition(G)) + [[(4, 5), (5, 3), (3, 4)]] + + Notes + ----- + The worst-case running time of this implementation is linear in the + number of nodes and number of edges [1]_. + + References + ---------- + .. [1] Jens M. Schmidt (2013). "A simple test on 2-vertex- + and 2-edge-connectivity." *Information Processing Letters*, + 113, 241–244. Elsevier. + + """ + + def _dfs_cycle_forest(G, root=None): + """Builds a directed graph composed of cycles from the given graph. + + `G` is an undirected simple graph. `root` is a node in the graph + from which the depth-first search is started. + + This function returns both the depth-first search cycle graph + (as a :class:`~networkx.DiGraph`) and the list of nodes in + depth-first preorder. The depth-first search cycle graph is a + directed graph whose edges are the edges of `G` oriented toward + the root if the edge is a tree edge and away from the root if + the edge is a non-tree edge. If `root` is not specified, this + performs a depth-first search on each connected component of `G` + and returns a directed forest instead. + + If `root` is not in the graph, this raises :exc:`KeyError`. + + """ + # Create a directed graph from the depth-first search tree with + # root node `root` in which tree edges are directed toward the + # root and nontree edges are directed away from the root. For + # each node with an incident nontree edge, this creates a + # directed cycle starting with the nontree edge and returning to + # that node. + # + # The `parent` node attribute stores the parent of each node in + # the DFS tree. The `nontree` edge attribute indicates whether + # the edge is a tree edge or a nontree edge. + # + # We also store the order of the nodes found in the depth-first + # search in the `nodes` list. + H = nx.DiGraph() + nodes = [] + for u, v, d in nx.dfs_labeled_edges(G, source=root): + if d == "forward": + # `dfs_labeled_edges()` yields (root, root, 'forward') + # if it is beginning the search on a new connected + # component. + if u == v: + H.add_node(v, parent=None) + nodes.append(v) + else: + H.add_node(v, parent=u) + H.add_edge(v, u, nontree=False) + nodes.append(v) + # `dfs_labeled_edges` considers nontree edges in both + # orientations, so we need to not add the edge if it its + # other orientation has been added. + elif d == "nontree" and v not in H[u]: + H.add_edge(v, u, nontree=True) + else: + # Do nothing on 'reverse' edges; we only care about + # forward and nontree edges. + pass + return H, nodes + + def _build_chain(G, u, v, visited): + """Generate the chain starting from the given nontree edge. + + `G` is a DFS cycle graph as constructed by + :func:`_dfs_cycle_graph`. The edge (`u`, `v`) is a nontree edge + that begins a chain. `visited` is a set representing the nodes + in `G` that have already been visited. + + This function yields the edges in an initial segment of the + fundamental cycle of `G` starting with the nontree edge (`u`, + `v`) that includes all the edges up until the first node that + appears in `visited`. The tree edges are given by the 'parent' + node attribute. The `visited` set is updated to add each node in + an edge yielded by this function. + + """ + while v not in visited: + yield u, v + visited.add(v) + u, v = v, G.nodes[v]["parent"] + yield u, v + + # Check if the root is in the graph G. If not, raise NodeNotFound + if root is not None and root not in G: + raise nx.NodeNotFound(f"Root node {root} is not in graph") + + # Create a directed version of H that has the DFS edges directed + # toward the root and the nontree edges directed away from the root + # (in each connected component). + H, nodes = _dfs_cycle_forest(G, root) + + # Visit the nodes again in DFS order. For each node, and for each + # nontree edge leaving that node, compute the fundamental cycle for + # that nontree edge starting with that edge. If the fundamental + # cycle overlaps with any visited nodes, just take the prefix of the + # cycle up to the point of visited nodes. + # + # We repeat this process for each connected component (implicitly, + # since `nodes` already has a list of the nodes grouped by connected + # component). + visited = set() + for u in nodes: + visited.add(u) + # For each nontree edge going out of node u... + edges = ((u, v) for u, v, d in H.out_edges(u, data="nontree") if d) + for u, v in edges: + # Create the cycle or cycle prefix starting with the + # nontree edge. + chain = list(_build_chain(H, u, v, visited)) + yield chain diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chordal.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chordal.py new file mode 100644 index 0000000000000000000000000000000000000000..ab71c243f314d02b74eac9a7b0b4e601ed7e484d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/chordal.py @@ -0,0 +1,443 @@ +""" +Algorithms for chordal graphs. + +A graph is chordal if every cycle of length at least 4 has a chord +(an edge joining two nodes not adjacent in the cycle). +https://en.wikipedia.org/wiki/Chordal_graph +""" + +import sys + +import networkx as nx +from networkx.algorithms.components import connected_components +from networkx.utils import arbitrary_element, not_implemented_for + +__all__ = [ + "is_chordal", + "find_induced_nodes", + "chordal_graph_cliques", + "chordal_graph_treewidth", + "NetworkXTreewidthBoundExceeded", + "complete_to_chordal_graph", +] + + +class NetworkXTreewidthBoundExceeded(nx.NetworkXException): + """Exception raised when a treewidth bound has been provided and it has + been exceeded""" + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_chordal(G): + """Checks whether G is a chordal graph. + + A graph is chordal if every cycle of length at least 4 has a chord + (an edge joining two nodes not adjacent in the cycle). + + Parameters + ---------- + G : graph + A NetworkX graph. + + Returns + ------- + chordal : bool + True if G is a chordal graph and False otherwise. + + Raises + ------ + NetworkXNotImplemented + The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. + + Examples + -------- + >>> e = [ + ... (1, 2), + ... (1, 3), + ... (2, 3), + ... (2, 4), + ... (3, 4), + ... (3, 5), + ... (3, 6), + ... (4, 5), + ... (4, 6), + ... (5, 6), + ... ] + >>> G = nx.Graph(e) + >>> nx.is_chordal(G) + True + + Notes + ----- + The routine tries to go through every node following maximum cardinality + search. It returns False when it finds that the separator for any node + is not a clique. Based on the algorithms in [1]_. + + Self loops are ignored. + + References + ---------- + .. [1] R. E. Tarjan and M. Yannakakis, Simple linear-time algorithms + to test chordality of graphs, test acyclicity of hypergraphs, and + selectively reduce acyclic hypergraphs, SIAM J. Comput., 13 (1984), + pp. 566–579. + """ + if len(G.nodes) <= 3: + return True + return len(_find_chordality_breaker(G)) == 0 + + +@nx._dispatchable +def find_induced_nodes(G, s, t, treewidth_bound=sys.maxsize): + """Returns the set of induced nodes in the path from s to t. + + Parameters + ---------- + G : graph + A chordal NetworkX graph + s : node + Source node to look for induced nodes + t : node + Destination node to look for induced nodes + treewidth_bound: float + Maximum treewidth acceptable for the graph H. The search + for induced nodes will end as soon as the treewidth_bound is exceeded. + + Returns + ------- + induced_nodes : Set of nodes + The set of induced nodes in the path from s to t in G + + Raises + ------ + NetworkXError + The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. + If the input graph is an instance of one of these classes, a + :exc:`NetworkXError` is raised. + The algorithm can only be applied to chordal graphs. If the input + graph is found to be non-chordal, a :exc:`NetworkXError` is raised. + + Examples + -------- + >>> G = nx.Graph() + >>> G = nx.generators.classic.path_graph(10) + >>> induced_nodes = nx.find_induced_nodes(G, 1, 9, 2) + >>> sorted(induced_nodes) + [1, 2, 3, 4, 5, 6, 7, 8, 9] + + Notes + ----- + G must be a chordal graph and (s,t) an edge that is not in G. + + If a treewidth_bound is provided, the search for induced nodes will end + as soon as the treewidth_bound is exceeded. + + The algorithm is inspired by Algorithm 4 in [1]_. + A formal definition of induced node can also be found on that reference. + + Self Loops are ignored + + References + ---------- + .. [1] Learning Bounded Treewidth Bayesian Networks. + Gal Elidan, Stephen Gould; JMLR, 9(Dec):2699--2731, 2008. + http://jmlr.csail.mit.edu/papers/volume9/elidan08a/elidan08a.pdf + """ + if not is_chordal(G): + raise nx.NetworkXError("Input graph is not chordal.") + + H = nx.Graph(G) + H.add_edge(s, t) + induced_nodes = set() + triplet = _find_chordality_breaker(H, s, treewidth_bound) + while triplet: + (u, v, w) = triplet + induced_nodes.update(triplet) + for n in triplet: + if n != s: + H.add_edge(s, n) + triplet = _find_chordality_breaker(H, s, treewidth_bound) + if induced_nodes: + # Add t and the second node in the induced path from s to t. + induced_nodes.add(t) + for u in G[s]: + if len(induced_nodes & set(G[u])) == 2: + induced_nodes.add(u) + break + return induced_nodes + + +@nx._dispatchable +def chordal_graph_cliques(G): + """Returns all maximal cliques of a chordal graph. + + The algorithm breaks the graph in connected components and performs a + maximum cardinality search in each component to get the cliques. + + Parameters + ---------- + G : graph + A NetworkX graph + + Yields + ------ + frozenset of nodes + Maximal cliques, each of which is a frozenset of + nodes in `G`. The order of cliques is arbitrary. + + Raises + ------ + NetworkXError + The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. + The algorithm can only be applied to chordal graphs. If the input + graph is found to be non-chordal, a :exc:`NetworkXError` is raised. + + Examples + -------- + >>> e = [ + ... (1, 2), + ... (1, 3), + ... (2, 3), + ... (2, 4), + ... (3, 4), + ... (3, 5), + ... (3, 6), + ... (4, 5), + ... (4, 6), + ... (5, 6), + ... (7, 8), + ... ] + >>> G = nx.Graph(e) + >>> G.add_node(9) + >>> cliques = [c for c in chordal_graph_cliques(G)] + >>> cliques[0] + frozenset({1, 2, 3}) + """ + for C in (G.subgraph(c).copy() for c in connected_components(G)): + if C.number_of_nodes() == 1: + if nx.number_of_selfloops(C) > 0: + raise nx.NetworkXError("Input graph is not chordal.") + yield frozenset(C.nodes()) + else: + unnumbered = set(C.nodes()) + v = arbitrary_element(C) + unnumbered.remove(v) + numbered = {v} + clique_wanna_be = {v} + while unnumbered: + v = _max_cardinality_node(C, unnumbered, numbered) + unnumbered.remove(v) + numbered.add(v) + new_clique_wanna_be = set(C.neighbors(v)) & numbered + sg = C.subgraph(clique_wanna_be) + if _is_complete_graph(sg): + new_clique_wanna_be.add(v) + if not new_clique_wanna_be >= clique_wanna_be: + yield frozenset(clique_wanna_be) + clique_wanna_be = new_clique_wanna_be + else: + raise nx.NetworkXError("Input graph is not chordal.") + yield frozenset(clique_wanna_be) + + +@nx._dispatchable +def chordal_graph_treewidth(G): + """Returns the treewidth of the chordal graph G. + + Parameters + ---------- + G : graph + A NetworkX graph + + Returns + ------- + treewidth : int + The size of the largest clique in the graph minus one. + + Raises + ------ + NetworkXError + The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. + The algorithm can only be applied to chordal graphs. If the input + graph is found to be non-chordal, a :exc:`NetworkXError` is raised. + + Examples + -------- + >>> e = [ + ... (1, 2), + ... (1, 3), + ... (2, 3), + ... (2, 4), + ... (3, 4), + ... (3, 5), + ... (3, 6), + ... (4, 5), + ... (4, 6), + ... (5, 6), + ... (7, 8), + ... ] + >>> G = nx.Graph(e) + >>> G.add_node(9) + >>> nx.chordal_graph_treewidth(G) + 3 + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Tree_decomposition#Treewidth + """ + if not is_chordal(G): + raise nx.NetworkXError("Input graph is not chordal.") + + max_clique = -1 + for clique in nx.chordal_graph_cliques(G): + max_clique = max(max_clique, len(clique)) + return max_clique - 1 + + +def _is_complete_graph(G): + """Returns True if G is a complete graph.""" + if nx.number_of_selfloops(G) > 0: + raise nx.NetworkXError("Self loop found in _is_complete_graph()") + n = G.number_of_nodes() + if n < 2: + return True + e = G.number_of_edges() + max_edges = (n * (n - 1)) / 2 + return e == max_edges + + +def _find_missing_edge(G): + """Given a non-complete graph G, returns a missing edge.""" + nodes = set(G) + for u in G: + missing = nodes - set(list(G[u].keys()) + [u]) + if missing: + return (u, missing.pop()) + + +def _max_cardinality_node(G, choices, wanna_connect): + """Returns a the node in choices that has more connections in G + to nodes in wanna_connect. + """ + max_number = -1 + for x in choices: + number = len([y for y in G[x] if y in wanna_connect]) + if number > max_number: + max_number = number + max_cardinality_node = x + return max_cardinality_node + + +def _find_chordality_breaker(G, s=None, treewidth_bound=sys.maxsize): + """Given a graph G, starts a max cardinality search + (starting from s if s is given and from an arbitrary node otherwise) + trying to find a non-chordal cycle. + + If it does find one, it returns (u,v,w) where u,v,w are the three + nodes that together with s are involved in the cycle. + + It ignores any self loops. + """ + if len(G) == 0: + raise nx.NetworkXPointlessConcept("Graph has no nodes.") + unnumbered = set(G) + if s is None: + s = arbitrary_element(G) + unnumbered.remove(s) + numbered = {s} + current_treewidth = -1 + while unnumbered: # and current_treewidth <= treewidth_bound: + v = _max_cardinality_node(G, unnumbered, numbered) + unnumbered.remove(v) + numbered.add(v) + clique_wanna_be = set(G[v]) & numbered + sg = G.subgraph(clique_wanna_be) + if _is_complete_graph(sg): + # The graph seems to be chordal by now. We update the treewidth + current_treewidth = max(current_treewidth, len(clique_wanna_be)) + if current_treewidth > treewidth_bound: + raise nx.NetworkXTreewidthBoundExceeded( + f"treewidth_bound exceeded: {current_treewidth}" + ) + else: + # sg is not a clique, + # look for an edge that is not included in sg + (u, w) = _find_missing_edge(sg) + return (u, v, w) + return () + + +@not_implemented_for("directed") +@nx._dispatchable(returns_graph=True) +def complete_to_chordal_graph(G): + """Return a copy of G completed to a chordal graph + + Adds edges to a copy of G to create a chordal graph. A graph G=(V,E) is + called chordal if for each cycle with length bigger than 3, there exist + two non-adjacent nodes connected by an edge (called a chord). + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + Returns + ------- + H : NetworkX graph + The chordal enhancement of G + alpha : Dictionary + The elimination ordering of nodes of G + + Notes + ----- + There are different approaches to calculate the chordal + enhancement of a graph. The algorithm used here is called + MCS-M and gives at least minimal (local) triangulation of graph. Note + that this triangulation is not necessarily a global minimum. + + https://en.wikipedia.org/wiki/Chordal_graph + + References + ---------- + .. [1] Berry, Anne & Blair, Jean & Heggernes, Pinar & Peyton, Barry. (2004) + Maximum Cardinality Search for Computing Minimal Triangulations of + Graphs. Algorithmica. 39. 287-298. 10.1007/s00453-004-1084-3. + + Examples + -------- + >>> from networkx.algorithms.chordal import complete_to_chordal_graph + >>> G = nx.wheel_graph(10) + >>> H, alpha = complete_to_chordal_graph(G) + """ + H = G.copy() + alpha = {node: 0 for node in H} + if nx.is_chordal(H): + return H, alpha + chords = set() + weight = {node: 0 for node in H.nodes()} + unnumbered_nodes = list(H.nodes()) + for i in range(len(H.nodes()), 0, -1): + # get the node in unnumbered_nodes with the maximum weight + z = max(unnumbered_nodes, key=lambda node: weight[node]) + unnumbered_nodes.remove(z) + alpha[z] = i + update_nodes = [] + for y in unnumbered_nodes: + if G.has_edge(y, z): + update_nodes.append(y) + else: + # y_weight will be bigger than node weights between y and z + y_weight = weight[y] + lower_nodes = [ + node for node in unnumbered_nodes if weight[node] < y_weight + ] + if nx.has_path(H.subgraph(lower_nodes + [z, y]), y, z): + update_nodes.append(y) + chords.add((z, y)) + # during calculation of paths the weights should not be updated + for node in update_nodes: + weight[node] += 1 + H.add_edges_from(chords) + return H, alpha diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/clique.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/clique.py new file mode 100644 index 0000000000000000000000000000000000000000..2a1aba4acf6947da9681433b13d293995188fe4d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/clique.py @@ -0,0 +1,818 @@ +"""Functions for finding and manipulating cliques. + +Finding the largest clique in a graph is NP-complete problem, so most of +these algorithms have an exponential running time; for more information, +see the Wikipedia article on the clique problem [1]_. + +.. [1] clique problem:: https://en.wikipedia.org/wiki/Clique_problem + +""" + +from collections import Counter, defaultdict, deque +from itertools import chain, combinations, islice + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "find_cliques", + "find_cliques_recursive", + "make_max_clique_graph", + "make_clique_bipartite", + "node_clique_number", + "number_of_cliques", + "enumerate_all_cliques", + "max_weight_clique", +] + + +@not_implemented_for("directed") +@nx._dispatchable +def enumerate_all_cliques(G): + """Returns all cliques in an undirected graph. + + This function returns an iterator over cliques, each of which is a + list of nodes. The iteration is ordered by cardinality of the + cliques: first all cliques of size one, then all cliques of size + two, etc. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + Returns + ------- + iterator + An iterator over cliques, each of which is a list of nodes in + `G`. The cliques are ordered according to size. + + Notes + ----- + To obtain a list of all cliques, use + `list(enumerate_all_cliques(G))`. However, be aware that in the + worst-case, the length of this list can be exponential in the number + of nodes in the graph (for example, when the graph is the complete + graph). This function avoids storing all cliques in memory by only + keeping current candidate node lists in memory during its search. + + The implementation is adapted from the algorithm by Zhang, et + al. (2005) [1]_ to output all cliques discovered. + + This algorithm ignores self-loops and parallel edges, since cliques + are not conventionally defined with such edges. + + References + ---------- + .. [1] Yun Zhang, Abu-Khzam, F.N., Baldwin, N.E., Chesler, E.J., + Langston, M.A., Samatova, N.F., + "Genome-Scale Computational Approaches to Memory-Intensive + Applications in Systems Biology". + *Supercomputing*, 2005. Proceedings of the ACM/IEEE SC 2005 + Conference, pp. 12, 12--18 Nov. 2005. + . + + """ + index = {} + nbrs = {} + for u in G: + index[u] = len(index) + # Neighbors of u that appear after u in the iteration order of G. + nbrs[u] = {v for v in G[u] if v not in index} + + queue = deque(([u], sorted(nbrs[u], key=index.__getitem__)) for u in G) + # Loop invariants: + # 1. len(base) is nondecreasing. + # 2. (base + cnbrs) is sorted with respect to the iteration order of G. + # 3. cnbrs is a set of common neighbors of nodes in base. + while queue: + base, cnbrs = map(list, queue.popleft()) + yield base + for i, u in enumerate(cnbrs): + # Use generators to reduce memory consumption. + queue.append( + ( + chain(base, [u]), + filter(nbrs[u].__contains__, islice(cnbrs, i + 1, None)), + ) + ) + + +@not_implemented_for("directed") +@nx._dispatchable +def find_cliques(G, nodes=None): + """Returns all maximal cliques in an undirected graph. + + For each node *n*, a *maximal clique for n* is a largest complete + subgraph containing *n*. The largest maximal clique is sometimes + called the *maximum clique*. + + This function returns an iterator over cliques, each of which is a + list of nodes. It is an iterative implementation, so should not + suffer from recursion depth issues. + + This function accepts a list of `nodes` and only the maximal cliques + containing all of these `nodes` are returned. It can considerably speed up + the running time if some specific cliques are desired. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + nodes : list, optional (default=None) + If provided, only yield *maximal cliques* containing all nodes in `nodes`. + If `nodes` isn't a clique itself, a ValueError is raised. + + Returns + ------- + iterator + An iterator over maximal cliques, each of which is a list of + nodes in `G`. If `nodes` is provided, only the maximal cliques + containing all the nodes in `nodes` are returned. The order of + cliques is arbitrary. + + Raises + ------ + ValueError + If `nodes` is not a clique. + + Examples + -------- + >>> from pprint import pprint # For nice dict formatting + >>> G = nx.karate_club_graph() + >>> sum(1 for c in nx.find_cliques(G)) # The number of maximal cliques in G + 36 + >>> max(nx.find_cliques(G), key=len) # The largest maximal clique in G + [0, 1, 2, 3, 13] + + The size of the largest maximal clique is known as the *clique number* of + the graph, which can be found directly with: + + >>> max(len(c) for c in nx.find_cliques(G)) + 5 + + One can also compute the number of maximal cliques in `G` that contain a given + node. The following produces a dictionary keyed by node whose + values are the number of maximal cliques in `G` that contain the node: + + >>> from collections import Counter + >>> from itertools import chain + >>> counts = Counter(chain.from_iterable(nx.find_cliques(G))) + >>> pprint(dict(counts)) + {0: 13, + 1: 6, + 2: 7, + 3: 3, + 4: 2, + 5: 3, + 6: 3, + 7: 1, + 8: 3, + 9: 2, + 10: 2, + 11: 1, + 12: 1, + 13: 2, + 14: 1, + 15: 1, + 16: 1, + 17: 1, + 18: 1, + 19: 2, + 20: 1, + 21: 1, + 22: 1, + 23: 3, + 24: 2, + 25: 2, + 26: 1, + 27: 3, + 28: 2, + 29: 2, + 30: 2, + 31: 4, + 32: 9, + 33: 14} + + Or, similarly, the maximal cliques in `G` that contain a given node. + For example, the 4 maximal cliques that contain node 31: + + >>> [c for c in nx.find_cliques(G) if 31 in c] + [[0, 31], [33, 32, 31], [33, 28, 31], [24, 25, 31]] + + See Also + -------- + find_cliques_recursive + A recursive version of the same algorithm. + + Notes + ----- + To obtain a list of all maximal cliques, use + `list(find_cliques(G))`. However, be aware that in the worst-case, + the length of this list can be exponential in the number of nodes in + the graph. This function avoids storing all cliques in memory by + only keeping current candidate node lists in memory during its search. + + This implementation is based on the algorithm published by Bron and + Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi + (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. It + essentially unrolls the recursion used in the references to avoid + issues of recursion stack depth (for a recursive implementation, see + :func:`find_cliques_recursive`). + + This algorithm ignores self-loops and parallel edges, since cliques + are not conventionally defined with such edges. + + References + ---------- + .. [1] Bron, C. and Kerbosch, J. + "Algorithm 457: finding all cliques of an undirected graph". + *Communications of the ACM* 16, 9 (Sep. 1973), 575--577. + + + .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi, + "The worst-case time complexity for generating all maximal + cliques and computational experiments", + *Theoretical Computer Science*, Volume 363, Issue 1, + Computing and Combinatorics, + 10th Annual International Conference on + Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42 + + + .. [3] F. Cazals, C. Karande, + "A note on the problem of reporting maximal cliques", + *Theoretical Computer Science*, + Volume 407, Issues 1--3, 6 November 2008, Pages 564--568, + + + """ + if len(G) == 0: + return + + adj = {u: {v for v in G[u] if v != u} for u in G} + + # Initialize Q with the given nodes and subg, cand with their nbrs + Q = nodes[:] if nodes is not None else [] + cand = set(G) + for node in Q: + if node not in cand: + raise ValueError(f"The given `nodes` {nodes} do not form a clique") + cand &= adj[node] + + if not cand: + yield Q[:] + return + + subg = cand.copy() + stack = [] + Q.append(None) + + u = max(subg, key=lambda u: len(cand & adj[u])) + ext_u = cand - adj[u] + + try: + while True: + if ext_u: + q = ext_u.pop() + cand.remove(q) + Q[-1] = q + adj_q = adj[q] + subg_q = subg & adj_q + if not subg_q: + yield Q[:] + else: + cand_q = cand & adj_q + if cand_q: + stack.append((subg, cand, ext_u)) + Q.append(None) + subg = subg_q + cand = cand_q + u = max(subg, key=lambda u: len(cand & adj[u])) + ext_u = cand - adj[u] + else: + Q.pop() + subg, cand, ext_u = stack.pop() + except IndexError: + pass + + +@not_implemented_for("directed") +@nx._dispatchable +def find_cliques_recursive(G, nodes=None): + """Returns all maximal cliques in a graph. + + For each node *v*, a *maximal clique for v* is a largest complete + subgraph containing *v*. The largest maximal clique is sometimes + called the *maximum clique*. + + This function returns an iterator over cliques, each of which is a + list of nodes. It is a recursive implementation, so may suffer from + recursion depth issues, but is included for pedagogical reasons. + For a non-recursive implementation, see :func:`find_cliques`. + + This function accepts a list of `nodes` and only the maximal cliques + containing all of these `nodes` are returned. It can considerably speed up + the running time if some specific cliques are desired. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + nodes : list, optional (default=None) + If provided, only yield *maximal cliques* containing all nodes in `nodes`. + If `nodes` isn't a clique itself, a ValueError is raised. + + Returns + ------- + iterator + An iterator over maximal cliques, each of which is a list of + nodes in `G`. If `nodes` is provided, only the maximal cliques + containing all the nodes in `nodes` are yielded. The order of + cliques is arbitrary. + + Raises + ------ + NetworkXNotImplemented + If `G` is directed. + + ValueError + If `nodes` is not a clique. + + See Also + -------- + find_cliques + An iterative version of the same algorithm. See docstring for examples. + + Notes + ----- + To obtain a list of all maximal cliques, use + `list(find_cliques_recursive(G))`. However, be aware that in the + worst-case, the length of this list can be exponential in the number + of nodes in the graph. This function avoids storing all cliques in memory + by only keeping current candidate node lists in memory during its search. + + This implementation is based on the algorithm published by Bron and + Kerbosch (1973) [1]_, as adapted by Tomita, Tanaka and Takahashi + (2006) [2]_ and discussed in Cazals and Karande (2008) [3]_. For a + non-recursive implementation, see :func:`find_cliques`. + + This algorithm ignores self-loops and parallel edges, since cliques + are not conventionally defined with such edges. + + References + ---------- + .. [1] Bron, C. and Kerbosch, J. + "Algorithm 457: finding all cliques of an undirected graph". + *Communications of the ACM* 16, 9 (Sep. 1973), 575--577. + + + .. [2] Etsuji Tomita, Akira Tanaka, Haruhisa Takahashi, + "The worst-case time complexity for generating all maximal + cliques and computational experiments", + *Theoretical Computer Science*, Volume 363, Issue 1, + Computing and Combinatorics, + 10th Annual International Conference on + Computing and Combinatorics (COCOON 2004), 25 October 2006, Pages 28--42 + + + .. [3] F. Cazals, C. Karande, + "A note on the problem of reporting maximal cliques", + *Theoretical Computer Science*, + Volume 407, Issues 1--3, 6 November 2008, Pages 564--568, + + + """ + if len(G) == 0: + return iter([]) + + adj = {u: {v for v in G[u] if v != u} for u in G} + + # Initialize Q with the given nodes and subg, cand with their nbrs + Q = nodes[:] if nodes is not None else [] + cand_init = set(G) + for node in Q: + if node not in cand_init: + raise ValueError(f"The given `nodes` {nodes} do not form a clique") + cand_init &= adj[node] + + if not cand_init: + return iter([Q]) + + subg_init = cand_init.copy() + + def expand(subg, cand): + u = max(subg, key=lambda u: len(cand & adj[u])) + for q in cand - adj[u]: + cand.remove(q) + Q.append(q) + adj_q = adj[q] + subg_q = subg & adj_q + if not subg_q: + yield Q[:] + else: + cand_q = cand & adj_q + if cand_q: + yield from expand(subg_q, cand_q) + Q.pop() + + return expand(subg_init, cand_init) + + +@nx._dispatchable(returns_graph=True) +def make_max_clique_graph(G, create_using=None): + """Returns the maximal clique graph of the given graph. + + The nodes of the maximal clique graph of `G` are the cliques of + `G` and an edge joins two cliques if the cliques are not disjoint. + + Parameters + ---------- + G : NetworkX graph + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + A graph whose nodes are the cliques of `G` and whose edges + join two cliques if they are not disjoint. + + Notes + ----- + This function behaves like the following code:: + + import networkx as nx + + G = nx.make_clique_bipartite(G) + cliques = [v for v in G.nodes() if G.nodes[v]["bipartite"] == 0] + G = nx.bipartite.projected_graph(G, cliques) + G = nx.relabel_nodes(G, {-v: v - 1 for v in G}) + + It should be faster, though, since it skips all the intermediate + steps. + + """ + if create_using is None: + B = G.__class__() + else: + B = nx.empty_graph(0, create_using) + cliques = list(enumerate(set(c) for c in find_cliques(G))) + # Add a numbered node for each clique. + B.add_nodes_from(i for i, c in cliques) + # Join cliques by an edge if they share a node. + clique_pairs = combinations(cliques, 2) + B.add_edges_from((i, j) for (i, c1), (j, c2) in clique_pairs if c1 & c2) + return B + + +@nx._dispatchable(returns_graph=True) +def make_clique_bipartite(G, fpos=None, create_using=None, name=None): + """Returns the bipartite clique graph corresponding to `G`. + + In the returned bipartite graph, the "bottom" nodes are the nodes of + `G` and the "top" nodes represent the maximal cliques of `G`. + There is an edge from node *v* to clique *C* in the returned graph + if and only if *v* is an element of *C*. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + fpos : bool + If True or not None, the returned graph will have an + additional attribute, `pos`, a dictionary mapping node to + position in the Euclidean plane. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + A bipartite graph whose "bottom" set is the nodes of the graph + `G`, whose "top" set is the cliques of `G`, and whose edges + join nodes of `G` to the cliques that contain them. + + The nodes of the graph `G` have the node attribute + 'bipartite' set to 1 and the nodes representing cliques + have the node attribute 'bipartite' set to 0, as is the + convention for bipartite graphs in NetworkX. + + """ + B = nx.empty_graph(0, create_using) + B.clear() + # The "bottom" nodes in the bipartite graph are the nodes of the + # original graph, G. + B.add_nodes_from(G, bipartite=1) + for i, cl in enumerate(find_cliques(G)): + # The "top" nodes in the bipartite graph are the cliques. These + # nodes get negative numbers as labels. + name = -i - 1 + B.add_node(name, bipartite=0) + B.add_edges_from((v, name) for v in cl) + return B + + +@nx._dispatchable +def node_clique_number(G, nodes=None, cliques=None, separate_nodes=False): + """Returns the size of the largest maximal clique containing each given node. + + Returns a single or list depending on input nodes. + An optional list of cliques can be input if already computed. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + cliques : list, optional (default=None) + A list of cliques, each of which is itself a list of nodes. + If not specified, the list of all cliques will be computed + using :func:`find_cliques`. + + Returns + ------- + int or dict + If `nodes` is a single node, returns the size of the + largest maximal clique in `G` containing that node. + Otherwise return a dict keyed by node to the size + of the largest maximal clique containing that node. + + See Also + -------- + find_cliques + find_cliques yields the maximal cliques of G. + It accepts a `nodes` argument which restricts consideration to + maximal cliques containing all the given `nodes`. + The search for the cliques is optimized for `nodes`. + number_of_cliques + """ + if cliques is None: + if nodes is not None: + # Use ego_graph to decrease size of graph + # check for single node + if nodes in G: + return max(len(c) for c in find_cliques(nx.ego_graph(G, nodes))) + # handle multiple nodes + return { + n: max(len(c) for c in find_cliques(nx.ego_graph(G, n))) for n in nodes + } + + # nodes is None--find all cliques + cliques = list(find_cliques(G)) + + # single node requested + if nodes in G: + return max(len(c) for c in cliques if nodes in c) + + # multiple nodes requested + # preprocess all nodes (faster than one at a time for even 2 nodes) + size_for_n = defaultdict(int) + for c in cliques: + size_of_c = len(c) + for n in c: + if size_for_n[n] < size_of_c: + size_for_n[n] = size_of_c + if nodes is None: + return size_for_n + return {n: size_for_n[n] for n in nodes} + + +def number_of_cliques(G, nodes=None, cliques=None): + """Return the number of maximal cliques each node is part of. + + Output is a single value or dict depending on `nodes`. + Optional list of cliques can be input if already computed. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + nodes : list or None, optional (default=None) + A list of nodes to return the number of maximal cliques for. + If `None`, return the number of maximal cliques for all nodes. + + cliques : list or None, optional (default=None) + A precomputed list of maximal cliques to use for the calculation. + + Returns + ------- + int or dict + If `nodes` is a single node, return the number of maximal cliques it is + part of. If `nodes` is a list, return a dictionary keyed by node to the + number of maximal cliques it is part of. + + Raises + ------ + NetworkXNotImplemented + If `G` is directed. + + See Also + -------- + find_cliques + node_clique_number + + Examples + -------- + Compute the number of maximal cliques a node is part of: + + >>> G = nx.complete_graph(3) + >>> nx.add_cycle(G, [0, 3, 4]) + >>> nx.number_of_cliques(G, nodes=0) + 2 + >>> nx.number_of_cliques(G, nodes=1) + 1 + + Or, for a list of nodes: + + >>> nx.number_of_cliques(G, nodes=[0, 1]) + {0: 2, 1: 1} + + If no explicit `nodes` are provided, all nodes are considered: + + >>> nx.number_of_cliques(G) + {0: 2, 1: 1, 2: 1, 3: 1, 4: 1} + + The list of maximal cliques can also be precomputed: + + >>> cl = list(nx.find_cliques(G)) + >>> nx.number_of_cliques(G, cliques=cl) + {0: 2, 1: 1, 2: 1, 3: 1, 4: 1} + """ + if cliques is None: + cliques = find_cliques(G) + + if nodes is None: + nodes = list(G.nodes()) # none, get entire graph + + if not isinstance(nodes, list): # check for a list + v = nodes + # assume it is a single value + numcliq = sum(1 for c in cliques if v in c) + else: + numcliq = Counter(chain.from_iterable(cliques)) + numcliq = {v: numcliq[v] for v in nodes} # return a dict + return numcliq + + +class MaxWeightClique: + """A class for the maximum weight clique algorithm. + + This class is a helper for the `max_weight_clique` function. The class + should not normally be used directly. + + Parameters + ---------- + G : NetworkX graph + The undirected graph for which a maximum weight clique is sought + weight : string or None, optional (default='weight') + The node attribute that holds the integer value used as a weight. + If None, then each node has weight 1. + + Attributes + ---------- + G : NetworkX graph + The undirected graph for which a maximum weight clique is sought + node_weights: dict + The weight of each node + incumbent_nodes : list + The nodes of the incumbent clique (the best clique found so far) + incumbent_weight: int + The weight of the incumbent clique + """ + + def __init__(self, G, weight): + self.G = G + self.incumbent_nodes = [] + self.incumbent_weight = 0 + + if weight is None: + self.node_weights = {v: 1 for v in G.nodes()} + else: + for v in G.nodes(): + if weight not in G.nodes[v]: + errmsg = f"Node {v!r} does not have the requested weight field." + raise KeyError(errmsg) + if not isinstance(G.nodes[v][weight], int): + errmsg = f"The {weight!r} field of node {v!r} is not an integer." + raise ValueError(errmsg) + self.node_weights = {v: G.nodes[v][weight] for v in G.nodes()} + + def update_incumbent_if_improved(self, C, C_weight): + """Update the incumbent if the node set C has greater weight. + + C is assumed to be a clique. + """ + if C_weight > self.incumbent_weight: + self.incumbent_nodes = C[:] + self.incumbent_weight = C_weight + + def greedily_find_independent_set(self, P): + """Greedily find an independent set of nodes from a set of + nodes P.""" + independent_set = [] + P = P[:] + while P: + v = P[0] + independent_set.append(v) + P = [w for w in P if v != w and not self.G.has_edge(v, w)] + return independent_set + + def find_branching_nodes(self, P, target): + """Find a set of nodes to branch on.""" + residual_wt = {v: self.node_weights[v] for v in P} + total_wt = 0 + P = P[:] + while P: + independent_set = self.greedily_find_independent_set(P) + min_wt_in_class = min(residual_wt[v] for v in independent_set) + total_wt += min_wt_in_class + if total_wt > target: + break + for v in independent_set: + residual_wt[v] -= min_wt_in_class + P = [v for v in P if residual_wt[v] != 0] + return P + + def expand(self, C, C_weight, P): + """Look for the best clique that contains all the nodes in C and zero or + more of the nodes in P, backtracking if it can be shown that no such + clique has greater weight than the incumbent. + """ + self.update_incumbent_if_improved(C, C_weight) + branching_nodes = self.find_branching_nodes(P, self.incumbent_weight - C_weight) + while branching_nodes: + v = branching_nodes.pop() + P.remove(v) + new_C = C + [v] + new_C_weight = C_weight + self.node_weights[v] + new_P = [w for w in P if self.G.has_edge(v, w)] + self.expand(new_C, new_C_weight, new_P) + + def find_max_weight_clique(self): + """Find a maximum weight clique.""" + # Sort nodes in reverse order of degree for speed + nodes = sorted(self.G.nodes(), key=lambda v: self.G.degree(v), reverse=True) + nodes = [v for v in nodes if self.node_weights[v] > 0] + self.expand([], 0, nodes) + + +@not_implemented_for("directed") +@nx._dispatchable(node_attrs="weight") +def max_weight_clique(G, weight="weight"): + """Find a maximum weight clique in G. + + A *clique* in a graph is a set of nodes such that every two distinct nodes + are adjacent. The *weight* of a clique is the sum of the weights of its + nodes. A *maximum weight clique* of graph G is a clique C in G such that + no clique in G has weight greater than the weight of C. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + weight : string or None, optional (default='weight') + The node attribute that holds the integer value used as a weight. + If None, then each node has weight 1. + + Returns + ------- + clique : list + the nodes of a maximum weight clique + weight : int + the weight of a maximum weight clique + + Notes + ----- + The implementation is recursive, and therefore it may run into recursion + depth issues if G contains a clique whose number of nodes is close to the + recursion depth limit. + + At each search node, the algorithm greedily constructs a weighted + independent set cover of part of the graph in order to find a small set of + nodes on which to branch. The algorithm is very similar to the algorithm + of Tavares et al. [1]_, other than the fact that the NetworkX version does + not use bitsets. This style of algorithm for maximum weight clique (and + maximum weight independent set, which is the same problem but on the + complement graph) has a decades-long history. See Algorithm B of Warren + and Hicks [2]_ and the references in that paper. + + References + ---------- + .. [1] Tavares, W.A., Neto, M.B.C., Rodrigues, C.D., Michelon, P.: Um + algoritmo de branch and bound para o problema da clique máxima + ponderada. Proceedings of XLVII SBPO 1 (2015). + + .. [2] Warren, Jeffrey S, Hicks, Illya V.: Combinatorial Branch-and-Bound + for the Maximum Weight Independent Set Problem. Technical Report, + Texas A&M University (2016). + """ + + mwc = MaxWeightClique(G, weight) + mwc.find_max_weight_clique() + return mwc.incumbent_nodes, mwc.incumbent_weight diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cluster.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ac54edfadf6fe851cbafdbfe5991f1e6df4946 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cluster.py @@ -0,0 +1,732 @@ +"""Algorithms to characterize the number of triangles in a graph.""" + +from collections import Counter +from itertools import chain, combinations + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "triangles", + "all_triangles", + "average_clustering", + "clustering", + "transitivity", + "square_clustering", + "generalized_degree", +] + + +@not_implemented_for("directed") +@nx._dispatchable +def triangles(G, nodes=None): + """Compute the number of triangles. + + Finds the number of triangles that include a node as one vertex. + + Parameters + ---------- + G : graph + A networkx graph + + nodes : node, iterable of nodes, or None (default=None) + If a singleton node, return the number of triangles for that node. + If an iterable, compute the number of triangles for each of those nodes. + If `None` (the default) compute the number of triangles for all nodes in `G`. + + Returns + ------- + out : dict or int + If `nodes` is a container of nodes, returns number of triangles keyed by node (dict). + If `nodes` is a specific node, returns number of triangles for the node (int). + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.triangles(G, 0)) + 6 + >>> print(nx.triangles(G)) + {0: 6, 1: 6, 2: 6, 3: 6, 4: 6} + >>> print(list(nx.triangles(G, [0, 1]).values())) + [6, 6] + + The total number of unique triangles in `G` can be determined by summing + the number of triangles for each node and dividing by 3 (because a given + triangle gets counted three times, once for each of its nodes). + + >>> sum(nx.triangles(G).values()) // 3 + 10 + + Notes + ----- + Self loops are ignored. + + """ + if nodes is not None: + # If `nodes` represents a single node, return only its number of triangles + if nodes in G: + return next(_triangles_and_degree_iter(G, nodes))[2] // 2 + + # if `nodes` is a container of nodes, then return a + # dictionary mapping node to number of triangles. + return {v: t // 2 for v, d, t, _ in _triangles_and_degree_iter(G, nodes)} + + # if nodes is None, then compute triangles for the complete graph + + # dict used to avoid visiting the same nodes twice + # this allows calculating/counting each triangle only once + later_nbrs = {} + + # iterate over the nodes in a graph + for node, neighbors in G.adjacency(): + later_nbrs[node] = {n for n in neighbors if n not in later_nbrs and n != node} + + # instantiate Counter for each node to include isolated nodes + # add 1 to the count if a nodes neighbor's neighbor is also a neighbor + triangle_counts = Counter(dict.fromkeys(G, 0)) + for node1, neighbors in later_nbrs.items(): + for node2 in neighbors: + third_nodes = neighbors & later_nbrs[node2] + m = len(third_nodes) + triangle_counts[node1] += m + triangle_counts[node2] += m + triangle_counts.update(third_nodes) + + return dict(triangle_counts) + + +@not_implemented_for("multigraph") +def _triangles_and_degree_iter(G, nodes=None): + """Return an iterator of (node, degree, triangles, generalized degree). + + This double counts triangles so you may want to divide by 2. + See degree(), triangles() and generalized_degree() for definitions + and details. + + """ + if nodes is None: + nodes_nbrs = G.adj.items() + else: + nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes)) + + for v, v_nbrs in nodes_nbrs: + vs = set(v_nbrs) - {v} + gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs) + ntriangles = sum(k * val for k, val in gen_degree.items()) + yield (v, len(vs), ntriangles, gen_degree) + + +@not_implemented_for("multigraph") +def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"): + """Return an iterator of (node, degree, weighted_triangles). + + Used for weighted clustering. + Note: this returns the geometric average weight of edges in the triangle. + Also, each triangle is counted twice (each direction). + So you may want to divide by 2. + + """ + import numpy as np + + if weight is None or G.number_of_edges() == 0: + max_weight = 1 + else: + max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True)) + if nodes is None: + nodes_nbrs = G.adj.items() + else: + nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes)) + + def wt(u, v): + return G[u][v].get(weight, 1) / max_weight + + for i, nbrs in nodes_nbrs: + inbrs = set(nbrs) - {i} + weighted_triangles = 0 + seen = set() + for j in inbrs: + seen.add(j) + # This avoids counting twice -- we double at the end. + jnbrs = set(G[j]) - seen + # Only compute the edge weight once, before the inner inner + # loop. + wij = wt(i, j) + weighted_triangles += np.cbrt( + [(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs] + ).sum() + yield (i, len(inbrs), 2 * float(weighted_triangles)) + + +@not_implemented_for("multigraph") +def _directed_triangles_and_degree_iter(G, nodes=None): + """Return an iterator of + (node, total_degree, reciprocal_degree, directed_triangles). + + Used for directed clustering. + Note that unlike `_triangles_and_degree_iter()`, this function counts + directed triangles so does not count triangles twice. + + """ + nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes)) + + for i, preds, succs in nodes_nbrs: + ipreds = set(preds) - {i} + isuccs = set(succs) - {i} + + directed_triangles = 0 + for j in chain(ipreds, isuccs): + jpreds = set(G._pred[j]) - {j} + jsuccs = set(G._succ[j]) - {j} + directed_triangles += sum( + 1 + for k in chain( + (ipreds & jpreds), + (ipreds & jsuccs), + (isuccs & jpreds), + (isuccs & jsuccs), + ) + ) + dtotal = len(ipreds) + len(isuccs) + dbidirectional = len(ipreds & isuccs) + yield (i, dtotal, dbidirectional, directed_triangles) + + +@not_implemented_for("multigraph") +def _directed_weighted_triangles_and_degree_iter(G, nodes=None, weight="weight"): + """Return an iterator of + (node, total_degree, reciprocal_degree, directed_weighted_triangles). + + Used for directed weighted clustering. + Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts + directed triangles so does not count triangles twice. + + """ + import numpy as np + + if weight is None or G.number_of_edges() == 0: + max_weight = 1 + else: + max_weight = max(d.get(weight, 1) for u, v, d in G.edges(data=True)) + + nodes_nbrs = ((n, G._pred[n], G._succ[n]) for n in G.nbunch_iter(nodes)) + + def wt(u, v): + return G[u][v].get(weight, 1) / max_weight + + for i, preds, succs in nodes_nbrs: + ipreds = set(preds) - {i} + isuccs = set(succs) - {i} + + directed_triangles = 0 + for j in ipreds: + jpreds = set(G._pred[j]) - {j} + jsuccs = set(G._succ[j]) - {j} + directed_triangles += np.cbrt( + [(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds] + ).sum() + directed_triangles += np.cbrt( + [(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs] + ).sum() + directed_triangles += np.cbrt( + [(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds] + ).sum() + directed_triangles += np.cbrt( + [(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs] + ).sum() + + for j in isuccs: + jpreds = set(G._pred[j]) - {j} + jsuccs = set(G._succ[j]) - {j} + directed_triangles += np.cbrt( + [(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds] + ).sum() + directed_triangles += np.cbrt( + [(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs] + ).sum() + directed_triangles += np.cbrt( + [(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds] + ).sum() + directed_triangles += np.cbrt( + [(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs] + ).sum() + + dtotal = len(ipreds) + len(isuccs) + dbidirectional = len(ipreds & isuccs) + yield (i, dtotal, dbidirectional, float(directed_triangles)) + + +@not_implemented_for("directed") +@nx._dispatchable +def all_triangles(G, nbunch=None): + """ + Yields all unique triangles in an undirected graph. + + A triangle is a set of three distinct nodes where each node is connected to + the other two. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + nbunch : node, iterable of nodes, or None (default=None) + If a node or iterable of nodes, only triangles involving at least one + node in `nbunch` are yielded. + If ``None``, yields all unique triangles in the graph. + + Yields + ------ + tuple + A tuple of three nodes forming a triangle ``(u, v, w)``. + + Examples + -------- + >>> G = nx.complete_graph(4) + >>> sorted([sorted(t) for t in all_triangles(G)]) + [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]] + + Notes + ----- + This algorithm ensures each triangle is yielded once using an internal node ordering. + In multigraphs, triangles are identified by their unique set of nodes, + ignoring multiple edges between the same nodes. Self-loops are ignored. + Runs in ``O(m * d)`` time in the worst case, where ``m`` the number of edges + and ``d`` the maximum degree. + + See Also + -------- + :func:`~networkx.algorithms.triads.all_triads` : related function for directed graphs + """ + if nbunch is None: + nbunch = relevant_nodes = G + else: + nbunch = dict.fromkeys(G.nbunch_iter(nbunch)) + relevant_nodes = chain( + nbunch, + (nbr for node in nbunch for nbr in G.neighbors(node) if nbr not in nbunch), + ) + + node_to_id = {node: i for i, node in enumerate(relevant_nodes)} + + for u in nbunch: + u_id = node_to_id[u] + u_nbrs = G._adj[u].keys() + for v in u_nbrs: + v_id = node_to_id.get(v, -1) + if v_id <= u_id: + continue + v_nbrs = G._adj[v].keys() + for w in v_nbrs & u_nbrs: + if node_to_id.get(w, -1) > v_id: + yield u, v, w + + +@nx._dispatchable(edge_attrs="weight") +def average_clustering(G, nodes=None, weight=None, count_zeros=True): + r"""Compute the average clustering coefficient for the graph G. + + The clustering coefficient for the graph is the average, + + .. math:: + + C = \frac{1}{n}\sum_{v \in G} c_v, + + where :math:`n` is the number of nodes in `G`. + + Parameters + ---------- + G : graph + + nodes : container of nodes, optional (default=all nodes in G) + Compute average clustering for nodes in this container. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used as a weight. + If None, then each edge has weight 1. + + count_zeros : bool + If False include only the nodes with nonzero clustering in the average. + + Returns + ------- + avg : float + Average clustering + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.average_clustering(G)) + 1.0 + + Notes + ----- + This is a space saving routine; it might be faster + to use the clustering function to get a list and then take the average. + + Self loops are ignored. + + References + ---------- + .. [1] Generalizations of the clustering coefficient to weighted + complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela, + K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007). + http://jponnela.com/web_documents/a9.pdf + .. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated + nodes and leafs on clustering measures for small-world networks. + https://arxiv.org/abs/0802.2512 + """ + c = clustering(G, nodes, weight=weight).values() + if not count_zeros: + c = [v for v in c if abs(v) > 0] + return sum(c) / len(c) + + +@nx._dispatchable(edge_attrs="weight") +def clustering(G, nodes=None, weight=None): + r"""Compute the clustering coefficient for nodes. + + For unweighted graphs, the clustering of a node :math:`u` + is the fraction of possible triangles through that node that exist, + + .. math:: + + c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)}, + + where :math:`T(u)` is the number of triangles through node :math:`u` and + :math:`deg(u)` is the degree of :math:`u`. + + For weighted graphs, there are several ways to define clustering [1]_. + the one used here is defined + as the geometric average of the subgraph edge weights [2]_, + + .. math:: + + c_u = \frac{1}{deg(u)(deg(u)-1))} + \sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}. + + The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight + in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`. + + The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`. + + Additionally, this weighted definition has been generalized to support negative edge weights [3]_. + + For directed graphs, the clustering is similarly defined as the fraction + of all possible directed triangles or geometric average of the subgraph + edge weights for unweighted and weighted directed graph respectively [4]_. + + .. math:: + + c_u = \frac{T(u)}{2(deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u))}, + + where :math:`T(u)` is the number of directed triangles through node + :math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of + :math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of + :math:`u`. + + + Parameters + ---------- + G : graph + + nodes : node, iterable of nodes, or None (default=None) + If a singleton node, return the number of triangles for that node. + If an iterable, compute the number of triangles for each of those nodes. + If `None` (the default) compute the number of triangles for all nodes in `G`. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used as a weight. + If None, then each edge has weight 1. + + Returns + ------- + out : float, or dictionary + Clustering coefficient at specified nodes + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.clustering(G, 0)) + 1.0 + >>> print(nx.clustering(G)) + {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0} + + Notes + ----- + Self loops are ignored. + + References + ---------- + .. [1] Generalizations of the clustering coefficient to weighted + complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela, + K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007). + http://jponnela.com/web_documents/a9.pdf + .. [2] Intensity and coherence of motifs in weighted complex + networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski, + Physical Review E, 71(6), 065103 (2005). + .. [3] Generalization of Clustering Coefficients to Signed Correlation Networks + by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014). + .. [4] Clustering in complex directed networks by G. Fagiolo, + Physical Review E, 76(2), 026107 (2007). + """ + if G.is_directed(): + if weight is not None: + td_iter = _directed_weighted_triangles_and_degree_iter(G, nodes, weight) + clusterc = { + v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2) + for v, dt, db, t in td_iter + } + else: + td_iter = _directed_triangles_and_degree_iter(G, nodes) + clusterc = { + v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2) + for v, dt, db, t in td_iter + } + else: + # The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T + if weight is not None: + td_iter = _weighted_triangles_and_degree_iter(G, nodes, weight) + clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter} + else: + td_iter = _triangles_and_degree_iter(G, nodes) + clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter} + if nodes in G: + # Return the value of the sole entry in the dictionary. + return clusterc[nodes] + return clusterc + + +@nx._dispatchable +def transitivity(G): + r"""Compute graph transitivity, the fraction of all possible triangles + present in G. + + Possible triangles are identified by the number of "triads" + (two edges with a shared vertex). + + The transitivity is + + .. math:: + + T = 3\frac{\#triangles}{\#triads}. + + Parameters + ---------- + G : graph + + Returns + ------- + out : float + Transitivity + + Notes + ----- + Self loops are ignored. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.transitivity(G)) + 1.0 + """ + triangles_contri = [ + (t, d * (d - 1)) for v, d, t, _ in _triangles_and_degree_iter(G) + ] + # If the graph is empty + if len(triangles_contri) == 0: + return 0 + triangles, contri = map(sum, zip(*triangles_contri)) + return 0 if triangles == 0 else triangles / contri + + +@nx._dispatchable +def square_clustering(G, nodes=None): + r"""Compute the squares clustering coefficient for nodes. + + For each node return the fraction of possible squares that exist at + the node [1]_ + + .. math:: + C_4(v) = \frac{ \sum_{u=1}^{k_v} + \sum_{w=u+1}^{k_v} q_v(u,w) }{ \sum_{u=1}^{k_v} + \sum_{w=u+1}^{k_v} [a_v(u,w) + q_v(u,w)]}, + + where :math:`q_v(u,w)` are the number of common neighbors of :math:`u` and + :math:`w` other than :math:`v` (ie squares), and :math:`a_v(u,w) = (k_u - + (1+q_v(u,w)+\theta_{uv})) + (k_w - (1+q_v(u,w)+\theta_{uw}))`, where + :math:`\theta_{uw} = 1` if :math:`u` and :math:`w` are connected and 0 + otherwise. [2]_ + + Parameters + ---------- + G : graph + + nodes : container of nodes, optional (default=all nodes in G) + Compute clustering for nodes in this container. + + Returns + ------- + c4 : dictionary + A dictionary keyed by node with the square clustering coefficient value. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.square_clustering(G, 0)) + 1.0 + >>> print(nx.square_clustering(G)) + {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0} + + Notes + ----- + Self loops are ignored. + + While :math:`C_3(v)` (triangle clustering) gives the probability that + two neighbors of node v are connected with each other, :math:`C_4(v)` is + the probability that two neighbors of node v share a common + neighbor different from v. This algorithm can be applied to both + bipartite and unipartite networks. + + References + ---------- + .. [1] Pedro G. Lind, Marta C. González, and Hans J. Herrmann. 2005 + Cycles and clustering in bipartite networks. + Physical Review E (72) 056127. + .. [2] Zhang, Peng et al. Clustering Coefficient and Community Structure of + Bipartite Networks. Physica A: Statistical Mechanics and its Applications 387.27 (2008): 6869–6875. + https://arxiv.org/abs/0710.0117v1 + """ + if nodes is None: + node_iter = G + else: + node_iter = G.nbunch_iter(nodes) + clustering = {} + _G_adj = G._adj + + class GAdj(dict): + """Calculate (and cache) node neighbor sets excluding self-loops.""" + + def __missing__(self, v): + v_neighbors = self[v] = set(_G_adj[v]) + v_neighbors.discard(v) # Ignore self-loops + return v_neighbors + + G_adj = GAdj() # Values are sets of neighbors (no self-loops) + + for v in node_iter: + v_neighbors = G_adj[v] + v_degrees_m1 = len(v_neighbors) - 1 # degrees[v] - 1 (used below) + if v_degrees_m1 <= 0: + # Can't form a square without at least two neighbors + clustering[v] = 0 + continue + + # Count squares with nodes u-v-w-x from the current node v. + # Terms of the denominator: potential = uw_degrees - uw_count - triangles - squares + # uw_degrees: degrees[u] + degrees[w] for each u-w combo + uw_degrees = 0 + # uw_count: 1 for each u and 1 for each w for all combos (degrees * (degrees - 1)) + uw_count = len(v_neighbors) * v_degrees_m1 + # triangles: 1 for each edge where u-w or w-u are connected (i.e. triangles) + triangles = 0 + # squares: the number of squares (also the numerator) + squares = 0 + + # Iterate over all neighbors + for u in v_neighbors: + u_neighbors = G_adj[u] + uw_degrees += len(u_neighbors) * v_degrees_m1 + # P2 from https://arxiv.org/abs/2007.11111 + p2 = len(u_neighbors & v_neighbors) + # triangles is C_3, sigma_4 from https://arxiv.org/abs/2007.11111 + # This double-counts triangles compared to `triangles` function + triangles += p2 + # squares is C_4, sigma_12 from https://arxiv.org/abs/2007.11111 + # Include this term, b/c a neighbor u can also be a neighbor of neighbor x + squares += p2 * (p2 - 1) # Will divide by 2 later + + # And iterate over all neighbors of neighbors. + # These nodes x may be the corners opposite v in squares u-v-w-x. + two_hop_neighbors = set.union(*(G_adj[u] for u in v_neighbors)) + two_hop_neighbors -= v_neighbors # Neighbors already counted above + two_hop_neighbors.discard(v) + for x in two_hop_neighbors: + p2 = len(v_neighbors & G_adj[x]) + squares += p2 * (p2 - 1) # Will divide by 2 later + + squares //= 2 + potential = uw_degrees - uw_count - triangles - squares + if potential > 0: + clustering[v] = squares / potential + else: + clustering[v] = 0 + if nodes in G: + # Return the value of the sole entry in the dictionary. + return clustering[nodes] + return clustering + + +@not_implemented_for("directed") +@nx._dispatchable +def generalized_degree(G, nodes=None): + r"""Compute the generalized degree for nodes. + + For each node, the generalized degree shows how many edges of given + triangle multiplicity the node is connected to. The triangle multiplicity + of an edge is the number of triangles an edge participates in. The + generalized degree of node :math:`i` can be written as a vector + :math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc, k_i^{(N-2)})` where + :math:`k_i^{(j)}` is the number of edges attached to node :math:`i` that + participate in :math:`j` triangles. + + Parameters + ---------- + G : graph + + nodes : container of nodes, optional (default=all nodes in G) + Compute the generalized degree for nodes in this container. + + Returns + ------- + out : Counter, or dictionary of Counters + Generalized degree of specified nodes. The Counter is keyed by edge + triangle multiplicity. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> print(nx.generalized_degree(G, 0)) + Counter({3: 4}) + >>> print(nx.generalized_degree(G)) + {0: Counter({3: 4}), 1: Counter({3: 4}), 2: Counter({3: 4}), 3: Counter({3: 4}), 4: Counter({3: 4})} + + To recover the number of triangles attached to a node: + + >>> k1 = nx.generalized_degree(G, 0) + >>> sum([k * v for k, v in k1.items()]) / 2 == nx.triangles(G, 0) + True + + Notes + ----- + Self loops are ignored. + + In a network of N nodes, the highest triangle multiplicity an edge can have + is N-2. + + The return value does not include a `zero` entry if no edges of a + particular triangle multiplicity are present. + + The number of triangles node :math:`i` is attached to can be recovered from + the generalized degree :math:`\mathbf{k}_i=(k_i^{(0)}, \dotsc, + k_i^{(N-2)})` by :math:`(k_i^{(1)}+2k_i^{(2)}+\dotsc +(N-2)k_i^{(N-2)})/2`. + + References + ---------- + .. [1] Networks with arbitrary edge multiplicities by V. Zlatić, + D. Garlaschelli and G. Caldarelli, EPL (Europhysics Letters), + Volume 97, Number 2 (2012). + https://iopscience.iop.org/article/10.1209/0295-5075/97/28005 + """ + if nodes in G: + return next(_triangles_and_degree_iter(G, nodes))[3] + return {v: gd for v, d, t, gd in _triangles_and_degree_iter(G, nodes)} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/communicability_alg.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/communicability_alg.py new file mode 100644 index 0000000000000000000000000000000000000000..dea156b633a2b367c184f4bf31ab465812de68b4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/communicability_alg.py @@ -0,0 +1,163 @@ +""" +Communicability. +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["communicability", "communicability_exp"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def communicability(G): + r"""Returns communicability between all pairs of nodes in G. + + The communicability between pairs of nodes in G is the sum of + walks of different lengths starting at node u and ending at node v. + + Parameters + ---------- + G: graph + + Returns + ------- + comm: dictionary of dictionaries + Dictionary of dictionaries keyed by nodes with communicability + as the value. + + Raises + ------ + NetworkXError + If the graph is not undirected and simple. + + See Also + -------- + communicability_exp: + Communicability between all pairs of nodes in G using spectral + decomposition. + communicability_betweenness_centrality: + Communicability betweenness centrality for each node in G. + + Notes + ----- + This algorithm uses a spectral decomposition of the adjacency matrix. + Let G=(V,E) be a simple undirected graph. Using the connection between + the powers of the adjacency matrix and the number of walks in the graph, + the communicability between nodes `u` and `v` based on the graph spectrum + is [1]_ + + .. math:: + C(u,v)=\sum_{j=1}^{n}\phi_{j}(u)\phi_{j}(v)e^{\lambda_{j}}, + + where `\phi_{j}(u)` is the `u\rm{th}` element of the `j\rm{th}` orthonormal + eigenvector of the adjacency matrix associated with the eigenvalue + `\lambda_{j}`. + + References + ---------- + .. [1] Ernesto Estrada, Naomichi Hatano, + "Communicability in complex networks", + Phys. Rev. E 77, 036111 (2008). + https://arxiv.org/abs/0707.0756 + + Examples + -------- + >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)]) + >>> c = nx.communicability(G) + """ + import numpy as np + + nodelist = list(G) # ordering of nodes in matrix + A = nx.to_numpy_array(G, nodelist) + # convert to 0-1 matrix + A[A != 0.0] = 1 + w, vec = np.linalg.eigh(A) + expw = np.exp(w) + mapping = dict(zip(nodelist, range(len(nodelist)))) + c = {} + # computing communicabilities + for u in G: + c[u] = {} + for v in G: + s = 0 + p = mapping[u] + q = mapping[v] + for j in range(len(nodelist)): + s += vec[:, j][p] * vec[:, j][q] * expw[j] + c[u][v] = float(s) + return c + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def communicability_exp(G): + r"""Returns communicability between all pairs of nodes in G. + + Communicability between pair of node (u,v) of node in G is the sum of + walks of different lengths starting at node u and ending at node v. + + Parameters + ---------- + G: graph + + Returns + ------- + comm: dictionary of dictionaries + Dictionary of dictionaries keyed by nodes with communicability + as the value. + + Raises + ------ + NetworkXError + If the graph is not undirected and simple. + + See Also + -------- + communicability: + Communicability between pairs of nodes in G. + communicability_betweenness_centrality: + Communicability betweenness centrality for each node in G. + + Notes + ----- + This algorithm uses matrix exponentiation of the adjacency matrix. + + Let G=(V,E) be a simple undirected graph. Using the connection between + the powers of the adjacency matrix and the number of walks in the graph, + the communicability between nodes u and v is [1]_, + + .. math:: + C(u,v) = (e^A)_{uv}, + + where `A` is the adjacency matrix of G. + + References + ---------- + .. [1] Ernesto Estrada, Naomichi Hatano, + "Communicability in complex networks", + Phys. Rev. E 77, 036111 (2008). + https://arxiv.org/abs/0707.0756 + + Examples + -------- + >>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)]) + >>> c = nx.communicability_exp(G) + """ + import scipy as sp + + nodelist = list(G) # ordering of nodes in matrix + A = nx.to_numpy_array(G, nodelist) + # convert to 0-1 matrix + A[A != 0.0] = 1 + # communicability matrix + expA = sp.linalg.expm(A) + mapping = dict(zip(nodelist, range(len(nodelist)))) + c = {} + for u in G: + c[u] = {} + for v in G: + c[u][v] = float(expA[mapping[u], mapping[v]]) + return c diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f9ae2caba856daba534037f4a6f967abfad49552 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/__init__.py @@ -0,0 +1,6 @@ +from .connected import * +from .strongly_connected import * +from .weakly_connected import * +from .attracting import * +from .biconnected import * +from .semiconnected import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/attracting.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/attracting.py new file mode 100644 index 0000000000000000000000000000000000000000..3d77cd93d70efab5f29c77c7d135f4730e4c3a4a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/attracting.py @@ -0,0 +1,115 @@ +"""Attracting components.""" + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +__all__ = [ + "number_attracting_components", + "attracting_components", + "is_attracting_component", +] + + +@not_implemented_for("undirected") +@nx._dispatchable +def attracting_components(G): + """Generates the attracting components in `G`. + + An attracting component in a directed graph `G` is a strongly connected + component with the property that a random walker on the graph will never + leave the component, once it enters the component. + + The nodes in attracting components can also be thought of as recurrent + nodes. If a random walker enters the attractor containing the node, then + the node will be visited infinitely often. + + To obtain induced subgraphs on each component use: + ``(G.subgraph(c).copy() for c in attracting_components(G))`` + + Parameters + ---------- + G : DiGraph, MultiDiGraph + The graph to be analyzed. + + Returns + ------- + attractors : generator of sets + A generator of sets of nodes, one for each attracting component of G. + + Raises + ------ + NetworkXNotImplemented + If the input graph is undirected. + + See Also + -------- + number_attracting_components + is_attracting_component + + """ + scc = list(nx.strongly_connected_components(G)) + cG = nx.condensation(G, scc) + for n in cG: + if cG.out_degree(n) == 0: + yield scc[n] + + +@not_implemented_for("undirected") +@nx._dispatchable +def number_attracting_components(G): + """Returns the number of attracting components in `G`. + + Parameters + ---------- + G : DiGraph, MultiDiGraph + The graph to be analyzed. + + Returns + ------- + n : int + The number of attracting components in G. + + Raises + ------ + NetworkXNotImplemented + If the input graph is undirected. + + See Also + -------- + attracting_components + is_attracting_component + + """ + return sum(1 for ac in attracting_components(G)) + + +@not_implemented_for("undirected") +@nx._dispatchable +def is_attracting_component(G): + """Returns True if `G` consists of a single attracting component. + + Parameters + ---------- + G : DiGraph, MultiDiGraph + The graph to be analyzed. + + Returns + ------- + attracting : bool + True if `G` has a single attracting component. Otherwise, False. + + Raises + ------ + NetworkXNotImplemented + If the input graph is undirected. + + See Also + -------- + attracting_components + number_attracting_components + + """ + ac = list(attracting_components(G)) + if len(ac) == 1: + return len(ac[0]) == len(G) + return False diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/connected.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/connected.py new file mode 100644 index 0000000000000000000000000000000000000000..0dd009a8220b08ce47c71afe8239958786f09209 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/connected.py @@ -0,0 +1,282 @@ +"""Connected components.""" + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +from ...utils import arbitrary_element + +__all__ = [ + "number_connected_components", + "connected_components", + "is_connected", + "node_connected_component", +] + + +@not_implemented_for("directed") +@nx._dispatchable +def connected_components(G): + """Generate connected components. + + The connected components of an undirected graph partition the graph into + disjoint sets of nodes. Each of these sets induces a subgraph of graph + `G` that is connected and not part of any larger connected subgraph. + + A graph is connected (:func:`is_connected`) if, for every pair of distinct + nodes, there is a path between them. If there is a pair of nodes for + which such path does not exist, the graph is not connected (also referred + to as "disconnected"). + + A graph consisting of a single node and no edges is connected. + Connectivity is undefined for the null graph (graph with no nodes). + + Parameters + ---------- + G : NetworkX graph + An undirected graph + + Yields + ------ + comp : set + A set of nodes in one connected component of the graph. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + Examples + -------- + Generate a sorted list of connected components, largest first. + + >>> G = nx.path_graph(4) + >>> nx.add_path(G, [10, 11, 12]) + >>> [len(c) for c in sorted(nx.connected_components(G), key=len, reverse=True)] + [4, 3] + + If you only want the largest connected component, it's more + efficient to use max instead of sort. + + >>> largest_cc = max(nx.connected_components(G), key=len) + + To create the induced subgraph of each component use: + + >>> S = [G.subgraph(c).copy() for c in nx.connected_components(G)] + + See Also + -------- + number_connected_components + is_connected + number_weakly_connected_components + number_strongly_connected_components + + Notes + ----- + This function is for undirected graphs only. For directed graphs, use + :func:`strongly_connected_components` or + :func:`weakly_connected_components`. + + The algorithm is based on a Breadth-First Search (BFS) traversal and its + time complexity is $O(n + m)$, where $n$ is the number of nodes and $m$ the + number of edges in the graph. + + """ + seen = set() + n = len(G) # must be outside the loop to avoid performance hit with graph views + for v in G: + if v not in seen: + c = _plain_bfs(G, n - len(seen), v) + seen.update(c) + yield c + + +@not_implemented_for("directed") +@nx._dispatchable +def number_connected_components(G): + """Returns the number of connected components. + + The connected components of an undirected graph partition the graph into + disjoint sets of nodes. Each of these sets induces a subgraph of graph + `G` that is connected and not part of any larger connected subgraph. + + A graph is connected (:func:`is_connected`) if, for every pair of distinct + nodes, there is a path between them. If there is a pair of nodes for + which such path does not exist, the graph is not connected (also referred + to as "disconnected"). + + A graph consisting of a single node and no edges is connected. + Connectivity is undefined for the null graph (graph with no nodes). + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + Returns + ------- + n : integer + Number of connected components + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (1, 2), (5, 6), (3, 4)]) + >>> nx.number_connected_components(G) + 3 + + See Also + -------- + connected_components + is_connected + number_weakly_connected_components + number_strongly_connected_components + + Notes + ----- + This function is for undirected graphs only. For directed graphs, use + :func:`number_strongly_connected_components` or + :func:`number_weakly_connected_components`. + + The algorithm is based on a Breadth-First Search (BFS) traversal and its + time complexity is $O(n + m)$, where $n$ is the number of nodes and $m$ the + number of edges in the graph. + + """ + return sum(1 for _ in connected_components(G)) + + +@not_implemented_for("directed") +@nx._dispatchable +def is_connected(G): + """Returns True if the graph is connected, False otherwise. + + A graph is connected if, for every pair of distinct nodes, there is a + path between them. If there is a pair of nodes for which such path does + not exist, the graph is not connected (also referred to as "disconnected"). + + A graph consisting of a single node and no edges is connected. + Connectivity is undefined for the null graph (graph with no nodes). + + Parameters + ---------- + G : NetworkX Graph + An undirected graph. + + Returns + ------- + connected : bool + True if the graph is connected, False otherwise. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> print(nx.is_connected(G)) + True + + See Also + -------- + is_strongly_connected + is_weakly_connected + is_semiconnected + is_biconnected + connected_components + + Notes + ----- + This function is for undirected graphs only. For directed graphs, use + :func:`is_strongly_connected` or :func:`is_weakly_connected`. + + The algorithm is based on a Breadth-First Search (BFS) traversal and its + time complexity is $O(n + m)$, where $n$ is the number of nodes and $m$ the + number of edges in the graph. + + """ + n = len(G) + if n == 0: + raise nx.NetworkXPointlessConcept( + "Connectivity is undefined for the null graph." + ) + return len(next(connected_components(G))) == n + + +@not_implemented_for("directed") +@nx._dispatchable +def node_connected_component(G, n): + """Returns the set of nodes in the component of graph containing node n. + + A connected component is a set of nodes that induces a subgraph of graph + `G` that is connected and not part of any larger connected subgraph. + + A graph is connected (:func:`is_connected`) if, for every pair of distinct + nodes, there is a path between them. If there is a pair of nodes for + which such path does not exist, the graph is not connected (also referred + to as "disconnected"). + + A graph consisting of a single node and no edges is connected. + Connectivity is undefined for the null graph (graph with no nodes). + + Parameters + ---------- + G : NetworkX Graph + An undirected graph. + + n : node label + A node in G + + Returns + ------- + comp : set + A set of nodes in the component of G containing node n. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (1, 2), (5, 6), (3, 4)]) + >>> nx.node_connected_component(G, 0) # nodes of component that contains node 0 + {0, 1, 2} + + See Also + -------- + connected_components + + Notes + ----- + This function is for undirected graphs only. + + The algorithm is based on a Breadth-First Search (BFS) traversal and its + time complexity is $O(n + m)$, where $n$ is the number of nodes and $m$ the + number of edges in the graph. + + """ + return _plain_bfs(G, len(G), n) + + +def _plain_bfs(G, n, source): + """A fast BFS node generator""" + adj = G._adj + seen = {source} + nextlevel = [source] + while nextlevel: + thislevel = nextlevel + nextlevel = [] + for v in thislevel: + for w in adj[v]: + if w not in seen: + seen.add(w) + nextlevel.append(w) + if len(seen) == n: + return seen + return seen diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/strongly_connected.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/strongly_connected.py new file mode 100644 index 0000000000000000000000000000000000000000..a69a6c8801fe896bd0bf6813637072480e1a28d5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/strongly_connected.py @@ -0,0 +1,359 @@ +"""Strongly connected components.""" + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +__all__ = [ + "number_strongly_connected_components", + "strongly_connected_components", + "is_strongly_connected", + "kosaraju_strongly_connected_components", + "condensation", +] + + +@not_implemented_for("undirected") +@nx._dispatchable +def strongly_connected_components(G): + """Generate nodes in strongly connected components of graph. + + Parameters + ---------- + G : NetworkX Graph + A directed graph. + + Returns + ------- + comp : generator of sets + A generator of sets of nodes, one for each strongly connected + component of G. + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + Generate a sorted list of strongly connected components, largest first. + + >>> G = nx.cycle_graph(4, create_using=nx.DiGraph()) + >>> nx.add_cycle(G, [10, 11, 12]) + >>> [ + ... len(c) + ... for c in sorted(nx.strongly_connected_components(G), key=len, reverse=True) + ... ] + [4, 3] + + If you only want the largest component, it's more efficient to + use max instead of sort. + + >>> largest = max(nx.strongly_connected_components(G), key=len) + + See Also + -------- + connected_components + weakly_connected_components + kosaraju_strongly_connected_components + + Notes + ----- + Uses Tarjan's algorithm[1]_ with Nuutila's modifications[2]_. + Nonrecursive version of algorithm. + + References + ---------- + .. [1] Depth-first search and linear graph algorithms, R. Tarjan + SIAM Journal of Computing 1(2):146-160, (1972). + + .. [2] On finding the strongly connected components in a directed graph. + E. Nuutila and E. Soisalon-Soinen + Information Processing Letters 49(1): 9-14, (1994).. + + """ + preorder = {} + lowlink = {} + scc_found = set() + scc_queue = [] + i = 0 # Preorder counter + neighbors = {v: iter(G._adj[v]) for v in G} + for source in G: + if source not in scc_found: + queue = [source] + while queue: + v = queue[-1] + if v not in preorder: + i = i + 1 + preorder[v] = i + done = True + for w in neighbors[v]: + if w not in preorder: + queue.append(w) + done = False + break + if done: + lowlink[v] = preorder[v] + for w in G._adj[v]: + if w not in scc_found: + if preorder[w] > preorder[v]: + lowlink[v] = min([lowlink[v], lowlink[w]]) + else: + lowlink[v] = min([lowlink[v], preorder[w]]) + queue.pop() + if lowlink[v] == preorder[v]: + scc = {v} + while scc_queue and preorder[scc_queue[-1]] > preorder[v]: + k = scc_queue.pop() + scc.add(k) + scc_found.update(scc) + yield scc + else: + scc_queue.append(v) + + +@not_implemented_for("undirected") +@nx._dispatchable +def kosaraju_strongly_connected_components(G, source=None): + """Generate nodes in strongly connected components of graph. + + Parameters + ---------- + G : NetworkX Graph + A directed graph. + + source : node, optional (default=None) + Specify a node from which to start the depth-first search. + If not provided, the algorithm will start from an arbitrary node. + + Yields + ------ + set + A set of all nodes in a strongly connected component of `G`. + + Raises + ------ + NetworkXNotImplemented + If `G` is undirected. + + NetworkXError + If `source` is not a node in `G`. + + Examples + -------- + Generate a list of strongly connected components of a graph: + + >>> G = nx.cycle_graph(4, create_using=nx.DiGraph()) + >>> nx.add_cycle(G, [10, 11, 12]) + >>> sorted(nx.kosaraju_strongly_connected_components(G), key=len, reverse=True) + [{0, 1, 2, 3}, {10, 11, 12}] + + If you only want the largest component, it's more efficient to + use `max()` instead of `sorted()`. + + >>> max(nx.kosaraju_strongly_connected_components(G), key=len) + {0, 1, 2, 3} + + See Also + -------- + strongly_connected_components + + Notes + ----- + Uses Kosaraju's algorithm. + """ + post = list(nx.dfs_postorder_nodes(G.reverse(copy=False), source=source)) + n = len(post) + seen = set() + while post and len(seen) < n: + r = post.pop() + if r in seen: + continue + new = {r} + seen.add(r) + stack = [r] + while stack and len(seen) < n: + v = stack.pop() + for w in G._adj[v]: + if w not in seen: + new.add(w) + seen.add(w) + stack.append(w) + yield new + + +@not_implemented_for("undirected") +@nx._dispatchable +def number_strongly_connected_components(G): + """Returns number of strongly connected components in graph. + + Parameters + ---------- + G : NetworkX graph + A directed graph. + + Returns + ------- + n : integer + Number of strongly connected components + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + >>> G = nx.DiGraph( + ... [(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)] + ... ) + >>> nx.number_strongly_connected_components(G) + 3 + + See Also + -------- + strongly_connected_components + number_connected_components + number_weakly_connected_components + + Notes + ----- + For directed graphs only. + """ + return sum(1 for scc in strongly_connected_components(G)) + + +@not_implemented_for("undirected") +@nx._dispatchable +def is_strongly_connected(G): + """Test directed graph for strong connectivity. + + A directed graph is strongly connected if and only if every vertex in + the graph is reachable from every other vertex. + + Parameters + ---------- + G : NetworkX Graph + A directed graph. + + Returns + ------- + connected : bool + True if the graph is strongly connected, False otherwise. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (4, 2)]) + >>> nx.is_strongly_connected(G) + True + >>> G.remove_edge(2, 3) + >>> nx.is_strongly_connected(G) + False + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + See Also + -------- + is_weakly_connected + is_semiconnected + is_connected + is_biconnected + strongly_connected_components + + Notes + ----- + For directed graphs only. + """ + if len(G) == 0: + raise nx.NetworkXPointlessConcept( + """Connectivity is undefined for the null graph.""" + ) + + return len(next(strongly_connected_components(G))) == len(G) + + +@not_implemented_for("undirected") +@nx._dispatchable(returns_graph=True) +def condensation(G, scc=None): + """Returns the condensation of G. + + The condensation of G is the graph with each of the strongly connected + components contracted into a single node. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph. + + scc: list or generator (optional, default=None) + Strongly connected components. If provided, the elements in + `scc` must partition the nodes in `G`. If not provided, it will be + calculated as scc=nx.strongly_connected_components(G). + + Returns + ------- + C : NetworkX DiGraph + The condensation graph C of G. The node labels are integers + corresponding to the index of the component in the list of + strongly connected components of G. C has a graph attribute named + 'mapping' with a dictionary mapping the original nodes to the + nodes in C to which they belong. Each node in C also has a node + attribute 'members' with the set of original nodes in G that + form the SCC that the node in C represents. + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + Contracting two sets of strongly connected nodes into two distinct SCC + using the barbell graph. + + >>> G = nx.barbell_graph(4, 0) + >>> G.remove_edge(3, 4) + >>> G = nx.DiGraph(G) + >>> H = nx.condensation(G) + >>> H.nodes.data() + NodeDataView({0: {'members': {0, 1, 2, 3}}, 1: {'members': {4, 5, 6, 7}}}) + >>> H.graph["mapping"] + {0: 0, 1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 1, 7: 1} + + Contracting a complete graph into one single SCC. + + >>> G = nx.complete_graph(7, create_using=nx.DiGraph) + >>> H = nx.condensation(G) + >>> H.nodes + NodeView((0,)) + >>> H.nodes.data() + NodeDataView({0: {'members': {0, 1, 2, 3, 4, 5, 6}}}) + + Notes + ----- + After contracting all strongly connected components to a single node, + the resulting graph is a directed acyclic graph. + + """ + if scc is None: + scc = nx.strongly_connected_components(G) + mapping = {} + members = {} + C = nx.DiGraph() + # Add mapping dict as graph attribute + C.graph["mapping"] = mapping + if len(G) == 0: + return C + for i, component in enumerate(scc): + members[i] = component + mapping.update((n, i) for n in component) + number_of_components = i + 1 + C.add_nodes_from(range(number_of_components)) + C.add_edges_from( + (mapping[u], mapping[v]) for u, v in G.edges() if mapping[u] != mapping[v] + ) + # Add a list of members (ie original nodes) to each node (ie scc) in C. + nx.set_node_attributes(C, members, "members") + return C diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/weakly_connected.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/weakly_connected.py new file mode 100644 index 0000000000000000000000000000000000000000..fdb07ff7de5d93b1b01889217d5d8698bbf7fa32 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/components/weakly_connected.py @@ -0,0 +1,196 @@ +"""Weakly connected components.""" + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +__all__ = [ + "number_weakly_connected_components", + "weakly_connected_components", + "is_weakly_connected", +] + + +@not_implemented_for("undirected") +@nx._dispatchable +def weakly_connected_components(G): + """Generate weakly connected components of G. + + Parameters + ---------- + G : NetworkX graph + A directed graph + + Returns + ------- + comp : generator of sets + A generator of sets of nodes, one for each weakly connected + component of G. + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + Generate a sorted list of weakly connected components, largest first. + + >>> G = nx.path_graph(4, create_using=nx.DiGraph()) + >>> nx.add_path(G, [10, 11, 12]) + >>> [ + ... len(c) + ... for c in sorted(nx.weakly_connected_components(G), key=len, reverse=True) + ... ] + [4, 3] + + If you only want the largest component, it's more efficient to + use max instead of sort: + + >>> largest_cc = max(nx.weakly_connected_components(G), key=len) + + See Also + -------- + connected_components + strongly_connected_components + + Notes + ----- + For directed graphs only. + + """ + seen = set() + n = len(G) # must be outside the loop to avoid performance hit with graph views + for v in G: + if v not in seen: + c = _plain_bfs(G, n - len(seen), v) + seen.update(c) + yield c + + +@not_implemented_for("undirected") +@nx._dispatchable +def number_weakly_connected_components(G): + """Returns the number of weakly connected components in G. + + Parameters + ---------- + G : NetworkX graph + A directed graph. + + Returns + ------- + n : integer + Number of weakly connected components + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (2, 1), (3, 4)]) + >>> nx.number_weakly_connected_components(G) + 2 + + See Also + -------- + weakly_connected_components + number_connected_components + number_strongly_connected_components + + Notes + ----- + For directed graphs only. + + """ + return sum(1 for _ in weakly_connected_components(G)) + + +@not_implemented_for("undirected") +@nx._dispatchable +def is_weakly_connected(G): + """Test directed graph for weak connectivity. + + A directed graph is weakly connected if and only if the graph + is connected when the direction of the edge between nodes is ignored. + + Note that if a graph is strongly connected (i.e. the graph is connected + even when we account for directionality), it is by definition weakly + connected as well. + + Parameters + ---------- + G : NetworkX Graph + A directed graph. + + Returns + ------- + connected : bool + True if the graph is weakly connected, False otherwise. + + Raises + ------ + NetworkXNotImplemented + If G is undirected. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (2, 1)]) + >>> G.add_node(3) + >>> nx.is_weakly_connected(G) # node 3 is not connected to the graph + False + >>> G.add_edge(2, 3) + >>> nx.is_weakly_connected(G) + True + + See Also + -------- + is_strongly_connected + is_semiconnected + is_connected + is_biconnected + weakly_connected_components + + Notes + ----- + For directed graphs only. + + """ + n = len(G) + if n == 0: + raise nx.NetworkXPointlessConcept( + """Connectivity is undefined for the null graph.""" + ) + + return len(next(weakly_connected_components(G))) == n + + +def _plain_bfs(G, n, source): + """A fast BFS node generator + + The direction of the edge between nodes is ignored. + + For directed graphs only. + + """ + Gsucc = G._succ + Gpred = G._pred + seen = {source} + nextlevel = [source] + + while nextlevel: + thislevel = nextlevel + nextlevel = [] + for v in thislevel: + for w in Gsucc[v]: + if w not in seen: + seen.add(w) + nextlevel.append(w) + for w in Gpred[v]: + if w not in seen: + seen.add(w) + nextlevel.append(w) + if len(seen) == n: + return seen + return seen diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d08a360628d4604bb37d350746e5c9796fe31d06 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/__init__.py @@ -0,0 +1,11 @@ +"""Connectivity and cut algorithms""" + +from .connectivity import * +from .cuts import * +from .edge_augmentation import * +from .edge_kcomponents import * +from .disjoint_paths import * +from .kcomponents import * +from .kcutsets import * +from .stoerwagner import * +from .utils import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..210413fb7a5098e76331a04ea1a090f8d555f0b7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/connectivity.py @@ -0,0 +1,811 @@ +""" +Flow based connectivity algorithms +""" + +import itertools +from operator import itemgetter + +import networkx as nx + +# Define the default maximum flow function to use in all flow based +# connectivity algorithms. +from networkx.algorithms.flow import ( + boykov_kolmogorov, + build_residual_network, + dinitz, + edmonds_karp, + preflow_push, + shortest_augmenting_path, +) + +from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity + +default_flow_func = edmonds_karp + +__all__ = [ + "average_node_connectivity", + "local_node_connectivity", + "node_connectivity", + "local_edge_connectivity", + "edge_connectivity", + "all_pairs_node_connectivity", +] + + +@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4}, preserve_graph_attrs={"auxiliary"}) +def local_node_connectivity( + G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None +): + r"""Computes local node connectivity for nodes s and t. + + Local node connectivity for two non adjacent nodes s and t is the + minimum number of nodes that must be removed (along with their incident + edges) to disconnect them. + + This is a flow based implementation of node connectivity. We compute the + maximum flow on an auxiliary digraph build from the original input + graph (see below for details). + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + s : node + Source node + + t : node + Target node + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The choice + of the default function may change from version to version and + should not be relied on. Default value: None. + + auxiliary : NetworkX DiGraph + Auxiliary digraph to compute flow based node connectivity. It has + to have a graph attribute called mapping with a dictionary mapping + node names in G and in the auxiliary digraph. If provided + it will be reused instead of recreated. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + cutoff : integer, float, or None (default: None) + If specified, the maximum flow algorithm will terminate when the + flow value reaches or exceeds the cutoff. This only works for flows + that support the cutoff parameter (most do) and is ignored otherwise. + + Returns + ------- + K : integer + local node connectivity for nodes s and t + + Examples + -------- + This function is not imported in the base NetworkX namespace, so you + have to explicitly import it from the connectivity package: + + >>> from networkx.algorithms.connectivity import local_node_connectivity + + We use in this example the platonic icosahedral graph, which has node + connectivity 5. + + >>> G = nx.icosahedral_graph() + >>> local_node_connectivity(G, 0, 6) + 5 + + If you need to compute local connectivity on several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for node connectivity, and the residual + network for the underlying maximum flow computation. + + Example of how to compute local node connectivity among + all pairs of nodes of the platonic icosahedral graph reusing + the data structures. + + >>> import itertools + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity + >>> H = build_auxiliary_node_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> result = dict.fromkeys(G, dict()) + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as parameters + >>> for u, v in itertools.combinations(G, 2): + ... k = local_node_connectivity(G, u, v, auxiliary=H, residual=R) + ... result[u][v] = k + >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2)) + True + + You can also use alternative flow algorithms for computing node + connectivity. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> local_node_connectivity(G, 0, 6, flow_func=shortest_augmenting_path) + 5 + + Notes + ----- + This is a flow based implementation of node connectivity. We compute the + maximum flow using, by default, the :meth:`edmonds_karp` algorithm (see: + :meth:`maximum_flow`) on an auxiliary digraph build from the original + input graph: + + For an undirected graph G having `n` nodes and `m` edges we derive a + directed graph H with `2n` nodes and `2m+n` arcs by replacing each + original node `v` with two nodes `v_A`, `v_B` linked by an (internal) + arc in H. Then for each edge (`u`, `v`) in G we add two arcs + (`u_B`, `v_A`) and (`v_B`, `u_A`) in H. Finally we set the attribute + capacity = 1 for each arc in H [1]_ . + + For a directed graph G having `n` nodes and `m` arcs we derive a + directed graph H with `2n` nodes and `m+n` arcs by replacing each + original node `v` with two nodes `v_A`, `v_B` linked by an (internal) + arc (`v_A`, `v_B`) in H. Then for each arc (`u`, `v`) in G we add one arc + (`u_B`, `v_A`) in H. Finally we set the attribute capacity = 1 for + each arc in H. + + This is equal to the local node connectivity because the value of + a maximum s-t-flow is equal to the capacity of a minimum s-t-cut. + + See also + -------- + :meth:`local_edge_connectivity` + :meth:`node_connectivity` + :meth:`minimum_node_cut` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and + Erlebach, 'Network Analysis: Methodological Foundations', Lecture + Notes in Computer Science, Volume 3418, Springer-Verlag, 2005. + http://www.informatik.uni-augsburg.de/thi/personen/kammer/Graph_Connectivity.pdf + + """ + if flow_func is None: + flow_func = default_flow_func + + if auxiliary is None: + H = build_auxiliary_node_connectivity(G) + else: + H = auxiliary + + mapping = H.graph.get("mapping", None) + if mapping is None: + raise nx.NetworkXError("Invalid auxiliary digraph.") + + kwargs = {"flow_func": flow_func, "residual": residual} + + if flow_func is not preflow_push: + kwargs["cutoff"] = cutoff + + if flow_func is shortest_augmenting_path: + kwargs["two_phase"] = True + + return nx.maximum_flow_value(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs) + + +@nx._dispatchable +def node_connectivity(G, s=None, t=None, flow_func=None): + r"""Returns node connectivity for a graph or digraph G. + + Node connectivity is equal to the minimum number of nodes that + must be removed to disconnect G or render it trivial. If source + and target nodes are provided, this function returns the local node + connectivity: the minimum number of nodes that must be removed to break + all paths from source to target in G. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + s : node + Source node. Optional. Default value: None. + + t : node + Target node. Optional. Default value: None. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + Returns + ------- + K : integer + Node connectivity of G, or local node connectivity if source + and target are provided. + + Examples + -------- + >>> # Platonic icosahedral graph is 5-node-connected + >>> G = nx.icosahedral_graph() + >>> nx.node_connectivity(G) + 5 + + You can use alternative flow algorithms for the underlying maximum + flow computation. In dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better + than the default :meth:`edmonds_karp`, which is faster for + sparse networks with highly skewed degree distributions. Alternative + flow functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> nx.node_connectivity(G, flow_func=shortest_augmenting_path) + 5 + + If you specify a pair of nodes (source and target) as parameters, + this function returns the value of local node connectivity. + + >>> nx.node_connectivity(G, 3, 7) + 5 + + If you need to perform several local computations among different + pairs of nodes on the same graph, it is recommended that you reuse + the data structures used in the maximum flow computations. See + :meth:`local_node_connectivity` for details. + + Notes + ----- + This is a flow based implementation of node connectivity. The + algorithm works by solving $O((n-\delta-1+\delta(\delta-1)/2))$ + maximum flow problems on an auxiliary digraph. Where $\delta$ + is the minimum degree of G. For details about the auxiliary + digraph and the computation of local node connectivity see + :meth:`local_node_connectivity`. This implementation is based + on algorithm 11 in [1]_. + + See also + -------- + :meth:`local_node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if (s is not None and t is None) or (s is None and t is not None): + raise nx.NetworkXError("Both source and target must be specified.") + + # Local node connectivity + if s is not None and t is not None: + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + return local_node_connectivity(G, s, t, flow_func=flow_func) + + # Global node connectivity + if G.is_directed(): + if not nx.is_weakly_connected(G): + return 0 + iter_func = itertools.permutations + # It is necessary to consider both predecessors + # and successors for directed graphs + + def neighbors(v): + return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)]) + + else: + if not nx.is_connected(G): + return 0 + iter_func = itertools.combinations + neighbors = G.neighbors + + # Reuse the auxiliary digraph and the residual network + H = build_auxiliary_node_connectivity(G) + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} + + # Pick a node with minimum degree + # Node connectivity is bounded by degree. + v, K = min(G.degree(), key=itemgetter(1)) + # compute local node connectivity with all its non-neighbors nodes + for w in set(G) - set(neighbors(v)) - {v}: + kwargs["cutoff"] = K + K = min(K, local_node_connectivity(G, v, w, **kwargs)) + # Also for non adjacent pairs of neighbors of v + for x, y in iter_func(neighbors(v), 2): + if y in G[x]: + continue + kwargs["cutoff"] = K + K = min(K, local_node_connectivity(G, x, y, **kwargs)) + + return K + + +@nx._dispatchable +def average_node_connectivity(G, flow_func=None): + r"""Returns the average connectivity of a graph G. + + The average connectivity `\bar{\kappa}` of a graph G is the average + of local node connectivity over all pairs of nodes of G [1]_ . + + .. math:: + + \bar{\kappa}(G) = \frac{\sum_{u,v} \kappa_{G}(u,v)}{{n \choose 2}} + + Parameters + ---------- + + G : NetworkX graph + Undirected graph + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See :meth:`local_node_connectivity` + for details. The choice of the default function may change from + version to version and should not be relied on. Default value: None. + + Returns + ------- + K : float + Average node connectivity + + See also + -------- + :meth:`local_node_connectivity` + :meth:`node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Beineke, L., O. Oellermann, and R. Pippert (2002). The average + connectivity of a graph. Discrete mathematics 252(1-3), 31-45. + http://www.sciencedirect.com/science/article/pii/S0012365X01001807 + + """ + if G.is_directed(): + iter_func = itertools.permutations + else: + iter_func = itertools.combinations + + # Reuse the auxiliary digraph and the residual network + H = build_auxiliary_node_connectivity(G) + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} + + num, den = 0, 0 + for u, v in iter_func(G, 2): + num += local_node_connectivity(G, u, v, **kwargs) + den += 1 + + if den == 0: # Null Graph + return 0 + return num / den + + +@nx._dispatchable +def all_pairs_node_connectivity(G, nbunch=None, flow_func=None): + """Compute node connectivity between all pairs of nodes of G. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + nbunch: container + Container of nodes. If provided node connectivity will be computed + only over pairs of nodes in nbunch. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + Returns + ------- + all_pairs : dict + A dictionary with node connectivity between all pairs of nodes + in G, or in nbunch if provided. + + See also + -------- + :meth:`local_node_connectivity` + :meth:`edge_connectivity` + :meth:`local_edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + """ + if nbunch is None: + nbunch = G + else: + nbunch = set(nbunch) + + directed = G.is_directed() + if directed: + iter_func = itertools.permutations + else: + iter_func = itertools.combinations + + all_pairs = {n: {} for n in nbunch} + + # Reuse auxiliary digraph and residual network + H = build_auxiliary_node_connectivity(G) + mapping = H.graph["mapping"] + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} + + for u, v in iter_func(nbunch, 2): + K = local_node_connectivity(G, u, v, **kwargs) + all_pairs[u][v] = K + if not directed: + all_pairs[v][u] = K + + return all_pairs + + +@nx._dispatchable(graphs={"G": 0, "auxiliary?": 4}) +def local_edge_connectivity( + G, s, t, flow_func=None, auxiliary=None, residual=None, cutoff=None +): + r"""Returns local edge connectivity for nodes s and t in G. + + Local edge connectivity for two nodes s and t is the minimum number + of edges that must be removed to disconnect them. + + This is a flow based implementation of edge connectivity. We compute the + maximum flow on an auxiliary digraph build from the original + network (see below for details). This is equal to the local edge + connectivity because the value of a maximum s-t-flow is equal to the + capacity of a minimum s-t-cut (Ford and Fulkerson theorem) [1]_ . + + Parameters + ---------- + G : NetworkX graph + Undirected or directed graph + + s : node + Source node + + t : node + Target node + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + auxiliary : NetworkX DiGraph + Auxiliary digraph for computing flow based edge connectivity. If + provided it will be reused instead of recreated. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + cutoff : integer, float, or None (default: None) + If specified, the maximum flow algorithm will terminate when the + flow value reaches or exceeds the cutoff. This only works for flows + that support the cutoff parameter (most do) and is ignored otherwise. + + Returns + ------- + K : integer + local edge connectivity for nodes s and t. + + Examples + -------- + This function is not imported in the base NetworkX namespace, so you + have to explicitly import it from the connectivity package: + + >>> from networkx.algorithms.connectivity import local_edge_connectivity + + We use in this example the platonic icosahedral graph, which has edge + connectivity 5. + + >>> G = nx.icosahedral_graph() + >>> local_edge_connectivity(G, 0, 6) + 5 + + If you need to compute local connectivity on several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for edge connectivity, and the residual + network for the underlying maximum flow computation. + + Example of how to compute local edge connectivity among + all pairs of nodes of the platonic icosahedral graph reusing + the data structures. + + >>> import itertools + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity + >>> H = build_auxiliary_edge_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> result = dict.fromkeys(G, dict()) + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as parameters + >>> for u, v in itertools.combinations(G, 2): + ... k = local_edge_connectivity(G, u, v, auxiliary=H, residual=R) + ... result[u][v] = k + >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2)) + True + + You can also use alternative flow algorithms for computing edge + connectivity. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> local_edge_connectivity(G, 0, 6, flow_func=shortest_augmenting_path) + 5 + + Notes + ----- + This is a flow based implementation of edge connectivity. We compute the + maximum flow using, by default, the :meth:`edmonds_karp` algorithm on an + auxiliary digraph build from the original input graph: + + If the input graph is undirected, we replace each edge (`u`,`v`) with + two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute + 'capacity' for each arc to 1. If the input graph is directed we simply + add the 'capacity' attribute. This is an implementation of algorithm 1 + in [1]_. + + The maximum flow in the auxiliary network is equal to the local edge + connectivity because the value of a maximum s-t-flow is equal to the + capacity of a minimum s-t-cut (Ford and Fulkerson theorem). + + See also + -------- + :meth:`edge_connectivity` + :meth:`local_node_connectivity` + :meth:`node_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if flow_func is None: + flow_func = default_flow_func + + if auxiliary is None: + H = build_auxiliary_edge_connectivity(G) + else: + H = auxiliary + + kwargs = {"flow_func": flow_func, "residual": residual} + + if flow_func is not preflow_push: + kwargs["cutoff"] = cutoff + + if flow_func is shortest_augmenting_path: + kwargs["two_phase"] = True + + return nx.maximum_flow_value(H, s, t, **kwargs) + + +@nx._dispatchable +def edge_connectivity(G, s=None, t=None, flow_func=None, cutoff=None): + r"""Returns the edge connectivity of the graph or digraph G. + + The edge connectivity is equal to the minimum number of edges that + must be removed to disconnect G or render it trivial. If source + and target nodes are provided, this function returns the local edge + connectivity: the minimum number of edges that must be removed to + break all paths from source to target in G. + + Parameters + ---------- + G : NetworkX graph + Undirected or directed graph + + s : node + Source node. Optional. Default value: None. + + t : node + Target node. Optional. Default value: None. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + cutoff : integer, float, or None (default: None) + If specified, the maximum flow algorithm will terminate when the + flow value reaches or exceeds the cutoff. This only works for flows + that support the cutoff parameter (most do) and is ignored otherwise. + + Returns + ------- + K : integer + Edge connectivity for G, or local edge connectivity if source + and target were provided + + Examples + -------- + >>> # Platonic icosahedral graph is 5-edge-connected + >>> G = nx.icosahedral_graph() + >>> nx.edge_connectivity(G) + 5 + + You can use alternative flow algorithms for the underlying + maximum flow computation. In dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better + than the default :meth:`edmonds_karp`, which is faster for + sparse networks with highly skewed degree distributions. + Alternative flow functions have to be explicitly imported + from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> nx.edge_connectivity(G, flow_func=shortest_augmenting_path) + 5 + + If you specify a pair of nodes (source and target) as parameters, + this function returns the value of local edge connectivity. + + >>> nx.edge_connectivity(G, 3, 7) + 5 + + If you need to perform several local computations among different + pairs of nodes on the same graph, it is recommended that you reuse + the data structures used in the maximum flow computations. See + :meth:`local_edge_connectivity` for details. + + Notes + ----- + This is a flow based implementation of global edge connectivity. + For undirected graphs the algorithm works by finding a 'small' + dominating set of nodes of G (see algorithm 7 in [1]_ ) and + computing local maximum flow (see :meth:`local_edge_connectivity`) + between an arbitrary node in the dominating set and the rest of + nodes in it. This is an implementation of algorithm 6 in [1]_ . + For directed graphs, the algorithm does n calls to the maximum + flow function. This is an implementation of algorithm 8 in [1]_ . + + See also + -------- + :meth:`local_edge_connectivity` + :meth:`local_node_connectivity` + :meth:`node_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + :meth:`k_edge_components` + :meth:`k_edge_subgraphs` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if (s is not None and t is None) or (s is None and t is not None): + raise nx.NetworkXError("Both source and target must be specified.") + + # Local edge connectivity + if s is not None and t is not None: + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + return local_edge_connectivity(G, s, t, flow_func=flow_func, cutoff=cutoff) + + # Global edge connectivity + # reuse auxiliary digraph and residual network + H = build_auxiliary_edge_connectivity(G) + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} + + if G.is_directed(): + # Algorithm 8 in [1] + if not nx.is_weakly_connected(G): + return 0 + + # initial value for \lambda is minimum degree + L = min(d for n, d in G.degree()) + nodes = list(G) + n = len(nodes) + + if cutoff is not None: + L = min(cutoff, L) + + for i in range(n): + kwargs["cutoff"] = L + try: + L = min(L, local_edge_connectivity(G, nodes[i], nodes[i + 1], **kwargs)) + except IndexError: # last node! + L = min(L, local_edge_connectivity(G, nodes[i], nodes[0], **kwargs)) + return L + else: # undirected + # Algorithm 6 in [1] + if not nx.is_connected(G): + return 0 + + # initial value for \lambda is minimum degree + L = min(d for n, d in G.degree()) + + if cutoff is not None: + L = min(cutoff, L) + + # A dominating set is \lambda-covering + # We need a dominating set with at least two nodes + for node in G: + D = nx.dominating_set(G, start_with=node) + v = D.pop() + if D: + break + else: + # in complete graphs the dominating sets will always be of one node + # thus we return min degree + return L + + for w in D: + kwargs["cutoff"] = L + L = min(L, local_edge_connectivity(G, v, w, **kwargs)) + + return L diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py new file mode 100644 index 0000000000000000000000000000000000000000..e7806e1e89fa2ad4368b44985dfac42a418dc326 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/cuts.py @@ -0,0 +1,616 @@ +""" +Flow based cut algorithms +""" + +import itertools + +import networkx as nx + +# Define the default maximum flow function to use in all flow based +# cut algorithms. +from networkx.algorithms.flow import build_residual_network, edmonds_karp + +from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity + +default_flow_func = edmonds_karp + +__all__ = [ + "minimum_st_node_cut", + "minimum_node_cut", + "minimum_st_edge_cut", + "minimum_edge_cut", +] + + +@nx._dispatchable( + graphs={"G": 0, "auxiliary?": 4}, + preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}}, + preserve_graph_attrs={"auxiliary"}, +) +def minimum_st_edge_cut(G, s, t, flow_func=None, auxiliary=None, residual=None): + """Returns the edges of the cut-set of a minimum (s, t)-cut. + + This function returns the set of edges of minimum cardinality that, + if removed, would destroy all paths among source and target in G. + Edge weights are not considered. See :meth:`minimum_cut` for + computing minimum cuts considering edge weights. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + auxiliary : NetworkX DiGraph + Auxiliary digraph to compute flow based node connectivity. It has + to have a graph attribute called mapping with a dictionary mapping + node names in G and in the auxiliary digraph. If provided + it will be reused instead of recreated. Default value: None. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See :meth:`node_connectivity` for + details. The choice of the default function may change from version + to version and should not be relied on. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + Returns + ------- + cutset : set + Set of edges that, if removed from the graph, will disconnect it. + + See also + -------- + :meth:`minimum_cut` + :meth:`minimum_node_cut` + :meth:`minimum_edge_cut` + :meth:`stoer_wagner` + :meth:`node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Examples + -------- + This function is not imported in the base NetworkX namespace, so you + have to explicitly import it from the connectivity package: + + >>> from networkx.algorithms.connectivity import minimum_st_edge_cut + + We use in this example the platonic icosahedral graph, which has edge + connectivity 5. + + >>> G = nx.icosahedral_graph() + >>> len(minimum_st_edge_cut(G, 0, 6)) + 5 + + If you need to compute local edge cuts on several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for edge connectivity, and the residual + network for the underlying maximum flow computation. + + Example of how to compute local edge cuts among all pairs of + nodes of the platonic icosahedral graph reusing the data + structures. + + >>> import itertools + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity + >>> H = build_auxiliary_edge_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> result = dict.fromkeys(G, dict()) + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as parameters + >>> for u, v in itertools.combinations(G, 2): + ... k = len(minimum_st_edge_cut(G, u, v, auxiliary=H, residual=R)) + ... result[u][v] = k + >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2)) + True + + You can also use alternative flow algorithms for computing edge + cuts. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> len(minimum_st_edge_cut(G, 0, 6, flow_func=shortest_augmenting_path)) + 5 + + """ + if flow_func is None: + flow_func = default_flow_func + + if auxiliary is None: + H = build_auxiliary_edge_connectivity(G) + else: + H = auxiliary + + kwargs = {"capacity": "capacity", "flow_func": flow_func, "residual": residual} + + cut_value, partition = nx.minimum_cut(H, s, t, **kwargs) + reachable, non_reachable = partition + # Any edge in the original graph linking the two sets in the + # partition is part of the edge cutset + cutset = set() + for u, nbrs in ((n, G[n]) for n in reachable): + cutset.update((u, v) for v in nbrs if v in non_reachable) + + return cutset + + +@nx._dispatchable( + graphs={"G": 0, "auxiliary?": 4}, + preserve_node_attrs={"auxiliary": {"id": None}}, + preserve_graph_attrs={"auxiliary"}, +) +def minimum_st_node_cut(G, s, t, flow_func=None, auxiliary=None, residual=None): + r"""Returns a set of nodes of minimum cardinality that disconnect source + from target in G. + + This function returns the set of nodes of minimum cardinality that, + if removed, would destroy all paths among source and target in G. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node. + + t : node + Target node. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The choice + of the default function may change from version to version and + should not be relied on. Default value: None. + + auxiliary : NetworkX DiGraph + Auxiliary digraph to compute flow based node connectivity. It has + to have a graph attribute called mapping with a dictionary mapping + node names in G and in the auxiliary digraph. If provided + it will be reused instead of recreated. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + Returns + ------- + cutset : set + Set of nodes that, if removed, would destroy all paths between + source and target in G. + + Returns an empty set if source and target are either in different + components or are directly connected by an edge, as no node removal + can destroy the path. + + Examples + -------- + This function is not imported in the base NetworkX namespace, so you + have to explicitly import it from the connectivity package: + + >>> from networkx.algorithms.connectivity import minimum_st_node_cut + + We use in this example the platonic icosahedral graph, which has node + connectivity 5. + + >>> G = nx.icosahedral_graph() + >>> len(minimum_st_node_cut(G, 0, 6)) + 5 + + If you need to compute local st cuts between several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for node connectivity and node cuts, and the + residual network for the underlying maximum flow computation. + + Example of how to compute local st node cuts reusing the data + structures: + + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity + >>> H = build_auxiliary_node_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as parameters + >>> len(minimum_st_node_cut(G, 0, 6, auxiliary=H, residual=R)) + 5 + + You can also use alternative flow algorithms for computing minimum st + node cuts. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> len(minimum_st_node_cut(G, 0, 6, flow_func=shortest_augmenting_path)) + 5 + + Notes + ----- + This is a flow based implementation of minimum node cut. The algorithm + is based in solving a number of maximum flow computations to determine + the capacity of the minimum cut on an auxiliary directed network that + corresponds to the minimum node cut of G. It handles both directed + and undirected graphs. This implementation is based on algorithm 11 + in [1]_. + + See also + -------- + :meth:`minimum_node_cut` + :meth:`minimum_edge_cut` + :meth:`stoer_wagner` + :meth:`node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if auxiliary is None: + H = build_auxiliary_node_connectivity(G) + else: + H = auxiliary + + mapping = H.graph.get("mapping", None) + if mapping is None: + raise nx.NetworkXError("Invalid auxiliary digraph.") + if G.has_edge(s, t) or G.has_edge(t, s): + return set() + kwargs = {"flow_func": flow_func, "residual": residual, "auxiliary": H} + + # The edge cut in the auxiliary digraph corresponds to the node cut in the + # original graph. + edge_cut = minimum_st_edge_cut(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs) + # Each node in the original graph maps to two nodes of the auxiliary graph + node_cut = {H.nodes[node]["id"] for edge in edge_cut for node in edge} + return node_cut - {s, t} + + +@nx._dispatchable +def minimum_node_cut(G, s=None, t=None, flow_func=None): + r"""Returns a set of nodes of minimum cardinality that disconnects G. + + If source and target nodes are provided, this function returns the + set of nodes of minimum cardinality that, if removed, would destroy + all paths among source and target in G. If not, it returns a set + of nodes of minimum cardinality that disconnects G. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node. Optional. Default value: None. + + t : node + Target node. Optional. Default value: None. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + Returns + ------- + cutset : set + Set of nodes that, if removed, would disconnect G. If source + and target nodes are provided, the set contains the nodes that + if removed, would destroy all paths between source and target. + + Examples + -------- + >>> # Platonic icosahedral graph has node connectivity 5 + >>> G = nx.icosahedral_graph() + >>> node_cut = nx.minimum_node_cut(G) + >>> len(node_cut) + 5 + + You can use alternative flow algorithms for the underlying maximum + flow computation. In dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better + than the default :meth:`edmonds_karp`, which is faster for + sparse networks with highly skewed degree distributions. Alternative + flow functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> node_cut == nx.minimum_node_cut(G, flow_func=shortest_augmenting_path) + True + + If you specify a pair of nodes (source and target) as parameters, + this function returns a local st node cut. + + >>> len(nx.minimum_node_cut(G, 3, 7)) + 5 + + If you need to perform several local st cuts among different + pairs of nodes on the same graph, it is recommended that you reuse + the data structures used in the maximum flow computations. See + :meth:`minimum_st_node_cut` for details. + + Notes + ----- + This is a flow based implementation of minimum node cut. The algorithm + is based in solving a number of maximum flow computations to determine + the capacity of the minimum cut on an auxiliary directed network that + corresponds to the minimum node cut of G. It handles both directed + and undirected graphs. This implementation is based on algorithm 11 + in [1]_. + + See also + -------- + :meth:`minimum_st_node_cut` + :meth:`minimum_cut` + :meth:`minimum_edge_cut` + :meth:`stoer_wagner` + :meth:`node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if (s is not None and t is None) or (s is None and t is not None): + raise nx.NetworkXError("Both source and target must be specified.") + + # Local minimum node cut. + if s is not None and t is not None: + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + return minimum_st_node_cut(G, s, t, flow_func=flow_func) + + # Global minimum node cut. + # Analog to the algorithm 11 for global node connectivity in [1]. + if G.is_directed(): + if not nx.is_weakly_connected(G): + raise nx.NetworkXError("Input graph is not connected") + iter_func = itertools.permutations + + def neighbors(v): + return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)]) + + else: + if not nx.is_connected(G): + raise nx.NetworkXError("Input graph is not connected") + iter_func = itertools.combinations + neighbors = G.neighbors + + # Reuse the auxiliary digraph and the residual network. + H = build_auxiliary_node_connectivity(G) + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "auxiliary": H, "residual": R} + + # Choose a node with minimum degree. + v = min(G, key=G.degree) + # Initial node cutset is all neighbors of the node with minimum degree. + min_cut = set(G[v]) + # Compute st node cuts between v and all its non-neighbors nodes in G. + for w in set(G) - set(neighbors(v)) - {v}: + this_cut = minimum_st_node_cut(G, v, w, **kwargs) + if len(min_cut) >= len(this_cut): + min_cut = this_cut + # Also for non adjacent pairs of neighbors of v. + for x, y in iter_func(neighbors(v), 2): + if y in G[x]: + continue + this_cut = minimum_st_node_cut(G, x, y, **kwargs) + if len(min_cut) >= len(this_cut): + min_cut = this_cut + + return min_cut + + +@nx._dispatchable +def minimum_edge_cut(G, s=None, t=None, flow_func=None): + r"""Returns a set of edges of minimum cardinality that disconnects G. + + If source and target nodes are provided, this function returns the + set of edges of minimum cardinality that, if removed, would break + all paths among source and target in G. If not, it returns a set of + edges of minimum cardinality that disconnects G. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node. Optional. Default value: None. + + t : node + Target node. Optional. Default value: None. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The + choice of the default function may change from version + to version and should not be relied on. Default value: None. + + Returns + ------- + cutset : set + Set of edges that, if removed, would disconnect G. If source + and target nodes are provided, the set contains the edges that + if removed, would destroy all paths between source and target. + + Examples + -------- + >>> # Platonic icosahedral graph has edge connectivity 5 + >>> G = nx.icosahedral_graph() + >>> len(nx.minimum_edge_cut(G)) + 5 + + You can use alternative flow algorithms for the underlying + maximum flow computation. In dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better + than the default :meth:`edmonds_karp`, which is faster for + sparse networks with highly skewed degree distributions. + Alternative flow functions have to be explicitly imported + from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> len(nx.minimum_edge_cut(G, flow_func=shortest_augmenting_path)) + 5 + + If you specify a pair of nodes (source and target) as parameters, + this function returns the value of local edge connectivity. + + >>> nx.edge_connectivity(G, 3, 7) + 5 + + If you need to perform several local computations among different + pairs of nodes on the same graph, it is recommended that you reuse + the data structures used in the maximum flow computations. See + :meth:`local_edge_connectivity` for details. + + Notes + ----- + This is a flow based implementation of minimum edge cut. For + undirected graphs the algorithm works by finding a 'small' dominating + set of nodes of G (see algorithm 7 in [1]_) and computing the maximum + flow between an arbitrary node in the dominating set and the rest of + nodes in it. This is an implementation of algorithm 6 in [1]_. For + directed graphs, the algorithm does n calls to the max flow function. + The function raises an error if the directed graph is not weakly + connected and returns an empty set if it is weakly connected. + It is an implementation of algorithm 8 in [1]_. + + See also + -------- + :meth:`minimum_st_edge_cut` + :meth:`minimum_node_cut` + :meth:`stoer_wagner` + :meth:`node_connectivity` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + if (s is not None and t is None) or (s is None and t is not None): + raise nx.NetworkXError("Both source and target must be specified.") + + # reuse auxiliary digraph and residual network + H = build_auxiliary_edge_connectivity(G) + R = build_residual_network(H, "capacity") + kwargs = {"flow_func": flow_func, "residual": R, "auxiliary": H} + + # Local minimum edge cut if s and t are not None + if s is not None and t is not None: + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + return minimum_st_edge_cut(H, s, t, **kwargs) + + # Global minimum edge cut + # Analog to the algorithm for global edge connectivity + if G.is_directed(): + # Based on algorithm 8 in [1] + if not nx.is_weakly_connected(G): + raise nx.NetworkXError("Input graph is not connected") + + # Initial cutset is all edges of a node with minimum degree + node = min(G, key=G.degree) + min_cut = set(G.edges(node)) + nodes = list(G) + n = len(nodes) + for i in range(n): + try: + this_cut = minimum_st_edge_cut(H, nodes[i], nodes[i + 1], **kwargs) + if len(this_cut) <= len(min_cut): + min_cut = this_cut + except IndexError: # Last node! + this_cut = minimum_st_edge_cut(H, nodes[i], nodes[0], **kwargs) + if len(this_cut) <= len(min_cut): + min_cut = this_cut + + return min_cut + + else: # undirected + # Based on algorithm 6 in [1] + if not nx.is_connected(G): + raise nx.NetworkXError("Input graph is not connected") + + # Initial cutset is all edges of a node with minimum degree + node = min(G, key=G.degree) + min_cut = set(G.edges(node)) + # A dominating set is \lambda-covering + # We need a dominating set with at least two nodes + for node in G: + D = nx.dominating_set(G, start_with=node) + v = D.pop() + if D: + break + else: + # in complete graphs the dominating set will always be of one node + # thus we return min_cut, which now contains the edges of a node + # with minimum degree + return min_cut + for w in D: + this_cut = minimum_st_edge_cut(H, v, w, **kwargs) + if len(this_cut) <= len(min_cut): + min_cut = this_cut + + return min_cut diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py new file mode 100644 index 0000000000000000000000000000000000000000..fdd85225fb0818d9ad3ad60f27b0ee8bb6ebb6c2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/disjoint_paths.py @@ -0,0 +1,408 @@ +"""Flow based node and edge disjoint paths.""" + +from itertools import filterfalse as _filterfalse + +import networkx as nx + +# Define the default maximum flow function to use for the underlying +# maximum flow computations +from networkx.algorithms.flow import ( + edmonds_karp, + preflow_push, + shortest_augmenting_path, +) +from networkx.exception import NetworkXNoPath + +# Functions to build auxiliary data structures. +from .utils import build_auxiliary_edge_connectivity, build_auxiliary_node_connectivity + +__all__ = ["edge_disjoint_paths", "node_disjoint_paths"] +default_flow_func = edmonds_karp + + +@nx._dispatchable( + graphs={"G": 0, "auxiliary?": 5}, + preserve_edge_attrs={"auxiliary": {"capacity": float("inf")}}, +) +def edge_disjoint_paths( + G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None +): + """Returns the edges disjoint paths between source and target. + + Edge disjoint paths are paths that do not share any edge. The + number of edge disjoint paths between source and target is equal + to their edge connectivity. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. The choice of the default function + may change from version to version and should not be relied on. + Default value: None. + + cutoff : integer or None (default: None) + Maximum number of paths to yield. If specified, the maximum flow + algorithm will terminate when the flow value reaches or exceeds the + cutoff. This only works for flows that support the cutoff parameter + (most do) and is ignored otherwise. + + auxiliary : NetworkX DiGraph + Auxiliary digraph to compute flow based edge connectivity. It has + to have a graph attribute called mapping with a dictionary mapping + node names in G and in the auxiliary digraph. If provided + it will be reused instead of recreated. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + Returns + ------- + paths : generator + A generator of edge independent paths. + + Raises + ------ + NetworkXNoPath + If there is no path between source and target. + + NetworkXError + If source or target are not in the graph G. + + See also + -------- + :meth:`node_disjoint_paths` + :meth:`edge_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Examples + -------- + We use in this example the platonic icosahedral graph, which has node + edge connectivity 5, thus there are 5 edge disjoint paths between any + pair of nodes. + + >>> G = nx.icosahedral_graph() + >>> len(list(nx.edge_disjoint_paths(G, 0, 6))) + 5 + + + If you need to compute edge disjoint paths on several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for edge connectivity, and the residual + network for the underlying maximum flow computation. + + Example of how to compute edge disjoint paths among all pairs of + nodes of the platonic icosahedral graph reusing the data + structures. + + >>> import itertools + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_edge_connectivity + >>> H = build_auxiliary_edge_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> result = {n: {} for n in G} + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as arguments + >>> for u, v in itertools.combinations(G, 2): + ... k = len(list(nx.edge_disjoint_paths(G, u, v, auxiliary=H, residual=R))) + ... result[u][v] = k + >>> all(result[u][v] == 5 for u, v in itertools.combinations(G, 2)) + True + + You can also use alternative flow algorithms for computing edge disjoint + paths. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> len(list(nx.edge_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path))) + 5 + + Notes + ----- + This is a flow based implementation of edge disjoint paths. We compute + the maximum flow between source and target on an auxiliary directed + network. The saturated edges in the residual network after running the + maximum flow algorithm correspond to edge disjoint paths between source + and target in the original network. This function handles both directed + and undirected graphs, and can use all flow algorithms from NetworkX flow + package. + + """ + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + + if flow_func is None: + flow_func = default_flow_func + + if auxiliary is None: + H = build_auxiliary_edge_connectivity(G) + else: + H = auxiliary + + # Maximum possible edge disjoint paths + possible = min(H.out_degree(s), H.in_degree(t)) + if not possible: + raise NetworkXNoPath + + if cutoff is None: + cutoff = possible + else: + cutoff = min(cutoff, possible) + + # Compute maximum flow between source and target. Flow functions in + # NetworkX return a residual network. + kwargs = { + "capacity": "capacity", + "residual": residual, + "cutoff": cutoff, + "value_only": True, + } + if flow_func is preflow_push: + del kwargs["cutoff"] + if flow_func is shortest_augmenting_path: + kwargs["two_phase"] = True + R = flow_func(H, s, t, **kwargs) + + if R.graph["flow_value"] == 0: + raise NetworkXNoPath + + # Saturated edges in the residual network form the edge disjoint paths + # between source and target + cutset = [ + (u, v) + for u, v, d in R.edges(data=True) + if d["capacity"] == d["flow"] and d["flow"] > 0 + ] + # This is equivalent of what flow.utils.build_flow_dict returns, but + # only for the nodes with saturated edges and without reporting 0 flows. + flow_dict = {n: {} for edge in cutset for n in edge} + for u, v in cutset: + flow_dict[u][v] = 1 + + # Rebuild the edge disjoint paths from the flow dictionary. + paths_found = 0 + for v in list(flow_dict[s]): + if paths_found >= cutoff: + # preflow_push does not support cutoff: we have to + # keep track of the paths founds and stop at cutoff. + break + path = [s] + if v == t: + path.append(v) + yield path + continue + u = v + while u != t: + path.append(u) + try: + u, _ = flow_dict[u].popitem() + except KeyError: + break + else: + path.append(t) + yield path + paths_found += 1 + + +@nx._dispatchable( + graphs={"G": 0, "auxiliary?": 5}, + preserve_node_attrs={"auxiliary": {"id": None}}, + preserve_graph_attrs={"auxiliary"}, +) +def node_disjoint_paths( + G, s, t, flow_func=None, cutoff=None, auxiliary=None, residual=None +): + r"""Computes node disjoint paths between source and target. + + Node disjoint paths are paths that only share their first and last + nodes. The number of node independent paths between two nodes is + equal to their local node connectivity. + + Parameters + ---------- + G : NetworkX graph + + s : node + Source node. + + t : node + Target node. + + flow_func : function + A function for computing the maximum flow among a pair of nodes. + The function has to accept at least three parameters: a Digraph, + a source node, and a target node. And return a residual network + that follows NetworkX conventions (see :meth:`maximum_flow` for + details). If flow_func is None, the default maximum flow function + (:meth:`edmonds_karp`) is used. See below for details. The choice + of the default function may change from version to version and + should not be relied on. Default value: None. + + cutoff : integer or None (default: None) + Maximum number of paths to yield. If specified, the maximum flow + algorithm will terminate when the flow value reaches or exceeds the + cutoff. This only works for flows that support the cutoff parameter + (most do) and is ignored otherwise. + + auxiliary : NetworkX DiGraph + Auxiliary digraph to compute flow based node connectivity. It has + to have a graph attribute called mapping with a dictionary mapping + node names in G and in the auxiliary digraph. If provided + it will be reused instead of recreated. Default value: None. + + residual : NetworkX DiGraph + Residual network to compute maximum flow. If provided it will be + reused instead of recreated. Default value: None. + + Returns + ------- + paths : generator + Generator of node disjoint paths. + + Raises + ------ + NetworkXNoPath + If there is no path between source and target. + + NetworkXError + If source or target are not in the graph G. + + Examples + -------- + We use in this example the platonic icosahedral graph, which has node + connectivity 5, thus there are 5 node disjoint paths between any pair + of non neighbor nodes. + + >>> G = nx.icosahedral_graph() + >>> len(list(nx.node_disjoint_paths(G, 0, 6))) + 5 + + If you need to compute node disjoint paths between several pairs of + nodes in the same graph, it is recommended that you reuse the + data structures that NetworkX uses in the computation: the + auxiliary digraph for node connectivity and node cuts, and the + residual network for the underlying maximum flow computation. + + Example of how to compute node disjoint paths reusing the data + structures: + + >>> # You also have to explicitly import the function for + >>> # building the auxiliary digraph from the connectivity package + >>> from networkx.algorithms.connectivity import build_auxiliary_node_connectivity + >>> H = build_auxiliary_node_connectivity(G) + >>> # And the function for building the residual network from the + >>> # flow package + >>> from networkx.algorithms.flow import build_residual_network + >>> # Note that the auxiliary digraph has an edge attribute named capacity + >>> R = build_residual_network(H, "capacity") + >>> # Reuse the auxiliary digraph and the residual network by passing them + >>> # as arguments + >>> len(list(nx.node_disjoint_paths(G, 0, 6, auxiliary=H, residual=R))) + 5 + + You can also use alternative flow algorithms for computing node disjoint + paths. For instance, in dense networks the algorithm + :meth:`shortest_augmenting_path` will usually perform better than + the default :meth:`edmonds_karp` which is faster for sparse + networks with highly skewed degree distributions. Alternative flow + functions have to be explicitly imported from the flow package. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> len(list(nx.node_disjoint_paths(G, 0, 6, flow_func=shortest_augmenting_path))) + 5 + + Notes + ----- + This is a flow based implementation of node disjoint paths. We compute + the maximum flow between source and target on an auxiliary directed + network. The saturated edges in the residual network after running the + maximum flow algorithm correspond to node disjoint paths between source + and target in the original network. This function handles both directed + and undirected graphs, and can use all flow algorithms from NetworkX flow + package. + + See also + -------- + :meth:`edge_disjoint_paths` + :meth:`node_connectivity` + :meth:`maximum_flow` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + """ + if s not in G: + raise nx.NetworkXError(f"node {s} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {t} not in graph") + + if auxiliary is None: + H = build_auxiliary_node_connectivity(G) + else: + H = auxiliary + + mapping = H.graph.get("mapping", None) + if mapping is None: + raise nx.NetworkXError("Invalid auxiliary digraph.") + + # Maximum possible edge disjoint paths + possible = min(H.out_degree(f"{mapping[s]}B"), H.in_degree(f"{mapping[t]}A")) + if not possible: + raise NetworkXNoPath + + if cutoff is None: + cutoff = possible + else: + cutoff = min(cutoff, possible) + + kwargs = { + "flow_func": flow_func, + "residual": residual, + "auxiliary": H, + "cutoff": cutoff, + } + + # The edge disjoint paths in the auxiliary digraph correspond to the node + # disjoint paths in the original graph. + paths_edges = edge_disjoint_paths(H, f"{mapping[s]}B", f"{mapping[t]}A", **kwargs) + for path in paths_edges: + # Each node in the original graph maps to two nodes in auxiliary graph + yield list(_unique_everseen(H.nodes[node]["id"] for node in path)) + + +def _unique_everseen(iterable): + # Adapted from https://docs.python.org/3/library/itertools.html examples + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + seen = set() + seen_add = seen.add + for element in _filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..6dfe0140268608c183e6d0122fe927dcf164a508 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_augmentation.py @@ -0,0 +1,1270 @@ +""" +Algorithms for finding k-edge-augmentations + +A k-edge-augmentation is a set of edges, that once added to a graph, ensures +that the graph is k-edge-connected; i.e. the graph cannot be disconnected +unless k or more edges are removed. Typically, the goal is to find the +augmentation with minimum weight. In general, it is not guaranteed that a +k-edge-augmentation exists. + +See Also +-------- +:mod:`edge_kcomponents` : algorithms for finding k-edge-connected components +:mod:`connectivity` : algorithms for determining edge connectivity. +""" + +import itertools as it +import math +from collections import defaultdict, namedtuple + +import networkx as nx +from networkx.utils import not_implemented_for, py_random_state + +__all__ = ["k_edge_augmentation", "is_k_edge_connected", "is_locally_k_edge_connected"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_k_edge_connected(G, k): + """Tests to see if a graph is k-edge-connected. + + Is it impossible to disconnect the graph by removing fewer than k edges? + If so, then G is k-edge-connected. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + k : integer + edge connectivity to test for + + Returns + ------- + boolean + True if G is k-edge-connected. + + See Also + -------- + :func:`is_locally_k_edge_connected` + + Examples + -------- + >>> G = nx.barbell_graph(10, 0) + >>> nx.is_k_edge_connected(G, k=1) + True + >>> nx.is_k_edge_connected(G, k=2) + False + """ + if k < 1: + raise ValueError(f"k must be positive, not {k}") + # First try to quickly determine if G is not k-edge-connected + if G.number_of_nodes() < k + 1: + return False + elif any(d < k for n, d in G.degree()): + return False + else: + # Otherwise perform the full check + if k == 1: + return nx.is_connected(G) + elif k == 2: + return nx.is_connected(G) and not nx.has_bridges(G) + else: + return nx.edge_connectivity(G, cutoff=k) >= k + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_locally_k_edge_connected(G, s, t, k): + """Tests to see if an edge in a graph is locally k-edge-connected. + + Is it impossible to disconnect s and t by removing fewer than k edges? + If so, then s and t are locally k-edge-connected in G. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + s : node + Source node + + t : node + Target node + + k : integer + local edge connectivity for nodes s and t + + Returns + ------- + boolean + True if s and t are locally k-edge-connected in G. + + See Also + -------- + :func:`is_k_edge_connected` + + Examples + -------- + >>> from networkx.algorithms.connectivity import is_locally_k_edge_connected + >>> G = nx.barbell_graph(10, 0) + >>> is_locally_k_edge_connected(G, 5, 15, k=1) + True + >>> is_locally_k_edge_connected(G, 5, 15, k=2) + False + >>> is_locally_k_edge_connected(G, 1, 5, k=2) + True + """ + if k < 1: + raise ValueError(f"k must be positive, not {k}") + + # First try to quickly determine s, t is not k-locally-edge-connected in G + if G.degree(s) < k or G.degree(t) < k: + return False + else: + # Otherwise perform the full check + if k == 1: + return nx.has_path(G, s, t) + else: + localk = nx.connectivity.local_edge_connectivity(G, s, t, cutoff=k) + return localk >= k + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def k_edge_augmentation(G, k, avail=None, weight=None, partial=False): + """Finds set of edges to k-edge-connect G. + + Adding edges from the augmentation to G make it impossible to disconnect G + unless k or more edges are removed. This function uses the most efficient + function available (depending on the value of k and if the problem is + weighted or unweighted) to search for a minimum weight subset of available + edges that k-edge-connects G. In general, finding a k-edge-augmentation is + NP-hard, so solutions are not guaranteed to be minimal. Furthermore, a + k-edge-augmentation may not exist. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + k : integer + Desired edge connectivity + + avail : dict or a set of 2 or 3 tuples + The available edges that can be used in the augmentation. + + If unspecified, then all edges in the complement of G are available. + Otherwise, each item is an available edge (with an optional weight). + + In the unweighted case, each item is an edge ``(u, v)``. + + In the weighted case, each item is a 3-tuple ``(u, v, d)`` or a dict + with items ``(u, v): d``. The third item, ``d``, can be a dictionary + or a real number. If ``d`` is a dictionary ``d[weight]`` + correspondings to the weight. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples where the + third item in each tuple is a dictionary. + + partial : boolean + If partial is True and no feasible k-edge-augmentation exists, then all + a partial k-edge-augmentation is generated. Adding the edges in a + partial augmentation to G, minimizes the number of k-edge-connected + components and maximizes the edge connectivity between those + components. For details, see :func:`partial_k_edge_augmentation`. + + Yields + ------ + edge : tuple + Edges that, once added to G, would cause G to become k-edge-connected. + If partial is False, an error is raised if this is not possible. + Otherwise, generated edges form a partial augmentation, which + k-edge-connects any part of G where it is possible, and maximally + connects the remaining parts. + + Raises + ------ + NetworkXUnfeasible + If partial is False and no k-edge-augmentation exists. + + NetworkXNotImplemented + If the input graph is directed or a multigraph. + + ValueError: + If k is less than 1 + + Notes + ----- + When k=1 this returns an optimal solution. + + When k=2 and ``avail`` is None, this returns an optimal solution. + Otherwise when k=2, this returns a 2-approximation of the optimal solution. + + For k>3, this problem is NP-hard and this uses a randomized algorithm that + produces a feasible solution, but provides no guarantees on the + solution weight. + + Examples + -------- + >>> # Unweighted cases + >>> G = nx.path_graph((1, 2, 3, 4)) + >>> G.add_node(5) + >>> sorted(nx.k_edge_augmentation(G, k=1)) + [(1, 5)] + >>> sorted(nx.k_edge_augmentation(G, k=2)) + [(1, 5), (5, 4)] + >>> sorted(nx.k_edge_augmentation(G, k=3)) + [(1, 4), (1, 5), (2, 5), (3, 5), (4, 5)] + >>> complement = list(nx.k_edge_augmentation(G, k=5, partial=True)) + >>> G.add_edges_from(complement) + >>> nx.edge_connectivity(G) + 4 + + >>> # Weighted cases + >>> G = nx.path_graph((1, 2, 3, 4)) + >>> G.add_node(5) + >>> # avail can be a tuple with a dict + >>> avail = [(1, 5, {"weight": 11}), (2, 5, {"weight": 10})] + >>> sorted(nx.k_edge_augmentation(G, k=1, avail=avail, weight="weight")) + [(2, 5)] + >>> # or avail can be a 3-tuple with a real number + >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)] + >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail)) + [(1, 5), (2, 5), (4, 5)] + >>> # or avail can be a dict + >>> avail = {(1, 5): 11, (2, 5): 10, (4, 3): 1, (4, 5): 51} + >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail)) + [(1, 5), (2, 5), (4, 5)] + >>> # If augmentation is infeasible, then a partial solution can be found + >>> avail = {(1, 5): 11} + >>> sorted(nx.k_edge_augmentation(G, k=2, avail=avail, partial=True)) + [(1, 5)] + """ + try: + if k <= 0: + raise ValueError(f"k must be a positive integer, not {k}") + elif G.number_of_nodes() < k + 1: + msg = f"impossible to {k} connect in graph with less than {k + 1} nodes" + raise nx.NetworkXUnfeasible(msg) + elif avail is not None and len(avail) == 0: + if not nx.is_k_edge_connected(G, k): + raise nx.NetworkXUnfeasible("no available edges") + aug_edges = [] + elif k == 1: + aug_edges = one_edge_augmentation( + G, avail=avail, weight=weight, partial=partial + ) + elif k == 2: + aug_edges = bridge_augmentation(G, avail=avail, weight=weight) + else: + # raise NotImplementedError(f'not implemented for k>2. k={k}') + aug_edges = greedy_k_edge_augmentation( + G, k=k, avail=avail, weight=weight, seed=0 + ) + # Do eager evaluation so we can catch any exceptions + # Before executing partial code. + yield from list(aug_edges) + except nx.NetworkXUnfeasible: + if partial: + # Return all available edges + if avail is None: + aug_edges = complement_edges(G) + else: + # If we can't k-edge-connect the entire graph, try to + # k-edge-connect as much as possible + aug_edges = partial_k_edge_augmentation( + G, k=k, avail=avail, weight=weight + ) + yield from aug_edges + else: + raise + + +@nx._dispatchable +def partial_k_edge_augmentation(G, k, avail, weight=None): + """Finds augmentation that k-edge-connects as much of the graph as possible. + + When a k-edge-augmentation is not possible, we can still try to find a + small set of edges that partially k-edge-connects as much of the graph as + possible. All possible edges are generated between remaining parts. + This minimizes the number of k-edge-connected subgraphs in the resulting + graph and maximizes the edge connectivity between those subgraphs. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + k : integer + Desired edge connectivity + + avail : dict or a set of 2 or 3 tuples + For more details, see :func:`k_edge_augmentation`. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples. + For more details, see :func:`k_edge_augmentation`. + + Yields + ------ + edge : tuple + Edges in the partial augmentation of G. These edges k-edge-connect any + part of G where it is possible, and maximally connects the remaining + parts. In other words, all edges from avail are generated except for + those within subgraphs that have already become k-edge-connected. + + Notes + ----- + Construct H that augments G with all edges in avail. + Find the k-edge-subgraphs of H. + For each k-edge-subgraph, if the number of nodes is more than k, then find + the k-edge-augmentation of that graph and add it to the solution. Then add + all edges in avail between k-edge subgraphs to the solution. + + See Also + -------- + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) + >>> G.add_node(8) + >>> avail = [(1, 3), (1, 4), (1, 5), (2, 4), (2, 5), (3, 5), (1, 8)] + >>> sorted(partial_k_edge_augmentation(G, k=2, avail=avail)) + [(1, 5), (1, 8)] + """ + + def _edges_between_disjoint(H, only1, only2): + """finds edges between disjoint nodes""" + only1_adj = {u: set(H.adj[u]) for u in only1} + for u, neighbs in only1_adj.items(): + # Find the neighbors of u in only1 that are also in only2 + neighbs12 = neighbs.intersection(only2) + for v in neighbs12: + yield (u, v) + + avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) + + # Find which parts of the graph can be k-edge-connected + H = G.copy() + H.add_edges_from( + ( + (u, v, {"weight": w, "generator": (u, v)}) + for (u, v), w in zip(avail, avail_w) + ) + ) + k_edge_subgraphs = list(nx.k_edge_subgraphs(H, k=k)) + + # Generate edges to k-edge-connect internal subgraphs + for nodes in k_edge_subgraphs: + if len(nodes) > 1: + # Get the k-edge-connected subgraph + C = H.subgraph(nodes).copy() + # Find the internal edges that were available + sub_avail = { + d["generator"]: d["weight"] + for (u, v, d) in C.edges(data=True) + if "generator" in d + } + # Remove potential augmenting edges + C.remove_edges_from(sub_avail.keys()) + # Find a subset of these edges that makes the component + # k-edge-connected and ignore the rest + yield from nx.k_edge_augmentation(C, k=k, avail=sub_avail) + + # Generate all edges between CCs that could not be k-edge-connected + for cc1, cc2 in it.combinations(k_edge_subgraphs, 2): + for u, v in _edges_between_disjoint(H, cc1, cc2): + d = H.get_edge_data(u, v) + edge = d.get("generator", None) + if edge is not None: + yield edge + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable +def one_edge_augmentation(G, avail=None, weight=None, partial=False): + """Finds minimum weight set of edges to connect G. + + Equivalent to :func:`k_edge_augmentation` when k=1. Adding the resulting + edges to G will make it 1-edge-connected. The solution is optimal for both + weighted and non-weighted variants. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + avail : dict or a set of 2 or 3 tuples + For more details, see :func:`k_edge_augmentation`. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples. + For more details, see :func:`k_edge_augmentation`. + + partial : boolean + If partial is True and no feasible k-edge-augmentation exists, then the + augmenting edges minimize the number of connected components. + + Yields + ------ + edge : tuple + Edges in the one-augmentation of G + + Raises + ------ + NetworkXUnfeasible + If partial is False and no one-edge-augmentation exists. + + Notes + ----- + Uses either :func:`unconstrained_one_edge_augmentation` or + :func:`weighted_one_edge_augmentation` depending on whether ``avail`` is + specified. Both algorithms are based on finding a minimum spanning tree. + As such both algorithms find optimal solutions and run in linear time. + + See Also + -------- + :func:`k_edge_augmentation` + """ + if avail is None: + return unconstrained_one_edge_augmentation(G) + else: + return weighted_one_edge_augmentation( + G, avail=avail, weight=weight, partial=partial + ) + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable +def bridge_augmentation(G, avail=None, weight=None): + """Finds the a set of edges that bridge connects G. + + Equivalent to :func:`k_edge_augmentation` when k=2, and partial=False. + Adding the resulting edges to G will make it 2-edge-connected. If no + constraints are specified the returned set of edges is minimum an optimal, + otherwise the solution is approximated. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + avail : dict or a set of 2 or 3 tuples + For more details, see :func:`k_edge_augmentation`. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples. + For more details, see :func:`k_edge_augmentation`. + + Yields + ------ + edge : tuple + Edges in the bridge-augmentation of G + + Raises + ------ + NetworkXUnfeasible + If no bridge-augmentation exists. + + Notes + ----- + If there are no constraints the solution can be computed in linear time + using :func:`unconstrained_bridge_augmentation`. Otherwise, the problem + becomes NP-hard and is the solution is approximated by + :func:`weighted_bridge_augmentation`. + + See Also + -------- + :func:`k_edge_augmentation` + """ + if G.number_of_nodes() < 3: + raise nx.NetworkXUnfeasible("impossible to bridge connect less than 3 nodes") + if avail is None: + return unconstrained_bridge_augmentation(G) + else: + return weighted_bridge_augmentation(G, avail, weight=weight) + + +# --- Algorithms and Helpers --- + + +def _ordered(u, v): + """Returns the nodes in an undirected edge in lower-triangular order""" + return (u, v) if u < v else (v, u) + + +def _unpack_available_edges(avail, weight=None, G=None): + """Helper to separate avail into edges and corresponding weights""" + if weight is None: + weight = "weight" + if isinstance(avail, dict): + avail_uv = list(avail.keys()) + avail_w = list(avail.values()) + else: + + def _try_getitem(d): + try: + return d[weight] + except TypeError: + return d + + avail_uv = [tup[0:2] for tup in avail] + avail_w = [1 if len(tup) == 2 else _try_getitem(tup[-1]) for tup in avail] + + if G is not None: + # Edges already in the graph are filtered + flags = [not G.has_edge(u, v) for u, v in avail_uv] + avail_uv = list(it.compress(avail_uv, flags)) + avail_w = list(it.compress(avail_w, flags)) + return avail_uv, avail_w + + +MetaEdge = namedtuple("MetaEdge", ("meta_uv", "uv", "w")) + + +def _lightest_meta_edges(mapping, avail_uv, avail_w): + """Maps available edges in the original graph to edges in the metagraph. + + Parameters + ---------- + mapping : dict + mapping produced by :func:`collapse`, that maps each node in the + original graph to a node in the meta graph + + avail_uv : list + list of edges + + avail_w : list + list of edge weights + + Notes + ----- + Each node in the metagraph is a k-edge-connected component in the original + graph. We don't care about any edge within the same k-edge-connected + component, so we ignore self edges. We also are only interested in the + minimum weight edge bridging each k-edge-connected component so, we group + the edges by meta-edge and take the lightest in each group. + + Examples + -------- + >>> # Each group represents a meta-node + >>> groups = ([1, 2, 3], [4, 5], [6]) + >>> mapping = {n: meta_n for meta_n, ns in enumerate(groups) for n in ns} + >>> avail_uv = [(1, 2), (3, 6), (1, 4), (5, 2), (6, 1), (2, 6), (3, 1)] + >>> avail_w = [20, 99, 20, 15, 50, 99, 20] + >>> sorted(_lightest_meta_edges(mapping, avail_uv, avail_w)) + [MetaEdge(meta_uv=(0, 1), uv=(5, 2), w=15), MetaEdge(meta_uv=(0, 2), uv=(6, 1), w=50)] + """ + grouped_wuv = defaultdict(list) + for w, (u, v) in zip(avail_w, avail_uv): + # Order the meta-edge so it can be used as a dict key + meta_uv = _ordered(mapping[u], mapping[v]) + # Group each available edge using the meta-edge as a key + grouped_wuv[meta_uv].append((w, u, v)) + + # Now that all available edges are grouped, choose one per group + for (mu, mv), choices_wuv in grouped_wuv.items(): + # Ignore available edges within the same meta-node + if mu != mv: + # Choose the lightest available edge belonging to each meta-edge + w, u, v = min(choices_wuv) + yield MetaEdge((mu, mv), (u, v), w) + + +@nx._dispatchable +def unconstrained_one_edge_augmentation(G): + """Finds the smallest set of edges to connect G. + + This is a variant of the unweighted MST problem. + If G is not empty, a feasible solution always exists. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + Yields + ------ + edge : tuple + Edges in the one-edge-augmentation of G + + See Also + -------- + :func:`one_edge_augmentation` + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) + >>> G.add_nodes_from([6, 7, 8]) + >>> sorted(unconstrained_one_edge_augmentation(G)) + [(1, 4), (4, 6), (6, 7), (7, 8)] + """ + ccs1 = list(nx.connected_components(G)) + C = collapse(G, ccs1) + # When we are not constrained, we can just make a meta graph tree. + meta_nodes = list(C.nodes()) + # build a path in the metagraph + meta_aug = list(zip(meta_nodes, meta_nodes[1:])) + # map that path to the original graph + inverse = defaultdict(list) + for k, v in C.graph["mapping"].items(): + inverse[v].append(k) + for mu, mv in meta_aug: + yield (inverse[mu][0], inverse[mv][0]) + + +@nx._dispatchable +def weighted_one_edge_augmentation(G, avail, weight=None, partial=False): + """Finds the minimum weight set of edges to connect G if one exists. + + This is a variant of the weighted MST problem. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + avail : dict or a set of 2 or 3 tuples + For more details, see :func:`k_edge_augmentation`. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples. + For more details, see :func:`k_edge_augmentation`. + + partial : boolean + If partial is True and no feasible k-edge-augmentation exists, then the + augmenting edges minimize the number of connected components. + + Yields + ------ + edge : tuple + Edges in the subset of avail chosen to connect G. + + See Also + -------- + :func:`one_edge_augmentation` + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) + >>> G.add_nodes_from([6, 7, 8]) + >>> # any edge not in avail has an implicit weight of infinity + >>> avail = [(1, 3), (1, 5), (4, 7), (4, 8), (6, 1), (8, 1), (8, 2)] + >>> sorted(weighted_one_edge_augmentation(G, avail)) + [(1, 5), (4, 7), (6, 1), (8, 1)] + >>> # find another solution by giving large weights to edges in the + >>> # previous solution (note some of the old edges must be used) + >>> avail = [(1, 3), (1, 5, 99), (4, 7, 9), (6, 1, 99), (8, 1, 99), (8, 2)] + >>> sorted(weighted_one_edge_augmentation(G, avail)) + [(1, 5), (4, 7), (6, 1), (8, 2)] + """ + avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) + # Collapse CCs in the original graph into nodes in a metagraph + # Then find an MST of the metagraph instead of the original graph + C = collapse(G, nx.connected_components(G)) + mapping = C.graph["mapping"] + # Assign each available edge to an edge in the metagraph + candidate_mapping = _lightest_meta_edges(mapping, avail_uv, avail_w) + # nx.set_edge_attributes(C, name='weight', values=0) + C.add_edges_from( + (mu, mv, {"weight": w, "generator": uv}) + for (mu, mv), uv, w in candidate_mapping + ) + # Find MST of the meta graph + meta_mst = nx.minimum_spanning_tree(C) + if not partial and not nx.is_connected(meta_mst): + raise nx.NetworkXUnfeasible("Not possible to connect G with available edges") + # Yield the edge that generated the meta-edge + for mu, mv, d in meta_mst.edges(data=True): + if "generator" in d: + edge = d["generator"] + yield edge + + +@nx._dispatchable +def unconstrained_bridge_augmentation(G): + """Finds an optimal 2-edge-augmentation of G using the fewest edges. + + This is an implementation of the algorithm detailed in [1]_. + The basic idea is to construct a meta-graph of bridge-ccs, connect leaf + nodes of the trees to connect the entire graph, and finally connect the + leafs of the tree in dfs-preorder to bridge connect the entire graph. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + Yields + ------ + edge : tuple + Edges in the bridge augmentation of G + + Notes + ----- + Input: a graph G. + First find the bridge components of G and collapse each bridge-cc into a + node of a metagraph graph C, which is guaranteed to be a forest of trees. + + C contains p "leafs" --- nodes with exactly one incident edge. + C contains q "isolated nodes" --- nodes with no incident edges. + + Theorem: If p + q > 1, then at least :math:`ceil(p / 2) + q` edges are + needed to bridge connect C. This algorithm achieves this min number. + + The method first adds enough edges to make G into a tree and then pairs + leafs in a simple fashion. + + Let n be the number of trees in C. Let v(i) be an isolated vertex in the + i-th tree if one exists, otherwise it is a pair of distinct leafs nodes + in the i-th tree. Alternating edges from these sets (i.e. adding edges + A1 = [(v(i)[0], v(i + 1)[1]), v(i + 1)[0], v(i + 2)[1])...]) connects C + into a tree T. This tree has p' = p + 2q - 2(n -1) leafs and no isolated + vertices. A1 has n - 1 edges. The next step finds ceil(p' / 2) edges to + biconnect any tree with p' leafs. + + Convert T into an arborescence T' by picking an arbitrary root node with + degree >= 2 and directing all edges away from the root. Note the + implementation implicitly constructs T'. + + The leafs of T are the nodes with no existing edges in T'. + Order the leafs of T' by DFS preorder. Then break this list in half + and add the zipped pairs to A2. + + The set A = A1 + A2 is the minimum augmentation in the metagraph. + + To convert this to edges in the original graph + + References + ---------- + .. [1] Eswaran, Kapali P., and R. Endre Tarjan. (1975) Augmentation problems. + http://epubs.siam.org/doi/abs/10.1137/0205044 + + See Also + -------- + :func:`bridge_augmentation` + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) + >>> sorted(unconstrained_bridge_augmentation(G)) + [(1, 7)] + >>> G = nx.path_graph((1, 2, 3, 2, 4, 5, 6, 7)) + >>> sorted(unconstrained_bridge_augmentation(G)) + [(1, 3), (3, 7)] + >>> G = nx.Graph([(0, 1), (0, 2), (1, 2)]) + >>> G.add_node(4) + >>> sorted(unconstrained_bridge_augmentation(G)) + [(1, 4), (4, 0)] + """ + # ----- + # Mapping of terms from (Eswaran and Tarjan): + # G = G_0 - the input graph + # C = G_0' - the bridge condensation of G. (This is a forest of trees) + # A1 = A_1 - the edges to connect the forest into a tree + # leaf = pendant - a node with degree of 1 + + # alpha(v) = maps the node v in G to its meta-node in C + # beta(x) = maps the meta-node x in C to any node in the bridge + # component of G corresponding to x. + + # find the 2-edge-connected components of G + bridge_ccs = list(nx.connectivity.bridge_components(G)) + # condense G into an forest C + C = collapse(G, bridge_ccs) + + # Choose pairs of distinct leaf nodes in each tree. If this is not + # possible then make a pair using the single isolated node in the tree. + vset1 = [ + tuple(cc) * 2 # case1: an isolated node + if len(cc) == 1 + else sorted(cc, key=C.degree)[0:2] # case2: pair of leaf nodes + for cc in nx.connected_components(C) + ] + if len(vset1) > 1: + # Use this set to construct edges that connect C into a tree. + nodes1 = [vs[0] for vs in vset1] + nodes2 = [vs[1] for vs in vset1] + A1 = list(zip(nodes1[1:], nodes2)) + else: + A1 = [] + # Connect each tree in the forest to construct an arborescence + T = C.copy() + T.add_edges_from(A1) + + # If there are only two leaf nodes, we simply connect them. + leafs = [n for n, d in T.degree() if d == 1] + if len(leafs) == 1: + A2 = [] + if len(leafs) == 2: + A2 = [tuple(leafs)] + else: + # Choose an arbitrary non-leaf root + try: + root = next(n for n, d in T.degree() if d > 1) + except StopIteration: # no nodes found with degree > 1 + return + # order the leaves of C by (induced directed) preorder + v2 = [n for n in nx.dfs_preorder_nodes(T, root) if T.degree(n) == 1] + # connecting first half of the leafs in pre-order to the second + # half will bridge connect the tree with the fewest edges. + half = math.ceil(len(v2) / 2) + A2 = list(zip(v2[:half], v2[-half:])) + + # collect the edges used to augment the original forest + aug_tree_edges = A1 + A2 + + # Construct the mapping (beta) from meta-nodes to regular nodes + inverse = defaultdict(list) + for k, v in C.graph["mapping"].items(): + inverse[v].append(k) + # sort so we choose minimum degree nodes first + inverse = { + mu: sorted(mapped, key=lambda u: (G.degree(u), u)) + for mu, mapped in inverse.items() + } + + # For each meta-edge, map back to an arbitrary pair in the original graph + G2 = G.copy() + for mu, mv in aug_tree_edges: + # Find the first available edge that doesn't exist and return it + for u, v in it.product(inverse[mu], inverse[mv]): + if not G2.has_edge(u, v): + G2.add_edge(u, v) + yield u, v + break + + +@nx._dispatchable +def weighted_bridge_augmentation(G, avail, weight=None): + """Finds an approximate min-weight 2-edge-augmentation of G. + + This is an implementation of the approximation algorithm detailed in [1]_. + It chooses a set of edges from avail to add to G that renders it + 2-edge-connected if such a subset exists. This is done by finding a + minimum spanning arborescence of a specially constructed metagraph. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + avail : set of 2 or 3 tuples. + candidate edges (with optional weights) to choose from + + weight : string + key to use to find weights if avail is a set of 3-tuples where the + third item in each tuple is a dictionary. + + Yields + ------ + edge : tuple + Edges in the subset of avail chosen to bridge augment G. + + Notes + ----- + Finding a weighted 2-edge-augmentation is NP-hard. + Any edge not in ``avail`` is considered to have a weight of infinity. + The approximation factor is 2 if ``G`` is connected and 3 if it is not. + Runs in :math:`O(m + n log(n))` time + + References + ---------- + .. [1] Khuller, Samir, and Ramakrishna Thurimella. (1993) Approximation + algorithms for graph augmentation. + http://www.sciencedirect.com/science/article/pii/S0196677483710102 + + See Also + -------- + :func:`bridge_augmentation` + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.path_graph((1, 2, 3, 4)) + >>> # When the weights are equal, (1, 4) is the best + >>> avail = [(1, 4, 1), (1, 3, 1), (2, 4, 1)] + >>> sorted(weighted_bridge_augmentation(G, avail)) + [(1, 4)] + >>> # Giving (1, 4) a high weight makes the two edge solution the best. + >>> avail = [(1, 4, 1000), (1, 3, 1), (2, 4, 1)] + >>> sorted(weighted_bridge_augmentation(G, avail)) + [(1, 3), (2, 4)] + >>> # ------ + >>> G = nx.path_graph((1, 2, 3, 4)) + >>> G.add_node(5) + >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 1)] + >>> sorted(weighted_bridge_augmentation(G, avail=avail)) + [(1, 5), (4, 5)] + >>> avail = [(1, 5, 11), (2, 5, 10), (4, 3, 1), (4, 5, 51)] + >>> sorted(weighted_bridge_augmentation(G, avail=avail)) + [(1, 5), (2, 5), (4, 5)] + """ + + if weight is None: + weight = "weight" + + # If input G is not connected the approximation factor increases to 3 + if not nx.is_connected(G): + H = G.copy() + connectors = list(one_edge_augmentation(H, avail=avail, weight=weight)) + H.add_edges_from(connectors) + + yield from connectors + else: + connectors = [] + H = G + + if len(avail) == 0: + if nx.has_bridges(H): + raise nx.NetworkXUnfeasible("no augmentation possible") + + avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=H) + + # Collapse input into a metagraph. Meta nodes are bridge-ccs + bridge_ccs = nx.connectivity.bridge_components(H) + C = collapse(H, bridge_ccs) + + # Use the meta graph to shrink avail to a small feasible subset + mapping = C.graph["mapping"] + # Choose the minimum weight feasible edge in each group + meta_to_wuv = { + (mu, mv): (w, uv) + for (mu, mv), uv, w in _lightest_meta_edges(mapping, avail_uv, avail_w) + } + + # Mapping of terms from (Khuller and Thurimella): + # C : G_0 = (V, E^0) + # This is the metagraph where each node is a 2-edge-cc in G. + # The edges in C represent bridges in the original graph. + # (mu, mv) : E - E^0 # they group both avail and given edges in E + # T : \Gamma + # D : G^D = (V, E_D) + + # The paper uses ancestor because children point to parents, which is + # contrary to networkx standards. So, we actually need to run + # nx.least_common_ancestor on the reversed Tree. + + # Pick an arbitrary leaf from C as the root + try: + root = next(n for n, d in C.degree() if d == 1) + except StopIteration: # no nodes found with degree == 1 + return + # Root C into a tree TR by directing all edges away from the root + # Note in their paper T directs edges towards the root + TR = nx.dfs_tree(C, root) + + # Add to D the directed edges of T and set their weight to zero + # This indicates that it costs nothing to use edges that were given. + D = nx.reverse(TR).copy() + + nx.set_edge_attributes(D, name="weight", values=0) + + # The LCA of mu and mv in T is the shared ancestor of mu and mv that is + # located farthest from the root. + lca_gen = nx.tree_all_pairs_lowest_common_ancestor( + TR, root=root, pairs=meta_to_wuv.keys() + ) + + for (mu, mv), lca in lca_gen: + w, uv = meta_to_wuv[(mu, mv)] + if lca == mu: + # If u is an ancestor of v in TR, then add edge u->v to D + D.add_edge(lca, mv, weight=w, generator=uv) + elif lca == mv: + # If v is an ancestor of u in TR, then add edge v->u to D + D.add_edge(lca, mu, weight=w, generator=uv) + else: + # If neither u nor v is a ancestor of the other in TR + # let t = lca(TR, u, v) and add edges t->u and t->v + # Track the original edge that GENERATED these edges. + D.add_edge(lca, mu, weight=w, generator=uv) + D.add_edge(lca, mv, weight=w, generator=uv) + + # Then compute a minimum rooted branching + try: + # Note the original edges must be directed towards to root for the + # branching to give us a bridge-augmentation. + A = _minimum_rooted_branching(D, root) + except nx.NetworkXException as err: + # If there is no branching then augmentation is not possible + raise nx.NetworkXUnfeasible("no 2-edge-augmentation possible") from err + + # For each edge e, in the branching that did not belong to the directed + # tree T, add the corresponding edge that **GENERATED** it (this is not + # necessarily e itself!) + + # ensure the third case does not generate edges twice + bridge_connectors = set() + for mu, mv in A.edges(): + data = D.get_edge_data(mu, mv) + if "generator" in data: + # Add the avail edge that generated the branching edge. + edge = data["generator"] + bridge_connectors.add(edge) + + yield from bridge_connectors + + +def _minimum_rooted_branching(D, root): + """Helper function to compute a minimum rooted branching (aka rooted + arborescence) + + Before the branching can be computed, the directed graph must be rooted by + removing the predecessors of root. + + A branching / arborescence of rooted graph G is a subgraph that contains a + directed path from the root to every other vertex. It is the directed + analog of the minimum spanning tree problem. + + References + ---------- + [1] Khuller, Samir (2002) Advanced Algorithms Lecture 24 Notes. + https://web.archive.org/web/20121030033722/https://www.cs.umd.edu/class/spring2011/cmsc651/lec07.pdf + """ + rooted = D.copy() + # root the graph by removing all predecessors to `root`. + rooted.remove_edges_from([(u, root) for u in D.predecessors(root)]) + # Then compute the branching / arborescence. + A = nx.minimum_spanning_arborescence(rooted) + return A + + +@nx._dispatchable(returns_graph=True) +def collapse(G, grouped_nodes): + """Collapses each group of nodes into a single node. + + This is similar to condensation, but works on undirected graphs. + + Parameters + ---------- + G : NetworkX Graph + + grouped_nodes: list or generator + Grouping of nodes to collapse. The grouping must be disjoint. + If grouped_nodes are strongly_connected_components then this is + equivalent to :func:`condensation`. + + Returns + ------- + C : NetworkX Graph + The collapsed graph C of G with respect to the node grouping. The node + labels are integers corresponding to the index of the component in the + list of grouped_nodes. C has a graph attribute named 'mapping' with a + dictionary mapping the original nodes to the nodes in C to which they + belong. Each node in C also has a node attribute 'members' with the set + of original nodes in G that form the group that the node in C + represents. + + Examples + -------- + >>> # Collapses a graph using disjoint groups, but not necessarily connected + >>> G = nx.Graph([(1, 0), (2, 3), (3, 1), (3, 4), (4, 5), (5, 6), (5, 7)]) + >>> G.add_node("A") + >>> grouped_nodes = [{0, 1, 2, 3}, {5, 6, 7}] + >>> C = collapse(G, grouped_nodes) + >>> members = nx.get_node_attributes(C, "members") + >>> sorted(members.keys()) + [0, 1, 2, 3] + >>> member_values = set(map(frozenset, members.values())) + >>> assert {0, 1, 2, 3} in member_values + >>> assert {4} in member_values + >>> assert {5, 6, 7} in member_values + >>> assert {"A"} in member_values + """ + mapping = {} + members = {} + C = G.__class__() + i = 0 # required if G is empty + remaining = set(G.nodes()) + for i, group in enumerate(grouped_nodes): + group = set(group) + assert remaining.issuperset(group), ( + "grouped nodes must exist in G and be disjoint" + ) + remaining.difference_update(group) + members[i] = group + mapping.update((n, i) for n in group) + # remaining nodes are in their own group + for i, node in enumerate(remaining, start=i + 1): + group = {node} + members[i] = group + mapping.update((n, i) for n in group) + number_of_groups = i + 1 + C.add_nodes_from(range(number_of_groups)) + C.add_edges_from( + (mapping[u], mapping[v]) for u, v in G.edges() if mapping[u] != mapping[v] + ) + # Add a list of members (ie original nodes) to each node (ie scc) in C. + nx.set_node_attributes(C, name="members", values=members) + # Add mapping dict as graph attribute + C.graph["mapping"] = mapping + return C + + +@nx._dispatchable +def complement_edges(G): + """Returns only the edges in the complement of G + + Parameters + ---------- + G : NetworkX Graph + + Yields + ------ + edge : tuple + Edges in the complement of G + + Examples + -------- + >>> G = nx.path_graph((1, 2, 3, 4)) + >>> sorted(complement_edges(G)) + [(1, 3), (1, 4), (2, 4)] + >>> G = nx.path_graph((1, 2, 3, 4), nx.DiGraph()) + >>> sorted(complement_edges(G)) + [(1, 3), (1, 4), (2, 1), (2, 4), (3, 1), (3, 2), (4, 1), (4, 2), (4, 3)] + >>> G = nx.complete_graph(1000) + >>> sorted(complement_edges(G)) + [] + """ + G_adj = G._adj # Store as a variable to eliminate attribute lookup + if G.is_directed(): + for u, v in it.combinations(G.nodes(), 2): + if v not in G_adj[u]: + yield (u, v) + if u not in G_adj[v]: + yield (v, u) + else: + for u, v in it.combinations(G.nodes(), 2): + if v not in G_adj[u]: + yield (u, v) + + +def _compat_shuffle(rng, input): + """wrapper around rng.shuffle for python 2 compatibility reasons""" + rng.shuffle(input) + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@py_random_state(4) +@nx._dispatchable +def greedy_k_edge_augmentation(G, k, avail=None, weight=None, seed=None): + """Greedy algorithm for finding a k-edge-augmentation + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + k : integer + Desired edge connectivity + + avail : dict or a set of 2 or 3 tuples + For more details, see :func:`k_edge_augmentation`. + + weight : string + key to use to find weights if ``avail`` is a set of 3-tuples. + For more details, see :func:`k_edge_augmentation`. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Yields + ------ + edge : tuple + Edges in the greedy augmentation of G + + Notes + ----- + The algorithm is simple. Edges are incrementally added between parts of the + graph that are not yet locally k-edge-connected. Then edges are from the + augmenting set are pruned as long as local-edge-connectivity is not broken. + + This algorithm is greedy and does not provide optimality guarantees. It + exists only to provide :func:`k_edge_augmentation` with the ability to + generate a feasible solution for arbitrary k. + + See Also + -------- + :func:`k_edge_augmentation` + + Examples + -------- + >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) + >>> sorted(greedy_k_edge_augmentation(G, k=2)) + [(1, 7)] + >>> sorted(greedy_k_edge_augmentation(G, k=1, avail=[])) + [] + >>> G = nx.path_graph((1, 2, 3, 4, 5, 6, 7)) + >>> avail = {(u, v): 1 for (u, v) in complement_edges(G)} + >>> # randomized pruning process can produce different solutions + >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=2)) + [(1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (2, 4), (2, 6), (3, 7), (5, 7)] + >>> sorted(greedy_k_edge_augmentation(G, k=4, avail=avail, seed=3)) + [(1, 3), (1, 5), (1, 6), (2, 4), (2, 6), (3, 7), (4, 7), (5, 7)] + """ + # Result set + aug_edges = [] + + done = is_k_edge_connected(G, k) + if done: + return + if avail is None: + # all edges are available + avail_uv = list(complement_edges(G)) + avail_w = [1] * len(avail_uv) + else: + # Get the unique set of unweighted edges + avail_uv, avail_w = _unpack_available_edges(avail, weight=weight, G=G) + + # Greedy: order lightest edges. Use degree sum to tie-break + tiebreaker = [sum(map(G.degree, uv)) for uv in avail_uv] + avail_wduv = sorted(zip(avail_w, tiebreaker, avail_uv)) + avail_uv = [uv for w, d, uv in avail_wduv] + + # Incrementally add edges in until we are k-connected + H = G.copy() + for u, v in avail_uv: + done = False + if not is_locally_k_edge_connected(H, u, v, k=k): + # Only add edges in parts that are not yet locally k-edge-connected + aug_edges.append((u, v)) + H.add_edge(u, v) + # Did adding this edge help? + if H.degree(u) >= k and H.degree(v) >= k: + done = is_k_edge_connected(H, k) + if done: + break + + # Check for feasibility + if not done: + raise nx.NetworkXUnfeasible("not able to k-edge-connect with available edges") + + # Randomized attempt to reduce the size of the solution + _compat_shuffle(seed, aug_edges) + for u, v in list(aug_edges): + # Don't remove if we know it would break connectivity + if H.degree(u) <= k or H.degree(v) <= k: + continue + H.remove_edge(u, v) + aug_edges.remove((u, v)) + if not is_k_edge_connected(H, k=k): + # If removing this edge breaks feasibility, undo + H.add_edge(u, v) + aug_edges.append((u, v)) + + # Generate results + yield from aug_edges diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py new file mode 100644 index 0000000000000000000000000000000000000000..96886f2ba39db1bb39812440e5d69b6f073b2af5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/edge_kcomponents.py @@ -0,0 +1,592 @@ +""" +Algorithms for finding k-edge-connected components and subgraphs. + +A k-edge-connected component (k-edge-cc) is a maximal set of nodes in G, such +that all pairs of node have an edge-connectivity of at least k. + +A k-edge-connected subgraph (k-edge-subgraph) is a maximal set of nodes in G, +such that the subgraph of G defined by the nodes has an edge-connectivity at +least k. +""" + +import itertools as it +from functools import partial + +import networkx as nx +from networkx.utils import arbitrary_element, not_implemented_for + +__all__ = [ + "k_edge_components", + "k_edge_subgraphs", + "bridge_components", + "EdgeComponentAuxGraph", +] + + +@not_implemented_for("multigraph") +@nx._dispatchable +def k_edge_components(G, k): + """Generates nodes in each maximal k-edge-connected component in G. + + Parameters + ---------- + G : NetworkX graph + + k : Integer + Desired edge connectivity + + Returns + ------- + k_edge_components : a generator of k-edge-ccs. Each set of returned nodes + will have k-edge-connectivity in the graph G. + + See Also + -------- + :func:`local_edge_connectivity` + :func:`k_edge_subgraphs` : similar to this function, but the subgraph + defined by the nodes must also have k-edge-connectivity. + :func:`k_components` : similar to this function, but uses node-connectivity + instead of edge-connectivity + + Raises + ------ + NetworkXNotImplemented + If the input graph is a multigraph. + + ValueError: + If k is less than 1 + + Notes + ----- + Attempts to use the most efficient implementation available based on k. + If k=1, this is simply connected components for directed graphs and + connected components for undirected graphs. + If k=2 on an efficient bridge connected component algorithm from _[1] is + run based on the chain decomposition. + Otherwise, the algorithm from _[2] is used. + + Examples + -------- + >>> import itertools as it + >>> from networkx.utils import pairwise + >>> paths = [ + ... (1, 2, 4, 3, 1, 4), + ... (5, 6, 7, 8, 5, 7, 8, 6), + ... ] + >>> G = nx.Graph() + >>> G.add_nodes_from(it.chain(*paths)) + >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths])) + >>> # note this returns {1, 4} unlike k_edge_subgraphs + >>> sorted(map(sorted, nx.k_edge_components(G, k=3))) + [[1, 4], [2], [3], [5, 6, 7, 8]] + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Bridge_%28graph_theory%29 + .. [2] Wang, Tianhao, et al. (2015) A simple algorithm for finding all + k-edge-connected components. + http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264 + """ + # Compute k-edge-ccs using the most efficient algorithms available. + if k < 1: + raise ValueError("k cannot be less than 1") + if G.is_directed(): + if k == 1: + return nx.strongly_connected_components(G) + else: + # TODO: investigate https://arxiv.org/abs/1412.6466 for k=2 + aux_graph = EdgeComponentAuxGraph.construct(G) + return aux_graph.k_edge_components(k) + else: + if k == 1: + return nx.connected_components(G) + elif k == 2: + return bridge_components(G) + else: + aux_graph = EdgeComponentAuxGraph.construct(G) + return aux_graph.k_edge_components(k) + + +@not_implemented_for("multigraph") +@nx._dispatchable +def k_edge_subgraphs(G, k): + """Generates nodes in each maximal k-edge-connected subgraph in G. + + Parameters + ---------- + G : NetworkX graph + + k : Integer + Desired edge connectivity + + Returns + ------- + k_edge_subgraphs : a generator of k-edge-subgraphs + Each k-edge-subgraph is a maximal set of nodes that defines a subgraph + of G that is k-edge-connected. + + See Also + -------- + :func:`edge_connectivity` + :func:`k_edge_components` : similar to this function, but nodes only + need to have k-edge-connectivity within the graph G and the subgraphs + might not be k-edge-connected. + + Raises + ------ + NetworkXNotImplemented + If the input graph is a multigraph. + + ValueError: + If k is less than 1 + + Notes + ----- + Attempts to use the most efficient implementation available based on k. + If k=1, or k=2 and the graph is undirected, then this simply calls + `k_edge_components`. Otherwise the algorithm from _[1] is used. + + Examples + -------- + >>> import itertools as it + >>> from networkx.utils import pairwise + >>> paths = [ + ... (1, 2, 4, 3, 1, 4), + ... (5, 6, 7, 8, 5, 7, 8, 6), + ... ] + >>> G = nx.Graph() + >>> G.add_nodes_from(it.chain(*paths)) + >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths])) + >>> # note this does not return {1, 4} unlike k_edge_components + >>> sorted(map(sorted, nx.k_edge_subgraphs(G, k=3))) + [[1], [2], [3], [4], [5, 6, 7, 8]] + + References + ---------- + .. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs + from a large graph. ACM International Conference on Extending Database + Technology 2012 480-–491. + https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf + """ + if k < 1: + raise ValueError("k cannot be less than 1") + if G.is_directed(): + if k <= 1: + # For directed graphs , + # When k == 1, k-edge-ccs and k-edge-subgraphs are the same + return k_edge_components(G, k) + else: + return _k_edge_subgraphs_nodes(G, k) + else: + if k <= 2: + # For undirected graphs, + # when k <= 2, k-edge-ccs and k-edge-subgraphs are the same + return k_edge_components(G, k) + else: + return _k_edge_subgraphs_nodes(G, k) + + +def _k_edge_subgraphs_nodes(G, k): + """Helper to get the nodes from the subgraphs. + + This allows k_edge_subgraphs to return a generator. + """ + for C in general_k_edge_subgraphs(G, k): + yield set(C.nodes()) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def bridge_components(G): + """Finds all bridge-connected components G. + + Parameters + ---------- + G : NetworkX undirected graph + + Returns + ------- + bridge_components : a generator of 2-edge-connected components + + + See Also + -------- + :func:`k_edge_subgraphs` : this function is a special case for an + undirected graph where k=2. + :func:`biconnected_components` : similar to this function, but is defined + using 2-node-connectivity instead of 2-edge-connectivity. + + Raises + ------ + NetworkXNotImplemented + If the input graph is directed or a multigraph. + + Notes + ----- + Bridge-connected components are also known as 2-edge-connected components. + + Examples + -------- + >>> # The barbell graph with parameter zero has a single bridge + >>> G = nx.barbell_graph(5, 0) + >>> from networkx.algorithms.connectivity.edge_kcomponents import bridge_components + >>> sorted(map(sorted, bridge_components(G))) + [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]] + """ + H = G.copy() + H.remove_edges_from(nx.bridges(G)) + yield from nx.connected_components(H) + + +class EdgeComponentAuxGraph: + r"""A simple algorithm to find all k-edge-connected components in a graph. + + Constructing the auxiliary graph (which may take some time) allows for the + k-edge-ccs to be found in linear time for arbitrary k. + + Notes + ----- + This implementation is based on [1]_. The idea is to construct an auxiliary + graph from which the k-edge-ccs can be extracted in linear time. The + auxiliary graph is constructed in $O(|V|\cdot F)$ operations, where F is the + complexity of max flow. Querying the components takes an additional $O(|V|)$ + operations. This algorithm can be slow for large graphs, but it handles an + arbitrary k and works for both directed and undirected inputs. + + The undirected case for k=1 is exactly connected components. + The undirected case for k=2 is exactly bridge connected components. + The directed case for k=1 is exactly strongly connected components. + + References + ---------- + .. [1] Wang, Tianhao, et al. (2015) A simple algorithm for finding all + k-edge-connected components. + http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0136264 + + Examples + -------- + >>> import itertools as it + >>> from networkx.utils import pairwise + >>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph + >>> # Build an interesting graph with multiple levels of k-edge-ccs + >>> paths = [ + ... (1, 2, 3, 4, 1, 3, 4, 2), # a 3-edge-cc (a 4 clique) + ... (5, 6, 7, 5), # a 2-edge-cc (a 3 clique) + ... (1, 5), # combine first two ccs into a 1-edge-cc + ... (0,), # add an additional disconnected 1-edge-cc + ... ] + >>> G = nx.Graph() + >>> G.add_nodes_from(it.chain(*paths)) + >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths])) + >>> # Constructing the AuxGraph takes about O(n ** 4) + >>> aux_graph = EdgeComponentAuxGraph.construct(G) + >>> # Once constructed, querying takes O(n) + >>> sorted(map(sorted, aux_graph.k_edge_components(k=1))) + [[0], [1, 2, 3, 4, 5, 6, 7]] + >>> sorted(map(sorted, aux_graph.k_edge_components(k=2))) + [[0], [1, 2, 3, 4], [5, 6, 7]] + >>> sorted(map(sorted, aux_graph.k_edge_components(k=3))) + [[0], [1, 2, 3, 4], [5], [6], [7]] + >>> sorted(map(sorted, aux_graph.k_edge_components(k=4))) + [[0], [1], [2], [3], [4], [5], [6], [7]] + + The auxiliary graph is primarily used for k-edge-ccs but it + can also speed up the queries of k-edge-subgraphs by refining the + search space. + + >>> import itertools as it + >>> from networkx.utils import pairwise + >>> from networkx.algorithms.connectivity import EdgeComponentAuxGraph + >>> paths = [ + ... (1, 2, 4, 3, 1, 4), + ... ] + >>> G = nx.Graph() + >>> G.add_nodes_from(it.chain(*paths)) + >>> G.add_edges_from(it.chain(*[pairwise(path) for path in paths])) + >>> aux_graph = EdgeComponentAuxGraph.construct(G) + >>> sorted(map(sorted, aux_graph.k_edge_subgraphs(k=3))) + [[1], [2], [3], [4]] + >>> sorted(map(sorted, aux_graph.k_edge_components(k=3))) + [[1, 4], [2], [3]] + """ + + # @not_implemented_for('multigraph') # TODO: fix decor for classmethods + @classmethod + def construct(EdgeComponentAuxGraph, G): + """Builds an auxiliary graph encoding edge-connectivity between nodes. + + Notes + ----- + Given G=(V, E), initialize an empty auxiliary graph A. + Choose an arbitrary source node s. Initialize a set N of available + nodes (that can be used as the sink). The algorithm picks an + arbitrary node t from N - {s}, and then computes the minimum st-cut + (S, T) with value w. If G is directed the minimum of the st-cut or + the ts-cut is used instead. Then, the edge (s, t) is added to the + auxiliary graph with weight w. The algorithm is called recursively + first using S as the available nodes and s as the source, and then + using T and t. Recursion stops when the source is the only available + node. + + Parameters + ---------- + G : NetworkX graph + """ + # workaround for classmethod decorator + not_implemented_for("multigraph")(lambda G: G)(G) + + def _recursive_build(H, A, source, avail): + # Terminate once the flow has been compute to every node. + if {source} == avail: + return + # pick an arbitrary node as the sink + sink = arbitrary_element(avail - {source}) + # find the minimum cut and its weight + value, (S, T) = nx.minimum_cut(H, source, sink) + if H.is_directed(): + # check if the reverse direction has a smaller cut + value_, (T_, S_) = nx.minimum_cut(H, sink, source) + if value_ < value: + value, S, T = value_, S_, T_ + # add edge with weight of cut to the aux graph + A.add_edge(source, sink, weight=value) + # recursively call until all but one node is used + _recursive_build(H, A, source, avail.intersection(S)) + _recursive_build(H, A, sink, avail.intersection(T)) + + # Copy input to ensure all edges have unit capacity + H = G.__class__() + H.add_nodes_from(G.nodes()) + H.add_edges_from(G.edges(), capacity=1) + + # A is the auxiliary graph to be constructed + # It is a weighted undirected tree + A = nx.Graph() + + # Pick an arbitrary node as the source + if H.number_of_nodes() > 0: + source = arbitrary_element(H.nodes()) + # Initialize a set of elements that can be chosen as the sink + avail = set(H.nodes()) + + # This constructs A + _recursive_build(H, A, source, avail) + + # This class is a container the holds the auxiliary graph A and + # provides access the k_edge_components function. + self = EdgeComponentAuxGraph() + self.A = A + self.H = H + return self + + def k_edge_components(self, k): + """Queries the auxiliary graph for k-edge-connected components. + + Parameters + ---------- + k : Integer + Desired edge connectivity + + Returns + ------- + k_edge_components : a generator of k-edge-ccs + + Notes + ----- + Given the auxiliary graph, the k-edge-connected components can be + determined in linear time by removing all edges with weights less than + k from the auxiliary graph. The resulting connected components are the + k-edge-ccs in the original graph. + """ + if k < 1: + raise ValueError("k cannot be less than 1") + A = self.A + # "traverse the auxiliary graph A and delete all edges with weights less + # than k" + aux_weights = nx.get_edge_attributes(A, "weight") + # Create a relevant graph with the auxiliary edges with weights >= k + R = nx.Graph() + R.add_nodes_from(A.nodes()) + R.add_edges_from(e for e, w in aux_weights.items() if w >= k) + + # Return the nodes that are k-edge-connected in the original graph + yield from nx.connected_components(R) + + def k_edge_subgraphs(self, k): + """Queries the auxiliary graph for k-edge-connected subgraphs. + + Parameters + ---------- + k : Integer + Desired edge connectivity + + Returns + ------- + k_edge_subgraphs : a generator of k-edge-subgraphs + + Notes + ----- + Refines the k-edge-ccs into k-edge-subgraphs. The running time is more + than $O(|V|)$. + + For single values of k it is faster to use `nx.k_edge_subgraphs`. + But for multiple values of k, it can be faster to build AuxGraph and + then use this method. + """ + if k < 1: + raise ValueError("k cannot be less than 1") + H = self.H + A = self.A + # "traverse the auxiliary graph A and delete all edges with weights less + # than k" + aux_weights = nx.get_edge_attributes(A, "weight") + # Create a relevant graph with the auxiliary edges with weights >= k + R = nx.Graph() + R.add_nodes_from(A.nodes()) + R.add_edges_from(e for e, w in aux_weights.items() if w >= k) + + # Return the components whose subgraphs are k-edge-connected + for cc in nx.connected_components(R): + if len(cc) < k: + # Early return optimization + for node in cc: + yield {node} + else: + # Call subgraph solution to refine the results + C = H.subgraph(cc) + yield from k_edge_subgraphs(C, k) + + +def _low_degree_nodes(G, k, nbunch=None): + """Helper for finding nodes with degree less than k.""" + # Nodes with degree less than k cannot be k-edge-connected. + if G.is_directed(): + # Consider both in and out degree in the directed case + seen = set() + for node, degree in G.out_degree(nbunch): + if degree < k: + seen.add(node) + yield node + for node, degree in G.in_degree(nbunch): + if node not in seen and degree < k: + seen.add(node) + yield node + else: + # Only the degree matters in the undirected case + for node, degree in G.degree(nbunch): + if degree < k: + yield node + + +def _high_degree_components(G, k): + """Helper for filtering components that can't be k-edge-connected. + + Removes and generates each node with degree less than k. Then generates + remaining components where all nodes have degree at least k. + """ + # Iteratively remove parts of the graph that are not k-edge-connected + H = G.copy() + singletons = set(_low_degree_nodes(H, k)) + while singletons: + # Only search neighbors of removed nodes + nbunch = set(it.chain.from_iterable(map(H.neighbors, singletons))) + nbunch.difference_update(singletons) + H.remove_nodes_from(singletons) + for node in singletons: + yield {node} + singletons = set(_low_degree_nodes(H, k, nbunch)) + + # Note: remaining connected components may not be k-edge-connected + if G.is_directed(): + yield from nx.strongly_connected_components(H) + else: + yield from nx.connected_components(H) + + +@nx._dispatchable(returns_graph=True) +def general_k_edge_subgraphs(G, k): + """General algorithm to find all maximal k-edge-connected subgraphs in `G`. + + Parameters + ---------- + G : nx.Graph + Graph in which all maximal k-edge-connected subgraphs will be found. + + k : int + + Yields + ------ + k_edge_subgraphs : Graph instances that are k-edge-subgraphs + Each k-edge-subgraph contains a maximal set of nodes that defines a + subgraph of `G` that is k-edge-connected. + + Notes + ----- + Implementation of the basic algorithm from [1]_. The basic idea is to find + a global minimum cut of the graph. If the cut value is at least k, then the + graph is a k-edge-connected subgraph and can be added to the results. + Otherwise, the cut is used to split the graph in two and the procedure is + applied recursively. If the graph is just a single node, then it is also + added to the results. At the end, each result is either guaranteed to be + a single node or a subgraph of G that is k-edge-connected. + + This implementation contains optimizations for reducing the number of calls + to max-flow, but there are other optimizations in [1]_ that could be + implemented. + + References + ---------- + .. [1] Zhou, Liu, et al. (2012) Finding maximal k-edge-connected subgraphs + from a large graph. ACM International Conference on Extending Database + Technology 2012 480-–491. + https://openproceedings.org/2012/conf/edbt/ZhouLYLCL12.pdf + + Examples + -------- + >>> from networkx.utils import pairwise + >>> paths = [ + ... (11, 12, 13, 14, 11, 13, 14, 12), # a 4-clique + ... (21, 22, 23, 24, 21, 23, 24, 22), # another 4-clique + ... # connect the cliques with high degree but low connectivity + ... (50, 13), + ... (12, 50, 22), + ... (13, 102, 23), + ... (14, 101, 24), + ... ] + >>> G = nx.Graph(it.chain(*[pairwise(path) for path in paths])) + >>> sorted(len(k_sg) for k_sg in k_edge_subgraphs(G, k=3)) + [1, 1, 1, 4, 4] + """ + if k < 1: + raise ValueError("k cannot be less than 1") + + # Node pruning optimization (incorporates early return) + # find_ccs is either connected_components/strongly_connected_components + find_ccs = partial(_high_degree_components, k=k) + + # Quick return optimization + if G.number_of_nodes() < k: + for node in G.nodes(): + yield G.subgraph([node]).copy() + return + + # Intermediate results + R0 = {G.subgraph(cc).copy() for cc in find_ccs(G)} + # Subdivide CCs in the intermediate results until they are k-conn + while R0: + G1 = R0.pop() + if G1.number_of_nodes() == 1: + yield G1 + else: + # Find a global minimum cut + cut_edges = nx.minimum_edge_cut(G1) + cut_value = len(cut_edges) + if cut_value < k: + # G1 is not k-edge-connected, so subdivide it + G1.remove_edges_from(cut_edges) + for cc in find_ccs(G1): + R0.add(G1.subgraph(cc).copy()) + else: + # Otherwise we found a k-edge-connected subgraph + yield G1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py new file mode 100644 index 0000000000000000000000000000000000000000..8380467623a53d2bdf73d1c544db658d2ba1eba4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcomponents.py @@ -0,0 +1,220 @@ +""" +Moody and White algorithm for k-components +""" + +from collections import defaultdict +from itertools import combinations +from operator import itemgetter + +import networkx as nx + +# Define the default maximum flow function. +from networkx.algorithms.flow import edmonds_karp +from networkx.utils import not_implemented_for + +default_flow_func = edmonds_karp + +__all__ = ["k_components"] + + +@not_implemented_for("directed") +@nx._dispatchable +def k_components(G, flow_func=None): + r"""Returns the k-component structure of a graph G. + + A `k`-component is a maximal subgraph of a graph G that has, at least, + node connectivity `k`: we need to remove at least `k` nodes to break it + into more components. `k`-components have an inherent hierarchical + structure because they are nested in terms of connectivity: a connected + graph can contain several 2-components, each of which can contain + one or more 3-components, and so forth. + + Parameters + ---------- + G : NetworkX graph + + flow_func : function + Function to perform the underlying flow computations. Default value + :meth:`edmonds_karp`. This function performs better in sparse graphs with + right tailed degree distributions. :meth:`shortest_augmenting_path` will + perform better in denser graphs. + + Returns + ------- + k_components : dict + Dictionary with all connectivity levels `k` in the input Graph as keys + and a list of sets of nodes that form a k-component of level `k` as + values. + + Raises + ------ + NetworkXNotImplemented + If the input graph is directed. + + Examples + -------- + >>> # Petersen graph has 10 nodes and it is triconnected, thus all + >>> # nodes are in a single component on all three connectivity levels + >>> G = nx.petersen_graph() + >>> k_components = nx.k_components(G) + + Notes + ----- + Moody and White [1]_ (appendix A) provide an algorithm for identifying + k-components in a graph, which is based on Kanevsky's algorithm [2]_ + for finding all minimum-size node cut-sets of a graph (implemented in + :meth:`all_node_cuts` function): + + 1. Compute node connectivity, k, of the input graph G. + + 2. Identify all k-cutsets at the current level of connectivity using + Kanevsky's algorithm. + + 3. Generate new graph components based on the removal of + these cutsets. Nodes in a cutset belong to both sides + of the induced cut. + + 4. If the graph is neither complete nor trivial, return to 1; + else end. + + This implementation also uses some heuristics (see [3]_ for details) + to speed up the computation. + + See also + -------- + node_connectivity + all_node_cuts + biconnected_components : special case of this function when k=2 + k_edge_components : similar to this function, but uses edge-connectivity + instead of node-connectivity + + References + ---------- + .. [1] Moody, J. and D. White (2003). Social cohesion and embeddedness: + A hierarchical conception of social groups. + American Sociological Review 68(1), 103--28. + http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf + + .. [2] Kanevsky, A. (1993). Finding all minimum-size separating vertex + sets in a graph. Networks 23(6), 533--541. + http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract + + .. [3] Torrents, J. and F. Ferraro (2015). Structural Cohesion: + Visualization and Heuristics for Fast Computation. + https://arxiv.org/pdf/1503.04476v1 + + """ + # Dictionary with connectivity level (k) as keys and a list of + # sets of nodes that form a k-component as values. Note that + # k-components can overlap (but only k - 1 nodes). + k_components = defaultdict(list) + # Define default flow function + if flow_func is None: + flow_func = default_flow_func + # Bicomponents as a base to check for higher order k-components + for component in nx.connected_components(G): + # isolated nodes have connectivity 0 + comp = set(component) + if len(comp) > 1: + k_components[1].append(comp) + bicomponents = [G.subgraph(c) for c in nx.biconnected_components(G)] + for bicomponent in bicomponents: + bicomp = set(bicomponent) + # avoid considering dyads as bicomponents + if len(bicomp) > 2: + k_components[2].append(bicomp) + for B in bicomponents: + if len(B) <= 2: + continue + k = nx.node_connectivity(B, flow_func=flow_func) + if k > 2: + k_components[k].append(set(B)) + # Perform cuts in a DFS like order. + cuts = list(nx.all_node_cuts(B, k=k, flow_func=flow_func)) + stack = [(k, _generate_partition(B, cuts, k))] + while stack: + (parent_k, partition) = stack[-1] + try: + nodes = next(partition) + C = B.subgraph(nodes) + this_k = nx.node_connectivity(C, flow_func=flow_func) + if this_k > parent_k and this_k > 2: + k_components[this_k].append(set(C)) + cuts = list(nx.all_node_cuts(C, k=this_k, flow_func=flow_func)) + if cuts: + stack.append((this_k, _generate_partition(C, cuts, this_k))) + except StopIteration: + stack.pop() + + # This is necessary because k-components may only be reported at their + # maximum k level. But we want to return a dictionary in which keys are + # connectivity levels and values list of sets of components, without + # skipping any connectivity level. Also, it's possible that subsets of + # an already detected k-component appear at a level k. Checking for this + # in the while loop above penalizes the common case. Thus we also have to + # _consolidate all connectivity levels in _reconstruct_k_components. + return _reconstruct_k_components(k_components) + + +def _consolidate(sets, k): + """Merge sets that share k or more elements. + + See: http://rosettacode.org/wiki/Set_consolidation + + The iterative python implementation posted there is + faster than this because of the overhead of building a + Graph and calling nx.connected_components, but it's not + clear for us if we can use it in NetworkX because there + is no licence for the code. + + """ + G = nx.Graph() + nodes = dict(enumerate(sets)) + G.add_nodes_from(nodes) + G.add_edges_from( + (u, v) for u, v in combinations(nodes, 2) if len(nodes[u] & nodes[v]) >= k + ) + for component in nx.connected_components(G): + yield set.union(*[nodes[n] for n in component]) + + +def _generate_partition(G, cuts, k): + def has_nbrs_in_partition(G, node, partition): + return any(n in partition for n in G[node]) + + components = [] + n_in_cuts = {n for cut in cuts for n in cut} + nodes = {n for n, d in G.degree() if d > k} - n_in_cuts + H = G.subgraph(nodes) + for cc in map(set, nx.connected_components(H)): + component = cc | {n for n in n_in_cuts if has_nbrs_in_partition(G, n, cc)} + if len(component) < G.order(): + components.append(component) + yield from _consolidate(components, k + 1) + + +def _reconstruct_k_components(k_comps): + result = {} + max_k = max(k_comps) if k_comps else 0 + for k in range(max_k, 0, -1): + if k == max_k: + result[k] = list(_consolidate(k_comps[k], k)) + elif k not in k_comps: + result[k] = list(_consolidate(result[k + 1], k)) + else: + nodes_at_k = set.union(*k_comps[k]) + to_add = [c for c in result[k + 1] if any(n not in nodes_at_k for n in c)] + if to_add: + result[k] = list(_consolidate(k_comps[k] + to_add, k)) + else: + result[k] = list(_consolidate(k_comps[k], k)) + return result + + +def build_k_number_dict(kcomps): + return { + node: k + for k, comps in sorted(kcomps.items(), key=itemgetter(0)) + for comp in comps + for node in comp + } diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py new file mode 100644 index 0000000000000000000000000000000000000000..de26f4c5d85f42312a811509b7a9b92cd5db952c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/kcutsets.py @@ -0,0 +1,235 @@ +""" +Kanevsky all minimum node k cutsets algorithm. +""" + +import copy +from collections import defaultdict +from itertools import combinations +from operator import itemgetter + +import networkx as nx +from networkx.algorithms.flow import ( + build_residual_network, + edmonds_karp, + shortest_augmenting_path, +) + +from .utils import build_auxiliary_node_connectivity + +default_flow_func = edmonds_karp + + +__all__ = ["all_node_cuts"] + + +@nx._dispatchable +def all_node_cuts(G, k=None, flow_func=None): + r"""Returns all minimum k cutsets of an undirected graph G. + + This implementation is based on Kanevsky's algorithm [1]_ for finding all + minimum-size node cut-sets of an undirected graph G; ie the set (or sets) + of nodes of cardinality equal to the node connectivity of G. Thus if + removed, would break G into two or more connected components. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + k : Integer + Node connectivity of the input graph. If k is None, then it is + computed. Default value: None. + + flow_func : function + Function to perform the underlying flow computations. Default value is + :func:`~networkx.algorithms.flow.edmonds_karp`. This function performs + better in sparse graphs with right tailed degree distributions. + :func:`~networkx.algorithms.flow.shortest_augmenting_path` will + perform better in denser graphs. + + + Returns + ------- + cuts : a generator of node cutsets + Each node cutset has cardinality equal to the node connectivity of + the input graph. + + Examples + -------- + >>> # A two-dimensional grid graph has 4 cutsets of cardinality 2 + >>> G = nx.grid_2d_graph(5, 5) + >>> cutsets = list(nx.all_node_cuts(G)) + >>> len(cutsets) + 4 + >>> all(2 == len(cutset) for cutset in cutsets) + True + >>> nx.node_connectivity(G) + 2 + + Notes + ----- + This implementation is based on the sequential algorithm for finding all + minimum-size separating vertex sets in a graph [1]_. The main idea is to + compute minimum cuts using local maximum flow computations among a set + of nodes of highest degree and all other non-adjacent nodes in the Graph. + Once we find a minimum cut, we add an edge between the high degree + node and the target node of the local maximum flow computation to make + sure that we will not find that minimum cut again. + + See also + -------- + node_connectivity + edmonds_karp + shortest_augmenting_path + + References + ---------- + .. [1] Kanevsky, A. (1993). Finding all minimum-size separating vertex + sets in a graph. Networks 23(6), 533--541. + http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract + + """ + if not nx.is_connected(G): + raise nx.NetworkXError("Input graph is disconnected.") + + # Address some corner cases first. + # For complete Graphs + + if nx.density(G) == 1: + yield from () + return + + # Initialize data structures. + # Keep track of the cuts already computed so we do not repeat them. + seen = [] + # Even-Tarjan reduction is what we call auxiliary digraph + # for node connectivity. + H = build_auxiliary_node_connectivity(G) + H_nodes = H.nodes # for speed + mapping = H.graph["mapping"] + # Keep a copy of original predecessors, H will be modified later. + # Shallow copy is enough. + original_H_pred = copy.copy(H._pred) + R = build_residual_network(H, "capacity") + kwargs = {"capacity": "capacity", "residual": R} + # Define default flow function + if flow_func is None: + flow_func = default_flow_func + if flow_func is shortest_augmenting_path: + kwargs["two_phase"] = True + # Begin the actual algorithm + # step 1: Find node connectivity k of G + if k is None: + k = nx.node_connectivity(G, flow_func=flow_func) + # step 2: + # Find k nodes with top degree, call it X: + X = {n for n, d in sorted(G.degree(), key=itemgetter(1), reverse=True)[:k]} + # Check if X is a k-node-cutset + if _is_separating_set(G, X): + seen.append(X) + yield X + + for x in X: + # step 3: Compute local connectivity flow of x with all other + # non adjacent nodes in G + non_adjacent = set(G) - {x} - set(G[x]) + for v in non_adjacent: + # step 4: compute maximum flow in an Even-Tarjan reduction H of G + # and step 5: build the associated residual network R + R = flow_func(H, f"{mapping[x]}B", f"{mapping[v]}A", **kwargs) + flow_value = R.graph["flow_value"] + + if flow_value == k: + # Find the nodes incident to the flow. + E1 = flowed_edges = [ + (u, w) for (u, w, d) in R.edges(data=True) if d["flow"] != 0 + ] + VE1 = incident_nodes = {n for edge in E1 for n in edge} + # Remove saturated edges form the residual network. + # Note that reversed edges are introduced with capacity 0 + # in the residual graph and they need to be removed too. + saturated_edges = [ + (u, w, d) + for (u, w, d) in R.edges(data=True) + if d["capacity"] == d["flow"] or d["capacity"] == 0 + ] + R.remove_edges_from(saturated_edges) + R_closure = nx.transitive_closure(R) + # step 6: shrink the strongly connected components of + # residual flow network R and call it L. + L = nx.condensation(R) + cmap = L.graph["mapping"] + inv_cmap = defaultdict(list) + for n, scc in cmap.items(): + inv_cmap[scc].append(n) + # Find the incident nodes in the condensed graph. + VE1 = {cmap[n] for n in VE1} + # step 7: Compute all antichains of L; + # they map to closed sets in H. + # Any edge in H that links a closed set is part of a cutset. + for antichain in nx.antichains(L): + # Only antichains that are subsets of incident nodes counts. + # Lemma 8 in reference. + if not set(antichain).issubset(VE1): + continue + # Nodes in an antichain of the condensation graph of + # the residual network map to a closed set of nodes that + # define a node partition of the auxiliary digraph H + # through taking all of antichain's predecessors in the + # transitive closure. + S = set() + for scc in antichain: + S.update(inv_cmap[scc]) + S_ancestors = set() + for n in S: + S_ancestors.update(R_closure._pred[n]) + S.update(S_ancestors) + if f"{mapping[x]}B" not in S or f"{mapping[v]}A" in S: + continue + # Find the cutset that links the node partition (S,~S) in H + cutset = set() + for u in S: + cutset.update((u, w) for w in original_H_pred[u] if w not in S) + # The edges in H that form the cutset are internal edges + # (ie edges that represent a node of the original graph G) + if any(H_nodes[u]["id"] != H_nodes[w]["id"] for u, w in cutset): + continue + node_cut = {H_nodes[u]["id"] for u, _ in cutset} + + if len(node_cut) == k: + # The cut is invalid if it includes internal edges of + # end nodes. The other half of Lemma 8 in ref. + if x in node_cut or v in node_cut: + continue + if node_cut not in seen: + yield node_cut + seen.append(node_cut) + + # Add an edge (x, v) to make sure that we do not + # find this cutset again. This is equivalent + # of adding the edge in the input graph + # G.add_edge(x, v) and then regenerate H and R: + # Add edges to the auxiliary digraph. + # See build_residual_network for convention we used + # in residual graphs. + H.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1) + H.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1) + # Add edges to the residual network. + R.add_edge(f"{mapping[x]}B", f"{mapping[v]}A", capacity=1) + R.add_edge(f"{mapping[v]}A", f"{mapping[x]}B", capacity=0) + R.add_edge(f"{mapping[v]}B", f"{mapping[x]}A", capacity=1) + R.add_edge(f"{mapping[x]}A", f"{mapping[v]}B", capacity=0) + + # Add again the saturated edges to reuse the residual network + R.add_edges_from(saturated_edges) + + +def _is_separating_set(G, cut): + """Assumes that the input graph is connected""" + if len(cut) == len(G) - 1: + return True + + H = nx.restricted_view(G, cut, []) + if nx.is_connected(H): + return False + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py new file mode 100644 index 0000000000000000000000000000000000000000..29604b148303703c73ad37baffec043abd4333e9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/stoerwagner.py @@ -0,0 +1,152 @@ +""" +Stoer-Wagner minimum cut algorithm. +""" + +from itertools import islice + +import networkx as nx + +from ...utils import BinaryHeap, arbitrary_element, not_implemented_for + +__all__ = ["stoer_wagner"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def stoer_wagner(G, weight="weight", heap=BinaryHeap): + r"""Returns the weighted minimum edge cut using the Stoer-Wagner algorithm. + + Determine the minimum edge cut of a connected graph using the + Stoer-Wagner algorithm. In weighted cases, all weights must be + nonnegative. + + The running time of the algorithm depends on the type of heaps used: + + ============== ============================================= + Type of heap Running time + ============== ============================================= + Binary heap $O(n (m + n) \log n)$ + Fibonacci heap $O(nm + n^2 \log n)$ + Pairing heap $O(2^{2 \sqrt{\log \log n}} nm + n^2 \log n)$ + ============== ============================================= + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute named by the + weight parameter below. If this attribute is not present, the edge is + considered to have unit weight. + + weight : string + Name of the weight attribute of the edges. If the attribute is not + present, unit weight is assumed. Default value: 'weight'. + + heap : class + Type of heap to be used in the algorithm. It should be a subclass of + :class:`MinHeap` or implement a compatible interface. + + If a stock heap implementation is to be used, :class:`BinaryHeap` is + recommended over :class:`PairingHeap` for Python implementations without + optimized attribute accesses (e.g., CPython) despite a slower + asymptotic running time. For Python implementations with optimized + attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better + performance. Default value: :class:`BinaryHeap`. + + Returns + ------- + cut_value : integer or float + The sum of weights of edges in a minimum cut. + + partition : pair of node lists + A partitioning of the nodes that defines a minimum cut. + + Raises + ------ + NetworkXNotImplemented + If the graph is directed or a multigraph. + + NetworkXError + If the graph has less than two nodes, is not connected or has a + negative-weighted edge. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edge("x", "a", weight=3) + >>> G.add_edge("x", "b", weight=1) + >>> G.add_edge("a", "c", weight=3) + >>> G.add_edge("b", "c", weight=5) + >>> G.add_edge("b", "d", weight=4) + >>> G.add_edge("d", "e", weight=2) + >>> G.add_edge("c", "y", weight=2) + >>> G.add_edge("e", "y", weight=3) + >>> cut_value, partition = nx.stoer_wagner(G) + >>> cut_value + 4 + """ + n = len(G) + if n < 2: + raise nx.NetworkXError("graph has less than two nodes.") + if not nx.is_connected(G): + raise nx.NetworkXError("graph is not connected.") + + # Make a copy of the graph for internal use. + G = nx.Graph( + (u, v, {"weight": e.get(weight, 1)}) for u, v, e in G.edges(data=True) if u != v + ) + G.__networkx_cache__ = None # Disable caching + + for u, v, e in G.edges(data=True): + if e["weight"] < 0: + raise nx.NetworkXError("graph has a negative-weighted edge.") + + cut_value = float("inf") + nodes = set(G) + contractions = [] # contracted node pairs + + # Repeatedly pick a pair of nodes to contract until only one node is left. + for i in range(n - 1): + # Pick an arbitrary node u and create a set A = {u}. + u = arbitrary_element(G) + A = {u} + # Repeatedly pick the node "most tightly connected" to A and add it to + # A. The tightness of connectivity of a node not in A is defined by the + # of edges connecting it to nodes in A. + h = heap() # min-heap emulating a max-heap + for v, e in G[u].items(): + h.insert(v, -e["weight"]) + # Repeat until all but one node has been added to A. + for j in range(n - i - 2): + u = h.pop()[0] + A.add(u) + for v, e in G[u].items(): + if v not in A: + h.insert(v, h.get(v, 0) - e["weight"]) + # A and the remaining node v define a "cut of the phase". There is a + # minimum cut of the original graph that is also a cut of the phase. + # Due to contractions in earlier phases, v may in fact represent + # multiple nodes in the original graph. + v, w = h.min() + w = -w + if w < cut_value: + cut_value = w + best_phase = i + # Contract v and the last node added to A. + contractions.append((u, v)) + for w, e in G[v].items(): + if w != u: + if w not in G[u]: + G.add_edge(u, w, weight=e["weight"]) + else: + G[u][w]["weight"] += e["weight"] + G.remove_node(v) + + # Recover the optimal partitioning from the contractions. + G = nx.Graph(islice(contractions, best_phase)) + v = contractions[best_phase][1] + G.add_node(v) + reachable = set(nx.single_source_shortest_path_length(G, v)) + partition = (list(reachable), list(nodes - reachable)) + + return cut_value, partition diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7bf9994598981e528f30e0deb15413c35f3dadbe --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/connectivity/utils.py @@ -0,0 +1,88 @@ +""" +Utilities for connectivity package +""" + +import networkx as nx + +__all__ = ["build_auxiliary_node_connectivity", "build_auxiliary_edge_connectivity"] + + +@nx._dispatchable(returns_graph=True) +def build_auxiliary_node_connectivity(G): + r"""Creates a directed graph D from an undirected graph G to compute flow + based node connectivity. + + For an undirected graph G having `n` nodes and `m` edges we derive a + directed graph D with `2n` nodes and `2m+n` arcs by replacing each + original node `v` with two nodes `vA`, `vB` linked by an (internal) + arc in D. Then for each edge (`u`, `v`) in G we add two arcs (`uB`, `vA`) + and (`vB`, `uA`) in D. Finally we set the attribute capacity = 1 for each + arc in D [1]_. + + For a directed graph having `n` nodes and `m` arcs we derive a + directed graph D with `2n` nodes and `m+n` arcs by replacing each + original node `v` with two nodes `vA`, `vB` linked by an (internal) + arc (`vA`, `vB`) in D. Then for each arc (`u`, `v`) in G we add one + arc (`uB`, `vA`) in D. Finally we set the attribute capacity = 1 for + each arc in D. + + A dictionary with a mapping between nodes in the original graph and the + auxiliary digraph is stored as a graph attribute: D.graph['mapping']. + + References + ---------- + .. [1] Kammer, Frank and Hanjo Taubig. Graph Connectivity. in Brandes and + Erlebach, 'Network Analysis: Methodological Foundations', Lecture + Notes in Computer Science, Volume 3418, Springer-Verlag, 2005. + https://doi.org/10.1007/978-3-540-31955-9_7 + + """ + directed = G.is_directed() + + mapping = {} + H = nx.DiGraph() + + for i, node in enumerate(G): + mapping[node] = i + H.add_node(f"{i}A", id=node) + H.add_node(f"{i}B", id=node) + H.add_edge(f"{i}A", f"{i}B", capacity=1) + + edges = [] + for source, target in G.edges(): + edges.append((f"{mapping[source]}B", f"{mapping[target]}A")) + if not directed: + edges.append((f"{mapping[target]}B", f"{mapping[source]}A")) + H.add_edges_from(edges, capacity=1) + + # Store mapping as graph attribute + H.graph["mapping"] = mapping + return H + + +@nx._dispatchable(returns_graph=True) +def build_auxiliary_edge_connectivity(G): + """Auxiliary digraph for computing flow based edge connectivity + + If the input graph is undirected, we replace each edge (`u`,`v`) with + two reciprocal arcs (`u`, `v`) and (`v`, `u`) and then we set the attribute + 'capacity' for each arc to 1. If the input graph is directed we simply + add the 'capacity' attribute. Part of algorithm 1 in [1]_ . + + References + ---------- + .. [1] Abdol-Hossein Esfahanian. Connectivity Algorithms. (this is a + chapter, look for the reference of the book). + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + """ + if G.is_directed(): + H = nx.DiGraph() + H.add_nodes_from(G.nodes()) + H.add_edges_from(G.edges(), capacity=1) + return H + else: + H = nx.DiGraph() + H.add_nodes_from(G.nodes()) + for source, target in G.edges(): + H.add_edges_from([(source, target), (target, source)], capacity=1) + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/core.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/core.py new file mode 100644 index 0000000000000000000000000000000000000000..fec26ec984161eb2d927019d90540690cbc5aa45 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/core.py @@ -0,0 +1,588 @@ +""" +Find the k-cores of a graph. + +The k-core is found by recursively pruning nodes with degrees less than k. + +See the following references for details: + +An O(m) Algorithm for Cores Decomposition of Networks +Vladimir Batagelj and Matjaz Zaversnik, 2003. +https://arxiv.org/abs/cs.DS/0310049 + +Generalized Cores +Vladimir Batagelj and Matjaz Zaversnik, 2002. +https://arxiv.org/pdf/cs/0202039 + +For directed graphs a more general notion is that of D-cores which +looks at (k, l) restrictions on (in, out) degree. The (k, k) D-core +is the k-core. + +D-cores: Measuring Collaboration of Directed Graphs Based on Degeneracy +Christos Giatsidis, Dimitrios M. Thilikos, Michalis Vazirgiannis, ICDM 2011. +http://www.graphdegeneracy.org/dcores_ICDM_2011.pdf + +Multi-scale structure and topological anomaly detection via a new network \ +statistic: The onion decomposition +L. Hébert-Dufresne, J. A. Grochow, and A. Allard +Scientific Reports 6, 31708 (2016) +http://doi.org/10.1038/srep31708 + +""" + +import networkx as nx + +__all__ = [ + "core_number", + "k_core", + "k_shell", + "k_crust", + "k_corona", + "k_truss", + "onion_layers", +] + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable +def core_number(G): + """Returns the core number for each node. + + A k-core is a maximal subgraph that contains nodes of degree k or more. + + The core number of a node is the largest value k of a k-core containing + that node. + + Parameters + ---------- + G : NetworkX graph + An undirected or directed graph + + Returns + ------- + core_number : dictionary + A dictionary keyed by node to the core number. + + Raises + ------ + NetworkXNotImplemented + If `G` is a multigraph or contains self loops. + + Notes + ----- + For directed graphs the node degree is defined to be the + in-degree + out-degree. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> nx.core_number(H) + {0: 1, 1: 2, 2: 2, 3: 2, 4: 1, 5: 2, 6: 0} + >>> G = nx.DiGraph() + >>> G.add_edges_from([(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)]) + >>> nx.core_number(G) + {1: 2, 2: 2, 3: 2, 4: 2} + + References + ---------- + .. [1] An O(m) Algorithm for Cores Decomposition of Networks + Vladimir Batagelj and Matjaz Zaversnik, 2003. + https://arxiv.org/abs/cs.DS/0310049 + """ + if nx.number_of_selfloops(G) > 0: + msg = ( + "Input graph has self loops which is not permitted; " + "Consider using G.remove_edges_from(nx.selfloop_edges(G))." + ) + raise nx.NetworkXNotImplemented(msg) + degrees = dict(G.degree()) + # Sort nodes by degree. + nodes = sorted(degrees, key=degrees.get) + bin_boundaries = [0] + curr_degree = 0 + for i, v in enumerate(nodes): + if degrees[v] > curr_degree: + bin_boundaries.extend([i] * (degrees[v] - curr_degree)) + curr_degree = degrees[v] + node_pos = {v: pos for pos, v in enumerate(nodes)} + # The initial guess for the core number of a node is its degree. + core = degrees + nbrs = {v: list(nx.all_neighbors(G, v)) for v in G} + for v in nodes: + for u in nbrs[v]: + if core[u] > core[v]: + nbrs[u].remove(v) + pos = node_pos[u] + bin_start = bin_boundaries[core[u]] + node_pos[u] = bin_start + node_pos[nodes[bin_start]] = pos + nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start] + bin_boundaries[core[u]] += 1 + core[u] -= 1 + return core + + +def _core_subgraph(G, k_filter, k=None, core=None): + """Returns the subgraph induced by nodes passing filter `k_filter`. + + Parameters + ---------- + G : NetworkX graph + The graph or directed graph to process + k_filter : filter function + This function filters the nodes chosen. It takes three inputs: + A node of G, the filter's cutoff, and the core dict of the graph. + The function should return a Boolean value. + k : int, optional + The order of the core. If not specified use the max core number. + This value is used as the cutoff for the filter. + core : dict, optional + Precomputed core numbers keyed by node for the graph `G`. + If not specified, the core numbers will be computed from `G`. + + """ + if core is None: + core = core_number(G) + if k is None: + k = max(core.values()) + nodes = (v for v in core if k_filter(v, k, core)) + return G.subgraph(nodes).copy() + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def k_core(G, k=None, core_number=None): + """Returns the k-core of G. + + A k-core is a maximal subgraph that contains nodes of degree `k` or more. + + Parameters + ---------- + G : NetworkX graph + A graph or directed graph + k : int, optional + The order of the core. If not specified return the main core. + core_number : dictionary, optional + Precomputed core numbers for the graph G. + + Returns + ------- + G : NetworkX graph + The k-core subgraph + + Raises + ------ + NetworkXNotImplemented + The k-core is not defined for multigraphs or graphs with self loops. + + Notes + ----- + The main core is the core with `k` as the largest core_number. + + For directed graphs the node degree is defined to be the + in-degree + out-degree. + + Graph, node, and edge attributes are copied to the subgraph. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.k_core(H).nodes + NodeView((1, 2, 3, 5)) + + See Also + -------- + core_number + + References + ---------- + .. [1] An O(m) Algorithm for Cores Decomposition of Networks + Vladimir Batagelj and Matjaz Zaversnik, 2003. + https://arxiv.org/abs/cs.DS/0310049 + """ + + def k_filter(v, k, c): + return c[v] >= k + + return _core_subgraph(G, k_filter, k, core_number) + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def k_shell(G, k=None, core_number=None): + """Returns the k-shell of G. + + The k-shell is the subgraph induced by nodes with core number k. + That is, nodes in the k-core that are not in the (k+1)-core. + + Parameters + ---------- + G : NetworkX graph + A graph or directed graph. + k : int, optional + The order of the shell. If not specified return the outer shell. + core_number : dictionary, optional + Precomputed core numbers for the graph G. + + + Returns + ------- + G : NetworkX graph + The k-shell subgraph + + Raises + ------ + NetworkXNotImplemented + The k-shell is not implemented for multigraphs or graphs with self loops. + + Notes + ----- + This is similar to k_corona but in that case only neighbors in the + k-core are considered. + + For directed graphs the node degree is defined to be the + in-degree + out-degree. + + Graph, node, and edge attributes are copied to the subgraph. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.k_shell(H, k=1).nodes + NodeView((0, 4)) + + See Also + -------- + core_number + k_corona + + + References + ---------- + .. [1] A model of Internet topology using k-shell decomposition + Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt, + and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154 + http://www.pnas.org/content/104/27/11150.full + """ + + def k_filter(v, k, c): + return c[v] == k + + return _core_subgraph(G, k_filter, k, core_number) + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def k_crust(G, k=None, core_number=None): + """Returns the k-crust of G. + + The k-crust is the graph G with the edges of the k-core removed + and isolated nodes found after the removal of edges are also removed. + + Parameters + ---------- + G : NetworkX graph + A graph or directed graph. + k : int, optional + The order of the shell. If not specified return the main crust. + core_number : dictionary, optional + Precomputed core numbers for the graph G. + + Returns + ------- + G : NetworkX graph + The k-crust subgraph + + Raises + ------ + NetworkXNotImplemented + The k-crust is not implemented for multigraphs or graphs with self loops. + + Notes + ----- + This definition of k-crust is different than the definition in [1]_. + The k-crust in [1]_ is equivalent to the k+1 crust of this algorithm. + + For directed graphs the node degree is defined to be the + in-degree + out-degree. + + Graph, node, and edge attributes are copied to the subgraph. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.k_crust(H, k=1).nodes + NodeView((0, 4, 6)) + + See Also + -------- + core_number + + References + ---------- + .. [1] A model of Internet topology using k-shell decomposition + Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt, + and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154 + http://www.pnas.org/content/104/27/11150.full + """ + # Default for k is one less than in _core_subgraph, so just inline. + # Filter is c[v] <= k + if core_number is None: + core_number = nx.core_number(G) + if k is None: + k = max(core_number.values()) - 1 + nodes = (v for v in core_number if core_number[v] <= k) + return G.subgraph(nodes).copy() + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def k_corona(G, k, core_number=None): + """Returns the k-corona of G. + + The k-corona is the subgraph of nodes in the k-core which have + exactly k neighbors in the k-core. + + Parameters + ---------- + G : NetworkX graph + A graph or directed graph + k : int + The order of the corona. + core_number : dictionary, optional + Precomputed core numbers for the graph G. + + Returns + ------- + G : NetworkX graph + The k-corona subgraph + + Raises + ------ + NetworkXNotImplemented + The k-corona is not defined for multigraphs or graphs with self loops. + + Notes + ----- + For directed graphs the node degree is defined to be the + in-degree + out-degree. + + Graph, node, and edge attributes are copied to the subgraph. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.k_corona(H, k=2).nodes + NodeView((1, 2, 3, 5)) + + See Also + -------- + core_number + + References + ---------- + .. [1] k -core (bootstrap) percolation on complex networks: + Critical phenomena and nonlocal effects, + A. V. Goltsev, S. N. Dorogovtsev, and J. F. F. Mendes, + Phys. Rev. E 73, 056101 (2006) + http://link.aps.org/doi/10.1103/PhysRevE.73.056101 + """ + + def func(v, k, c): + return c[v] == k and k == sum(1 for w in G[v] if c[w] >= k) + + return _core_subgraph(G, func, k, core_number) + + +@nx.utils.not_implemented_for("directed") +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def k_truss(G, k): + """Returns the k-truss of `G`. + + The k-truss is the maximal induced subgraph of `G` which contains at least + three vertices where every edge is incident to at least `k-2` triangles. + + Parameters + ---------- + G : NetworkX graph + An undirected graph + k : int + The order of the truss + + Returns + ------- + H : NetworkX graph + The k-truss subgraph + + Raises + ------ + NetworkXNotImplemented + If `G` is a multigraph or directed graph or if it contains self loops. + + Notes + ----- + A k-clique is a (k-2)-truss and a k-truss is a (k+1)-core. + + Graph, node, and edge attributes are copied to the subgraph. + + K-trusses were originally defined in [2] which states that the k-truss + is the maximal induced subgraph where each edge belongs to at least + `k-2` triangles. A more recent paper, [1], uses a slightly different + definition requiring that each edge belong to at least `k` triangles. + This implementation uses the original definition of `k-2` triangles. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.k_truss(H, k=2).nodes + NodeView((0, 1, 2, 3, 4, 5)) + + References + ---------- + .. [1] Bounds and Algorithms for k-truss. Paul Burkhardt, Vance Faber, + David G. Harris, 2018. https://arxiv.org/abs/1806.05523v2 + .. [2] Trusses: Cohesive Subgraphs for Social Network Analysis. Jonathan + Cohen, 2005. + """ + if nx.number_of_selfloops(G) > 0: + msg = ( + "Input graph has self loops which is not permitted; " + "Consider using G.remove_edges_from(nx.selfloop_edges(G))." + ) + raise nx.NetworkXNotImplemented(msg) + + H = G.copy() + + n_dropped = 1 + while n_dropped > 0: + n_dropped = 0 + to_drop = [] + seen = set() + for u in H: + nbrs_u = set(H[u]) + seen.add(u) + new_nbrs = [v for v in nbrs_u if v not in seen] + for v in new_nbrs: + if len(nbrs_u & set(H[v])) < (k - 2): + to_drop.append((u, v)) + H.remove_edges_from(to_drop) + n_dropped = len(to_drop) + H.remove_nodes_from(list(nx.isolates(H))) + + return H + + +@nx.utils.not_implemented_for("multigraph") +@nx.utils.not_implemented_for("directed") +@nx._dispatchable +def onion_layers(G): + """Returns the layer of each vertex in an onion decomposition of the graph. + + The onion decomposition refines the k-core decomposition by providing + information on the internal organization of each k-shell. It is usually + used alongside the `core numbers`. + + Parameters + ---------- + G : NetworkX graph + An undirected graph without self loops. + + Returns + ------- + od_layers : dictionary + A dictionary keyed by node to the onion layer. The layers are + contiguous integers starting at 1. + + Raises + ------ + NetworkXNotImplemented + If `G` is a multigraph or directed graph or if it contains self loops. + + Examples + -------- + >>> degrees = [0, 1, 2, 2, 2, 2, 3] + >>> H = nx.havel_hakimi_graph(degrees) + >>> H.degree + DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0}) + >>> nx.onion_layers(H) + {6: 1, 0: 2, 4: 3, 1: 4, 2: 4, 3: 4, 5: 4} + + See Also + -------- + core_number + + References + ---------- + .. [1] Multi-scale structure and topological anomaly detection via a new + network statistic: The onion decomposition + L. Hébert-Dufresne, J. A. Grochow, and A. Allard + Scientific Reports 6, 31708 (2016) + http://doi.org/10.1038/srep31708 + .. [2] Percolation and the effective structure of complex networks + A. Allard and L. Hébert-Dufresne + Physical Review X 9, 011023 (2019) + http://doi.org/10.1103/PhysRevX.9.011023 + """ + if nx.number_of_selfloops(G) > 0: + msg = ( + "Input graph contains self loops which is not permitted; " + "Consider using G.remove_edges_from(nx.selfloop_edges(G))." + ) + raise nx.NetworkXNotImplemented(msg) + # Dictionaries to register the k-core/onion decompositions. + od_layers = {} + # Adjacency list + neighbors = {v: list(nx.all_neighbors(G, v)) for v in G} + # Effective degree of nodes. + degrees = dict(G.degree()) + # Performs the onion decomposition. + current_core = 1 + current_layer = 1 + # Sets vertices of degree 0 to layer 1, if any. + isolated_nodes = list(nx.isolates(G)) + if len(isolated_nodes) > 0: + for v in isolated_nodes: + od_layers[v] = current_layer + degrees.pop(v) + current_layer = 2 + # Finds the layer for the remaining nodes. + while len(degrees) > 0: + # Sets the order for looking at nodes. + nodes = sorted(degrees, key=degrees.get) + # Sets properly the current core. + min_degree = degrees[nodes[0]] + if min_degree > current_core: + current_core = min_degree + # Identifies vertices in the current layer. + this_layer = [] + for n in nodes: + if degrees[n] > current_core: + break + this_layer.append(n) + # Identifies the core/layer of the vertices in the current layer. + for v in this_layer: + od_layers[v] = current_layer + for n in neighbors[v]: + neighbors[n].remove(v) + degrees[n] = degrees[n] - 1 + degrees.pop(v) + # Updates the layer count. + current_layer = current_layer + 1 + # Returns the dictionaries containing the onion layer of each vertices. + return od_layers diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/covering.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/covering.py new file mode 100644 index 0000000000000000000000000000000000000000..cdf607b3d798a9d2efeabb0bb95721d6370ca8cd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/covering.py @@ -0,0 +1,142 @@ +"""Functions related to graph covers.""" + +from functools import partial +from itertools import chain + +import networkx as nx +from networkx.utils import arbitrary_element, not_implemented_for + +__all__ = ["min_edge_cover", "is_edge_cover"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def min_edge_cover(G, matching_algorithm=None): + """Returns the min cardinality edge cover of the graph as a set of edges. + + A smallest edge cover can be found in polynomial time by finding + a maximum matching and extending it greedily so that all nodes + are covered. This function follows that process. A maximum matching + algorithm can be specified for the first step of the algorithm. + The resulting set may return a set with one 2-tuple for each edge, + (the usual case) or with both 2-tuples `(u, v)` and `(v, u)` for + each edge. The latter is only done when a bipartite matching algorithm + is specified as `matching_algorithm`. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + matching_algorithm : function + A function that returns a maximum cardinality matching for `G`. + The function must take one input, the graph `G`, and return + either a set of edges (with only one direction for the pair of nodes) + or a dictionary mapping each node to its mate. If not specified, + :func:`~networkx.algorithms.matching.max_weight_matching` is used. + Common bipartite matching functions include + :func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching` + or + :func:`~networkx.algorithms.bipartite.matching.eppstein_matching`. + + Returns + ------- + min_cover : set + + A set of the edges in a minimum edge cover in the form of tuples. + It contains only one of the equivalent 2-tuples `(u, v)` and `(v, u)` + for each edge. If a bipartite method is used to compute the matching, + the returned set contains both the 2-tuples `(u, v)` and `(v, u)` + for each edge of a minimum edge cover. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]) + >>> sorted(nx.min_edge_cover(G)) + [(2, 1), (3, 0)] + + Notes + ----- + An edge cover of a graph is a set of edges such that every node of + the graph is incident to at least one edge of the set. + The minimum edge cover is an edge covering of smallest cardinality. + + Due to its implementation, the worst-case running time of this algorithm + is bounded by the worst-case running time of the function + ``matching_algorithm``. + + Minimum edge cover for `G` can also be found using + :func:`~networkx.algorithms.bipartite.covering.min_edge_covering` which is + simply this function with a default matching algorithm of + :func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching` + """ + if len(G) == 0: + return set() + if nx.number_of_isolates(G) > 0: + # ``min_cover`` does not exist as there is an isolated node + raise nx.NetworkXException( + "Graph has a node with no edge incident on it, so no edge cover exists." + ) + if matching_algorithm is None: + matching_algorithm = partial(nx.max_weight_matching, maxcardinality=True) + maximum_matching = matching_algorithm(G) + # ``min_cover`` is superset of ``maximum_matching`` + try: + # bipartite matching algs return dict so convert if needed + min_cover = set(maximum_matching.items()) + bipartite_cover = True + except AttributeError: + min_cover = maximum_matching + bipartite_cover = False + # iterate for uncovered nodes + uncovered_nodes = set(G) - {v for u, v in min_cover} - {u for u, v in min_cover} + for v in uncovered_nodes: + # Since `v` is uncovered, each edge incident to `v` will join it + # with a covered node (otherwise, if there were an edge joining + # uncovered nodes `u` and `v`, the maximum matching algorithm + # would have found it), so we can choose an arbitrary edge + # incident to `v`. (This applies only in a simple graph, not a + # multigraph.) + u = arbitrary_element(G[v]) + min_cover.add((u, v)) + if bipartite_cover: + min_cover.add((v, u)) + return min_cover + + +@not_implemented_for("directed") +@nx._dispatchable +def is_edge_cover(G, cover): + """Decides whether a set of edges is a valid edge cover of the graph. + + Given a set of edges, whether it is an edge covering can + be decided if we just check whether all nodes of the graph + has an edge from the set, incident on it. + + Parameters + ---------- + G : NetworkX graph + An undirected bipartite graph. + + cover : set + Set of edges to be checked. + + Returns + ------- + bool + Whether the set of edges is a valid edge cover of the graph. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]) + >>> cover = {(2, 1), (3, 0)} + >>> nx.is_edge_cover(G, cover) + True + + Notes + ----- + An edge cover of a graph is a set of edges such that every node of + the graph is incident to at least one edge of the set. + """ + return set(G) <= set(chain.from_iterable(cover)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cuts.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cuts.py new file mode 100644 index 0000000000000000000000000000000000000000..040f4d49de1cb544410660ec0e4ca758fbd5975b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cuts.py @@ -0,0 +1,416 @@ +"""Functions for finding and evaluating cuts in a graph.""" + +from itertools import chain + +import networkx as nx + +__all__ = [ + "boundary_expansion", + "conductance", + "cut_size", + "edge_expansion", + "mixing_expansion", + "node_expansion", + "normalized_cut_size", + "volume", +] + + +# TODO STILL NEED TO UPDATE ALL THE DOCUMENTATION! + + +@nx._dispatchable(edge_attrs="weight") +def cut_size(G, S, T=None, weight=None): + """Returns the size of the cut between two sets of nodes. + + A *cut* is a partition of the nodes of a graph into two sets. The + *cut size* is the sum of the weights of the edges "between" the two + sets of nodes. + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + T : collection + A collection of nodes in `G`. If not specified, this is taken to + be the set complement of `S`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + Total weight of all edges from nodes in set `S` to nodes in + set `T` (and, in the case of directed graphs, all edges from + nodes in `T` to nodes in `S`). + + Examples + -------- + In the graph with two cliques joined by a single edges, the natural + bipartition of the graph into two blocks, one for each clique, + yields a cut of weight one: + + >>> G = nx.barbell_graph(3, 0) + >>> S = {0, 1, 2} + >>> T = {3, 4, 5} + >>> nx.cut_size(G, S, T) + 1 + + Each parallel edge in a multigraph is counted when determining the + cut size: + + >>> G = nx.MultiGraph(["ab", "ab"]) + >>> S = {"a"} + >>> T = {"b"} + >>> nx.cut_size(G, S, T) + 2 + + Notes + ----- + In a multigraph, the cut size is the total weight of edges including + multiplicity. + + """ + edges = nx.edge_boundary(G, S, T, data=weight, default=1) + if G.is_directed(): + edges = chain(edges, nx.edge_boundary(G, T, S, data=weight, default=1)) + return sum(weight for u, v, weight in edges) + + +@nx._dispatchable(edge_attrs="weight") +def volume(G, S, weight=None): + """Returns the volume of a set of nodes. + + The *volume* of a set *S* is the sum of the (out-)degrees of nodes + in *S* (taking into account parallel edges in multigraphs). [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + The volume of the set of nodes represented by `S` in the graph + `G`. + + See also + -------- + conductance + cut_size + edge_expansion + edge_boundary + normalized_cut_size + + References + ---------- + .. [1] David Gleich. + *Hierarchical Directed Spectral Graph Partitioning*. + + + """ + degree = G.out_degree if G.is_directed() else G.degree + return sum(d for v, d in degree(S, weight=weight)) + + +@nx._dispatchable(edge_attrs="weight") +def normalized_cut_size(G, S, T=None, weight=None): + """Returns the normalized size of the cut between two sets of nodes. + + The *normalized cut size* is the cut size times the sum of the + reciprocal sizes of the volumes of the two sets. [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + T : collection + A collection of nodes in `G`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + The normalized cut size between the two sets `S` and `T`. + + Notes + ----- + In a multigraph, the cut size is the total weight of edges including + multiplicity. + + See also + -------- + conductance + cut_size + edge_expansion + volume + + References + ---------- + .. [1] David Gleich. + *Hierarchical Directed Spectral Graph Partitioning*. + + + """ + if T is None: + T = set(G) - set(S) + num_cut_edges = cut_size(G, S, T=T, weight=weight) + volume_S = volume(G, S, weight=weight) + volume_T = volume(G, T, weight=weight) + return num_cut_edges * ((1 / volume_S) + (1 / volume_T)) + + +@nx._dispatchable(edge_attrs="weight") +def conductance(G, S, T=None, weight=None): + """Returns the conductance of two sets of nodes. + + The *conductance* is the quotient of the cut size and the smaller of + the volumes of the two sets. [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + T : collection + A collection of nodes in `G`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + The conductance between the two sets `S` and `T`. + + See also + -------- + cut_size + edge_expansion + normalized_cut_size + volume + + References + ---------- + .. [1] David Gleich. + *Hierarchical Directed Spectral Graph Partitioning*. + + + """ + if T is None: + T = set(G) - set(S) + num_cut_edges = cut_size(G, S, T, weight=weight) + volume_S = volume(G, S, weight=weight) + volume_T = volume(G, T, weight=weight) + return num_cut_edges / min(volume_S, volume_T) + + +@nx._dispatchable(edge_attrs="weight") +def edge_expansion(G, S, T=None, weight=None): + """Returns the edge expansion between two node sets. + + The *edge expansion* is the quotient of the cut size and the smaller + of the cardinalities of the two sets. [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + T : collection + A collection of nodes in `G`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + The edge expansion between the two sets `S` and `T`. + + See also + -------- + boundary_expansion + mixing_expansion + node_expansion + + References + ---------- + .. [1] Fan Chung. + *Spectral Graph Theory*. + (CBMS Regional Conference Series in Mathematics, No. 92), + American Mathematical Society, 1997, ISBN 0-8218-0315-8 + + + """ + if T is None: + T = set(G) - set(S) + num_cut_edges = cut_size(G, S, T=T, weight=weight) + return num_cut_edges / min(len(S), len(T)) + + +@nx._dispatchable(edge_attrs="weight") +def mixing_expansion(G, S, T=None, weight=None): + """Returns the mixing expansion between two node sets. + + The *mixing expansion* is the quotient of the cut size and twice the + number of edges in the graph. [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + T : collection + A collection of nodes in `G`. + + weight : object + Edge attribute key to use as weight. If not specified, edges + have weight one. + + Returns + ------- + number + The mixing expansion between the two sets `S` and `T`. + + See also + -------- + boundary_expansion + edge_expansion + node_expansion + + References + ---------- + .. [1] Vadhan, Salil P. + "Pseudorandomness." + *Foundations and Trends + in Theoretical Computer Science* 7.1–3 (2011): 1–336. + + + """ + num_cut_edges = cut_size(G, S, T=T, weight=weight) + num_total_edges = G.number_of_edges() + return num_cut_edges / (2 * num_total_edges) + + +# TODO What is the generalization to two arguments, S and T? Does the +# denominator become `min(len(S), len(T))`? +@nx._dispatchable +def node_expansion(G, S): + """Returns the node expansion of the set `S`. + + The *node expansion* is the quotient of the size of the node + boundary of *S* and the cardinality of *S*. [1] + + Parameters + ---------- + G : NetworkX graph + + S : collection + A collection of nodes in `G`. + + Returns + ------- + number + The node expansion of the set `S`. + + See also + -------- + boundary_expansion + edge_expansion + mixing_expansion + + References + ---------- + .. [1] Vadhan, Salil P. + "Pseudorandomness." + *Foundations and Trends + in Theoretical Computer Science* 7.1–3 (2011): 1–336. + + + """ + neighborhood = set(chain.from_iterable(G.neighbors(v) for v in S)) + return len(neighborhood) / len(S) + + +@nx._dispatchable +def boundary_expansion(G, S): + """Returns the boundary expansion of the set `S`. + + The *boundary expansion* of a set `S` is the ratio between the size of its + node boundary and the cardinality of the set itself [1]_ . + + Parameters + ---------- + G : NetworkX graph + The input graph. + + S : collection + A collection of nodes in `G`. + + Returns + ------- + number + The boundary expansion ratio: size of node boundary / size of `S`. + + Examples + -------- + The node boundary is {2, 3} (size 2), divided by ``|S|=2``: + + >>> G = nx.cycle_graph(4) + >>> S = {0, 1} + >>> nx.boundary_expansion(G, S) + 1.0 + + For disconnected sets, e.g. here where the node boundary is ``{1, 3, 5}``: + + >>> G = nx.cycle_graph(6) + >>> S = {0, 2, 4} + >>> nx.boundary_expansion(G, S) + 1.0 + + See also + -------- + :func:`~networkx.algorithms.boundary.node_boundary` + edge_expansion + mixing_expansion + node_expansion + + Notes + ----- + The node boundary is defined as all nodes not in `S` that are adjacent to + nodes in `S`. + + References + ---------- + .. [1] Vadhan, Salil P. + "Pseudorandomness." *Foundations and Trends in Theoretical Computer Science* + 7.1–3 (2011): 1–336. + """ + return len(nx.node_boundary(G, S)) / len(S) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cycles.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cycles.py new file mode 100644 index 0000000000000000000000000000000000000000..b8d82be1c4ace22262d7045cbae0b3e69837bb16 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/cycles.py @@ -0,0 +1,1234 @@ +""" +======================== +Cycle finding algorithms +======================== +""" + +from collections import defaultdict +from itertools import combinations, product +from math import inf + +import networkx as nx +from networkx.utils import not_implemented_for, pairwise + +__all__ = [ + "cycle_basis", + "simple_cycles", + "recursive_simple_cycles", + "find_cycle", + "minimum_cycle_basis", + "chordless_cycles", + "girth", +] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def cycle_basis(G, root=None): + """Returns a list of cycles which form a basis for cycles of G. + + A basis for cycles of a network is a minimal collection of + cycles such that any cycle in the network can be written + as a sum of cycles in the basis. Here summation of cycles + is defined as "exclusive or" of the edges. Cycle bases are + useful, e.g. when deriving equations for electric circuits + using Kirchhoff's Laws. + + Parameters + ---------- + G : NetworkX Graph + root : node, optional + Specify starting node for basis. + + Returns + ------- + A list of cycle lists. Each cycle list is a list of nodes + which forms a cycle (loop) in G. + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_cycle(G, [0, 1, 2, 3]) + >>> nx.add_cycle(G, [0, 3, 4, 5]) + >>> nx.cycle_basis(G, 0) + [[3, 4, 5, 0], [1, 2, 3, 0]] + + Notes + ----- + This is adapted from algorithm CACM 491 [1]_. + + References + ---------- + .. [1] Paton, K. An algorithm for finding a fundamental set of + cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. + + See Also + -------- + simple_cycles + minimum_cycle_basis + """ + gnodes = dict.fromkeys(G) # set-like object that maintains node order + cycles = [] + while gnodes: # loop over connected components + if root is None: + root = gnodes.popitem()[0] + stack = [root] + pred = {root: root} + used = {root: set()} + while stack: # walk the spanning tree finding cycles + z = stack.pop() # use last-in so cycles easier to find + zused = used[z] + for nbr in G[z]: + if nbr not in used: # new node + pred[nbr] = z + stack.append(nbr) + used[nbr] = {z} + elif nbr == z: # self loops + cycles.append([z]) + elif nbr not in zused: # found a cycle + pn = used[nbr] + cycle = [nbr, z] + p = pred[z] + while p not in pn: + cycle.append(p) + p = pred[p] + cycle.append(p) + cycles.append(cycle) + used[nbr].add(z) + for node in pred: + gnodes.pop(node, None) + root = None + return cycles + + +@nx._dispatchable +def simple_cycles(G, length_bound=None): + """Find simple cycles (elementary circuits) of a graph. + + A "simple cycle", or "elementary circuit", is a closed path where + no node appears twice. In a directed graph, two simple cycles are distinct + if they are not cyclic permutations of each other. In an undirected graph, + two simple cycles are distinct if they are not cyclic permutations of each + other nor of the other's reversal. + + Optionally, the cycles are bounded in length. In the unbounded case, we use + a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In + the bounded case, we use a version of the algorithm of Gupta and + Suzumura [2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_. + + The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some + well-known preprocessing techniques. When `G` is directed, we restrict our + attention to strongly connected components of `G`, generate all simple cycles + containing a certain node, remove that node, and further decompose the + remainder into strongly connected components. When `G` is undirected, we + restrict our attention to biconnected components, generate all simple cycles + containing a particular edge, remove that edge, and further decompose the + remainder into biconnected components. + + Note that multigraphs are supported by this function -- and in undirected + multigraphs, a pair of parallel edges is considered a cycle of length 2. + Likewise, self-loops are considered to be cycles of length 1. We define + cycles as sequences of nodes; so the presence of loops and parallel edges + does not change the number of simple cycles in a graph. + + Parameters + ---------- + G : NetworkX Graph + A networkx graph. Undirected, directed, and multigraphs are all supported. + + length_bound : int or None, optional (default=None) + If `length_bound` is an int, generate all simple cycles of `G` with length at + most `length_bound`. Otherwise, generate all simple cycles of `G`. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + + Examples + -------- + >>> G = nx.DiGraph([(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]) + >>> sorted(nx.simple_cycles(G)) + [[0], [0, 1, 2], [0, 2], [1, 2], [2]] + + To filter the cycles so that they don't include certain nodes or edges, + copy your graph and eliminate those nodes or edges before calling. + For example, to exclude self-loops from the above example: + + >>> H = G.copy() + >>> H.remove_edges_from(nx.selfloop_edges(G)) + >>> sorted(nx.simple_cycles(H)) + [[0, 1, 2], [0, 2], [1, 2]] + + Notes + ----- + When `length_bound` is None, the time complexity is $O((n+e)(c+1))$ for $n$ + nodes, $e$ edges and $c$ simple circuits. Otherwise, when ``length_bound > 1``, + the time complexity is $O((c+n)(k-1)d^k)$ where $d$ is the average degree of + the nodes of `G` and $k$ = `length_bound`. + + Raises + ------ + ValueError + when ``length_bound < 0``. + + References + ---------- + .. [1] Finding all the elementary circuits of a directed graph. + D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. + https://doi.org/10.1137/0204007 + .. [2] Finding All Bounded-Length Simple Cycles in a Directed Graph + A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094 + .. [3] Enumerating the cycles of a digraph: a new preprocessing strategy. + G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982. + .. [4] A search strategy for the elementary cycles of a directed graph. + J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS, + v. 16, no. 2, 192-204, 1976. + .. [5] Optimal Listing of Cycles and st-Paths in Undirected Graphs + R. Ferreira and R. Grossi and A. Marino and N. Pisanti and R. Rizzi and + G. Sacomoto https://arxiv.org/abs/1205.2766 + + See Also + -------- + cycle_basis + chordless_cycles + """ + + if length_bound is not None: + if length_bound == 0: + return + elif length_bound < 0: + raise ValueError("length bound must be non-negative") + + directed = G.is_directed() + yield from ([v] for v, Gv in G.adj.items() if v in Gv) + + if length_bound is not None and length_bound == 1: + return + + if G.is_multigraph() and not directed: + visited = set() + for u, Gu in G.adj.items(): + multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited) + yield from ([u, v] for v, m in multiplicity if m > 1) + visited.add(u) + + # explicitly filter out loops; implicitly filter out parallel edges + if directed: + G = nx.DiGraph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u) + else: + G = nx.Graph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u) + + # this case is not strictly necessary but improves performance + if length_bound is not None and length_bound == 2: + if directed: + visited = set() + for u, Gu in G.adj.items(): + yield from ( + [v, u] for v in visited.intersection(Gu) if G.has_edge(v, u) + ) + visited.add(u) + return + + if directed: + yield from _directed_cycle_search(G, length_bound) + else: + yield from _undirected_cycle_search(G, length_bound) + + +def _directed_cycle_search(G, length_bound): + """A dispatch function for `simple_cycles` for directed graphs. + + We generate all cycles of G through binary partition. + + 1. Pick a node v in G which belongs to at least one cycle + a. Generate all cycles of G which contain the node v. + b. Recursively generate all cycles of G \\ v. + + This is accomplished through the following: + + 1. Compute the strongly connected components SCC of G. + 2. Select and remove a biconnected component C from BCC. Select a + non-tree edge (u, v) of a depth-first search of G[C]. + 3. For each simple cycle P containing v in G[C], yield P. + 4. Add the biconnected components of G[C \\ v] to BCC. + + If the parameter length_bound is not None, then step 3 will be limited to + simple cycles of length at most length_bound. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + length_bound : int or None + If length_bound is an int, generate all simple cycles of G with length at most length_bound. + Otherwise, generate all simple cycles of G. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + """ + + scc = nx.strongly_connected_components + components = [c for c in scc(G) if len(c) >= 2] + while components: + c = components.pop() + Gc = G.subgraph(c) + v = next(iter(c)) + if length_bound is None: + yield from _johnson_cycle_search(Gc, [v]) + else: + yield from _bounded_cycle_search(Gc, [v], length_bound) + # delete v after searching G, to make sure we can find v + G.remove_node(v) + components.extend(c for c in scc(Gc) if len(c) >= 2) + + +def _undirected_cycle_search(G, length_bound): + """A dispatch function for `simple_cycles` for undirected graphs. + + We generate all cycles of G through binary partition. + + 1. Pick an edge (u, v) in G which belongs to at least one cycle + a. Generate all cycles of G which contain the edge (u, v) + b. Recursively generate all cycles of G \\ (u, v) + + This is accomplished through the following: + + 1. Compute the biconnected components BCC of G. + 2. Select and remove a biconnected component C from BCC. Select a + non-tree edge (u, v) of a depth-first search of G[C]. + 3. For each (v -> u) path P remaining in G[C] \\ (u, v), yield P. + 4. Add the biconnected components of G[C] \\ (u, v) to BCC. + + If the parameter length_bound is not None, then step 3 will be limited to simple paths + of length at most length_bound. + + Parameters + ---------- + G : NetworkX Graph + An undirected graph + + length_bound : int or None + If length_bound is an int, generate all simple cycles of G with length at most length_bound. + Otherwise, generate all simple cycles of G. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + """ + + bcc = nx.biconnected_components + components = [c for c in bcc(G) if len(c) >= 3] + while components: + c = components.pop() + Gc = G.subgraph(c) + uv = list(next(iter(Gc.edges))) + G.remove_edge(*uv) + # delete (u, v) before searching G, to avoid fake 3-cycles [u, v, u] + if length_bound is None: + yield from _johnson_cycle_search(Gc, uv) + else: + yield from _bounded_cycle_search(Gc, uv, length_bound) + components.extend(c for c in bcc(Gc) if len(c) >= 3) + + +class _NeighborhoodCache(dict): + """Very lightweight graph wrapper which caches neighborhoods as list. + + This dict subclass uses the __missing__ functionality to query graphs for + their neighborhoods, and store the result as a list. This is used to avoid + the performance penalty incurred by subgraph views. + """ + + def __init__(self, G): + self.G = G + + def __missing__(self, v): + Gv = self[v] = list(self.G[v]) + return Gv + + +def _johnson_cycle_search(G, path): + """The main loop of the cycle-enumeration algorithm of Johnson. + + Parameters + ---------- + G : NetworkX Graph or DiGraph + A graph + + path : list + A cycle prefix. All cycles generated will begin with this prefix. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + + References + ---------- + .. [1] Finding all the elementary circuits of a directed graph. + D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. + https://doi.org/10.1137/0204007 + + """ + + G = _NeighborhoodCache(G) + blocked = set(path) + B = defaultdict(set) # graph portions that yield no elementary circuit + start = path[0] + stack = [iter(G[path[-1]])] + closed = [False] + while stack: + nbrs = stack[-1] + for w in nbrs: + if w == start: + yield path[:] + closed[-1] = True + elif w not in blocked: + path.append(w) + closed.append(False) + stack.append(iter(G[w])) + blocked.add(w) + break + else: # no more nbrs + stack.pop() + v = path.pop() + if closed.pop(): + if closed: + closed[-1] = True + unblock_stack = {v} + while unblock_stack: + u = unblock_stack.pop() + if u in blocked: + blocked.remove(u) + unblock_stack.update(B[u]) + B[u].clear() + else: + for w in G[v]: + B[w].add(v) + + +def _bounded_cycle_search(G, path, length_bound): + """The main loop of the cycle-enumeration algorithm of Gupta and Suzumura. + + Parameters + ---------- + G : NetworkX Graph or DiGraph + A graph + + path : list + A cycle prefix. All cycles generated will begin with this prefix. + + length_bound: int + A length bound. All cycles generated will have length at most length_bound. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + + References + ---------- + .. [1] Finding All Bounded-Length Simple Cycles in a Directed Graph + A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094 + + """ + G = _NeighborhoodCache(G) + lock = {v: 0 for v in path} + B = defaultdict(set) + start = path[0] + stack = [iter(G[path[-1]])] + blen = [length_bound] + while stack: + nbrs = stack[-1] + for w in nbrs: + if w == start: + yield path[:] + blen[-1] = 1 + elif len(path) < lock.get(w, length_bound): + path.append(w) + blen.append(length_bound) + lock[w] = len(path) + stack.append(iter(G[w])) + break + else: + stack.pop() + v = path.pop() + bl = blen.pop() + if blen: + blen[-1] = min(blen[-1], bl) + if bl < length_bound: + relax_stack = [(bl, v)] + while relax_stack: + bl, u = relax_stack.pop() + if lock.get(u, length_bound) < length_bound - bl + 1: + lock[u] = length_bound - bl + 1 + relax_stack.extend((bl + 1, w) for w in B[u].difference(path)) + else: + for w in G[v]: + B[w].add(v) + + +@nx._dispatchable +def chordless_cycles(G, length_bound=None): + """Find simple chordless cycles of a graph. + + A `simple cycle` is a closed path where no node appears twice. In a simple + cycle, a `chord` is an additional edge between two nodes in the cycle. A + `chordless cycle` is a simple cycle without chords. Said differently, a + chordless cycle is a cycle C in a graph G where the number of edges in the + induced graph G[C] is equal to the length of `C`. + + Note that some care must be taken in the case that G is not a simple graph + nor a simple digraph. Some authors limit the definition of chordless cycles + to have a prescribed minimum length; we do not. + + 1. We interpret self-loops to be chordless cycles, except in multigraphs + with multiple loops in parallel. Likewise, in a chordless cycle of + length greater than 1, there can be no nodes with self-loops. + + 2. We interpret directed two-cycles to be chordless cycles, except in + multi-digraphs when any edge in a two-cycle has a parallel copy. + + 3. We interpret parallel pairs of undirected edges as two-cycles, except + when a third (or more) parallel edge exists between the two nodes. + + 4. Generalizing the above, edges with parallel clones may not occur in + chordless cycles. + + In a directed graph, two chordless cycles are distinct if they are not + cyclic permutations of each other. In an undirected graph, two chordless + cycles are distinct if they are not cyclic permutations of each other nor of + the other's reversal. + + Optionally, the cycles are bounded in length. + + We use an algorithm strongly inspired by that of Dias et al [1]_. It has + been modified in the following ways: + + 1. Recursion is avoided, per Python's limitations. + + 2. The labeling function is not necessary, because the starting paths + are chosen (and deleted from the host graph) to prevent multiple + occurrences of the same path. + + 3. The search is optionally bounded at a specified length. + + 4. Support for directed graphs is provided by extending cycles along + forward edges, and blocking nodes along forward and reverse edges. + + 5. Support for multigraphs is provided by omitting digons from the set + of forward edges. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + length_bound : int or None, optional (default=None) + If length_bound is an int, generate all simple cycles of G with length at + most length_bound. Otherwise, generate all simple cycles of G. + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + + Examples + -------- + >>> sorted(list(nx.chordless_cycles(nx.complete_graph(4)))) + [[1, 0, 2], [1, 0, 3], [2, 0, 3], [2, 1, 3]] + + Notes + ----- + When length_bound is None, and the graph is simple, the time complexity is + $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ chordless cycles. + + Raises + ------ + ValueError + when length_bound < 0. + + References + ---------- + .. [1] Efficient enumeration of chordless cycles + E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi + https://arxiv.org/abs/1309.1051 + + See Also + -------- + simple_cycles + """ + + if length_bound is not None: + if length_bound == 0: + return + elif length_bound < 0: + raise ValueError("length bound must be non-negative") + + directed = G.is_directed() + multigraph = G.is_multigraph() + + if multigraph: + yield from ([v] for v, Gv in G.adj.items() if len(Gv.get(v, ())) == 1) + else: + yield from ([v] for v, Gv in G.adj.items() if v in Gv) + + if length_bound is not None and length_bound == 1: + return + + # Nodes with loops cannot belong to longer cycles. Let's delete them here. + # also, we implicitly reduce the multiplicity of edges down to 1 in the case + # of multiedges. + loops = set(nx.nodes_with_selfloops(G)) + edges = ((u, v) for u in G if u not in loops for v in G._adj[u] if v not in loops) + if directed: + F = nx.DiGraph(edges) + B = F.to_undirected(as_view=False) + else: + F = nx.Graph(edges) + B = None + + # If we're given a multigraph, we have a few cases to consider with parallel + # edges. + # + # 1. If we have 2 or more edges in parallel between the nodes (u, v), we + # must not construct longer cycles along (u, v). + # 2. If G is not directed, then a pair of parallel edges between (u, v) is a + # chordless cycle unless there exists a third (or more) parallel edge. + # 3. If G is directed, then parallel edges do not form cycles, but do + # preclude back-edges from forming cycles (handled in the next section), + # Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also + # present, then we remove both from F. + # + # In directed graphs, we need to consider both directions that edges can + # take, so iterate over all edges (u, v) and possibly (v, u). In undirected + # graphs, we need to be a little careful to only consider every edge once, + # so we use a "visited" set to emulate node-order comparisons. + + if multigraph: + if not directed: + B = F.copy() + visited = set() + for u, Gu in G.adj.items(): + if u in loops: + continue + if directed: + multiplicity = ((v, len(Guv)) for v, Guv in Gu.items()) + for v, m in multiplicity: + if m > 1: + F.remove_edges_from(((u, v), (v, u))) + else: + multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited) + for v, m in multiplicity: + if m == 2: + yield [u, v] + if m > 1: + F.remove_edge(u, v) + visited.add(u) + + # If we're given a directed graphs, we need to think about digons. If we + # have two edges (u, v) and (v, u), then that's a two-cycle. If either edge + # was duplicated above, then we removed both from F. So, any digons we find + # here are chordless. After finding digons, we remove their edges from F + # to avoid traversing them in the search for chordless cycles. + if directed: + for u, Fu in F.adj.items(): + digons = [[u, v] for v in Fu if F.has_edge(v, u)] + yield from digons + F.remove_edges_from(digons) + F.remove_edges_from(e[::-1] for e in digons) + + if length_bound is not None and length_bound == 2: + return + + # Now, we prepare to search for cycles. We have removed all cycles of + # lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly + # separate digraphs into their strongly connected components, and undirected + # graphs into their biconnected components. For each component, we pick a + # node v, search for chordless cycles based at each "stem" (u, v, w), and + # then remove v from that component before separating the graph again. + if directed: + separate = nx.strongly_connected_components + + # Directed stems look like (u -> v -> w), so we use the product of + # predecessors of v with successors of v. + def stems(C, v): + for u, w in product(C.pred[v], C.succ[v]): + if not G.has_edge(u, w): # omit stems with acyclic chords + yield [u, v, w], F.has_edge(w, u) + + else: + separate = nx.biconnected_components + + # Undirected stems look like (u ~ v ~ w), but we must not also search + # (w ~ v ~ u), so we use combinations of v's neighbors of length 2. + def stems(C, v): + yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2)) + + components = [c for c in separate(F) if len(c) > 2] + while components: + c = components.pop() + v = next(iter(c)) + Fc = F.subgraph(c) + Fcc = Bcc = None + for S, is_triangle in stems(Fc, v): + if is_triangle: + yield S + else: + if Fcc is None: + Fcc = _NeighborhoodCache(Fc) + Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c)) + yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound) + + components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2) + + +def _chordless_cycle_search(F, B, path, length_bound): + """The main loop for chordless cycle enumeration. + + This algorithm is strongly inspired by that of Dias et al [1]_. It has been + modified in the following ways: + + 1. Recursion is avoided, per Python's limitations + + 2. The labeling function is not necessary, because the starting paths + are chosen (and deleted from the host graph) to prevent multiple + occurrences of the same path + + 3. The search is optionally bounded at a specified length + + 4. Support for directed graphs is provided by extending cycles along + forward edges, and blocking nodes along forward and reverse edges + + 5. Support for multigraphs is provided by omitting digons from the set + of forward edges + + Parameters + ---------- + F : _NeighborhoodCache + A graph of forward edges to follow in constructing cycles + + B : _NeighborhoodCache + A graph of blocking edges to prevent the production of chordless cycles + + path : list + A cycle prefix. All cycles generated will begin with this prefix. + + length_bound : int + A length bound. All cycles generated will have length at most length_bound. + + + Yields + ------ + list of nodes + Each cycle is represented by a list of nodes along the cycle. + + References + ---------- + .. [1] Efficient enumeration of chordless cycles + E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi + https://arxiv.org/abs/1309.1051 + + """ + blocked = defaultdict(int) + target = path[0] + blocked[path[1]] = 1 + for w in path[1:]: + for v in B[w]: + blocked[v] += 1 + + stack = [iter(F[path[2]])] + while stack: + nbrs = stack[-1] + for w in nbrs: + if blocked[w] == 1 and (length_bound is None or len(path) < length_bound): + Fw = F[w] + if target in Fw: + yield path + [w] + else: + Bw = B[w] + if target in Bw: + continue + for v in Bw: + blocked[v] += 1 + path.append(w) + stack.append(iter(Fw)) + break + else: + stack.pop() + for v in B[path.pop()]: + blocked[v] -= 1 + + +@not_implemented_for("undirected") +@nx._dispatchable(mutates_input=True) +def recursive_simple_cycles(G): + """Find simple cycles (elementary circuits) of a directed graph. + + A `simple cycle`, or `elementary circuit`, is a closed path where + no node appears twice. Two elementary circuits are distinct if they + are not cyclic permutations of each other. + + This version uses a recursive algorithm to build a list of cycles. + You should probably use the iterator version called simple_cycles(). + Warning: This recursive version uses lots of RAM! + It appears in NetworkX for pedagogical value. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + Returns + ------- + A list of cycles, where each cycle is represented by a list of nodes + along the cycle. + + Example: + + >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] + >>> G = nx.DiGraph(edges) + >>> nx.recursive_simple_cycles(G) + [[0], [2], [0, 1, 2], [0, 2], [1, 2]] + + Notes + ----- + The implementation follows pp. 79-80 in [1]_. + + The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ + elementary circuits. + + References + ---------- + .. [1] Finding all the elementary circuits of a directed graph. + D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. + https://doi.org/10.1137/0204007 + + See Also + -------- + simple_cycles, cycle_basis + """ + + # Jon Olav Vik, 2010-08-09 + def _unblock(thisnode): + """Recursively unblock and remove nodes from B[thisnode].""" + if blocked[thisnode]: + blocked[thisnode] = False + while B[thisnode]: + _unblock(B[thisnode].pop()) + + def circuit(thisnode, startnode, component): + closed = False # set to True if elementary path is closed + path.append(thisnode) + blocked[thisnode] = True + for nextnode in component[thisnode]: # direct successors of thisnode + if nextnode == startnode: + result.append(path[:]) + closed = True + elif not blocked[nextnode]: + if circuit(nextnode, startnode, component): + closed = True + if closed: + _unblock(thisnode) + else: + for nextnode in component[thisnode]: + if thisnode not in B[nextnode]: # TODO: use set for speedup? + B[nextnode].append(thisnode) + path.pop() # remove thisnode from path + return closed + + path = [] # stack of nodes in current path + blocked = defaultdict(bool) # vertex: blocked from search? + B = defaultdict(list) # graph portions that yield no elementary circuit + result = [] # list to accumulate the circuits found + + # Johnson's algorithm exclude self cycle edges like (v, v) + # To be backward compatible, we record those cycles in advance + # and then remove from subG + for v in G: + if G.has_edge(v, v): + result.append([v]) + G.remove_edge(v, v) + + # Johnson's algorithm requires some ordering of the nodes. + # They might not be sortable so we assign an arbitrary ordering. + ordering = dict(zip(G, range(len(G)))) + for s in ordering: + # Build the subgraph induced by s and following nodes in the ordering + subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s]) + # Find the strongly connected component in the subgraph + # that contains the least node according to the ordering + strongcomp = nx.strongly_connected_components(subgraph) + mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns)) + component = G.subgraph(mincomp) + if len(component) > 1: + # smallest node in the component according to the ordering + startnode = min(component, key=ordering.__getitem__) + for node in component: + blocked[node] = False + B[node][:] = [] + dummy = circuit(startnode, startnode, component) + return result + + +@nx._dispatchable +def find_cycle(G, source=None, orientation=None): + """Returns a cycle found via depth-first traversal. + + The cycle is a list of edges indicating the cyclic path. + Orientation of directed edges is controlled by `orientation`. + + Parameters + ---------- + G : graph + A directed/undirected graph/multigraph. + + source : node, list of nodes + The node from which the traversal begins. If None, then a source + is chosen arbitrarily and repeatedly until all edges from each node in + the graph are searched. + + orientation : None | 'original' | 'reverse' | 'ignore' (default: None) + For directed graphs and directed multigraphs, edge traversals need not + respect the original orientation of the edges. + When set to 'reverse' every edge is traversed in the reverse direction. + When set to 'ignore', every edge is treated as undirected. + When set to 'original', every edge is treated as directed. + In all three cases, the yielded edge tuples add a last entry to + indicate the direction in which that edge was traversed. + If orientation is None, the yielded edge has no direction indicated. + The direction is respected, but not reported. + + Returns + ------- + edges : directed edges + A list of directed edges indicating the path taken for the loop. + If no cycle is found, then an exception is raised. + For graphs, an edge is of the form `(u, v)` where `u` and `v` + are the tail and head of the edge as determined by the traversal. + For multigraphs, an edge is of the form `(u, v, key)`, where `key` is + the key of the edge. When the graph is directed, then `u` and `v` + are always in the order of the actual directed edge. + If orientation is not None then the edge tuple is extended to include + the direction of traversal ('forward' or 'reverse') on that edge. + + Raises + ------ + NetworkXNoCycle + If no cycle was found. + + Examples + -------- + In this example, we construct a DAG and find, in the first call, that there + are no directed cycles, and so an exception is raised. In the second call, + we ignore edge orientations and find that there is an undirected cycle. + Note that the second call finds a directed cycle while effectively + traversing an undirected graph, and so, we found an "undirected cycle". + This means that this DAG structure does not form a directed tree (which + is also known as a polytree). + + >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) + >>> nx.find_cycle(G, orientation="original") + Traceback (most recent call last): + ... + networkx.exception.NetworkXNoCycle: No cycle found. + >>> list(nx.find_cycle(G, orientation="ignore")) + [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')] + + See Also + -------- + simple_cycles + """ + if not G.is_directed() or orientation in (None, "original"): + + def tailhead(edge): + return edge[:2] + + elif orientation == "reverse": + + def tailhead(edge): + return edge[1], edge[0] + + elif orientation == "ignore": + + def tailhead(edge): + if edge[-1] == "reverse": + return edge[1], edge[0] + return edge[:2] + + explored = set() + cycle = [] + final_node = None + for start_node in G.nbunch_iter(source): + if start_node in explored: + # No loop is possible. + continue + + edges = [] + # All nodes seen in this iteration of edge_dfs + seen = {start_node} + # Nodes in active path. + active_nodes = {start_node} + previous_head = None + + for edge in nx.edge_dfs(G, start_node, orientation): + # Determine if this edge is a continuation of the active path. + tail, head = tailhead(edge) + if head in explored: + # Then we've already explored it. No loop is possible. + continue + if previous_head is not None and tail != previous_head: + # This edge results from backtracking. + # Pop until we get a node whose head equals the current tail. + # So for example, we might have: + # (0, 1), (1, 2), (2, 3), (1, 4) + # which must become: + # (0, 1), (1, 4) + while True: + try: + popped_edge = edges.pop() + except IndexError: + edges = [] + active_nodes = {tail} + break + else: + popped_head = tailhead(popped_edge)[1] + active_nodes.remove(popped_head) + + if edges: + last_head = tailhead(edges[-1])[1] + if tail == last_head: + break + edges.append(edge) + + if head in active_nodes: + # We have a loop! + cycle.extend(edges) + final_node = head + break + else: + seen.add(head) + active_nodes.add(head) + previous_head = head + + if cycle: + break + else: + explored.update(seen) + + else: + assert len(cycle) == 0 + raise nx.exception.NetworkXNoCycle("No cycle found.") + + # We now have a list of edges which ends on a cycle. + # So we need to remove from the beginning edges that are not relevant. + + for i, edge in enumerate(cycle): + tail, head = tailhead(edge) + if tail == final_node: + break + + return cycle[i:] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def minimum_cycle_basis(G, weight=None): + """Returns a minimum weight cycle basis for G + + Minimum weight means a cycle basis for which the total weight + (length for unweighted graphs) of all the cycles is minimum. + + Parameters + ---------- + G : NetworkX Graph + weight: string + name of the edge attribute to use for edge weights + + Returns + ------- + A list of cycle lists. Each cycle list is a list of nodes + which forms a cycle (loop) in G. Note that the nodes are not + necessarily returned in a order by which they appear in the cycle + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_cycle(G, [0, 1, 2, 3]) + >>> nx.add_cycle(G, [0, 3, 4, 5]) + >>> nx.minimum_cycle_basis(G) + [[5, 4, 3, 0], [3, 2, 1, 0]] + + References: + [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for + Minimum Cycle Basis of Graphs." + http://link.springer.com/article/10.1007/s00453-007-9064-z + [2] de Pina, J. 1995. Applications of shortest path methods. + Ph.D. thesis, University of Amsterdam, Netherlands + + See Also + -------- + simple_cycles, cycle_basis + """ + # We first split the graph in connected subgraphs + return sum( + (_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)), + [], + ) + + +def _min_cycle_basis(G, weight): + cb = [] + # We extract the edges not in a spanning tree. We do not really need a + # *minimum* spanning tree. That is why we call the next function with + # weight=None. Depending on implementation, it may be faster as well + tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False)) + chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges} + + # We maintain a set of vectors orthogonal to sofar found cycles + set_orth = [{edge} for edge in chords] + while set_orth: + base = set_orth.pop() + # kth cycle is "parallel" to kth vector in set_orth + cycle_edges = _min_cycle(G, base, weight) + cb.append([v for u, v in cycle_edges]) + + # now update set_orth so that k+1,k+2... th elements are + # orthogonal to the newly found cycle, as per [p. 336, 1] + set_orth = [ + ( + {e for e in orth if e not in base if e[::-1] not in base} + | {e for e in base if e not in orth if e[::-1] not in orth} + ) + if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2 + else orth + for orth in set_orth + ] + return cb + + +def _min_cycle(G, orth, weight): + """ + Computes the minimum weight cycle in G, + orthogonal to the vector orth as per [p. 338, 1] + Use (u, 1) to indicate the lifted copy of u (denoted u' in paper). + """ + Gi = nx.Graph() + + # Add 2 copies of each edge in G to Gi. + # If edge is in orth, add cross edge; otherwise in-plane edge + for u, v, wt in G.edges(data=weight, default=1): + if (u, v) in orth or (v, u) in orth: + Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt) + else: + Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt) + + # find the shortest length in Gi between n and (n, 1) for each n + # Note: Use "Gi_weight" for name of weight attribute + spl = nx.shortest_path_length + lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G} + + # Now compute that short path in Gi, which translates to a cycle in G + start = min(lift, key=lift.get) + end = (start, 1) + min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight") + + # Now we obtain the actual path, re-map nodes in Gi to those in G + min_path = [n if n in G else n[0] for n in min_path_i] + + # Now remove the edges that occur two times + # two passes: flag which edges get kept, then build it + edgelist = list(pairwise(min_path)) + edgeset = set() + for e in edgelist: + if e in edgeset: + edgeset.remove(e) + elif e[::-1] in edgeset: + edgeset.remove(e[::-1]) + else: + edgeset.add(e) + + min_edgelist = [] + for e in edgelist: + if e in edgeset: + min_edgelist.append(e) + edgeset.remove(e) + elif e[::-1] in edgeset: + min_edgelist.append(e[::-1]) + edgeset.remove(e[::-1]) + + return min_edgelist + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def girth(G): + """Returns the girth of the graph. + + The girth of a graph is the length of its shortest cycle, or infinity if + the graph is acyclic. The algorithm follows the description given on the + Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n + nodes. + + Parameters + ---------- + G : NetworkX Graph + + Returns + ------- + int or math.inf + + Examples + -------- + All examples below (except P_5) can easily be checked using Wikipedia, + which has a page for each of these famous graphs. + + >>> nx.girth(nx.chvatal_graph()) + 4 + >>> nx.girth(nx.tutte_graph()) + 4 + >>> nx.girth(nx.petersen_graph()) + 5 + >>> nx.girth(nx.heawood_graph()) + 6 + >>> nx.girth(nx.pappus_graph()) + 6 + >>> nx.girth(nx.path_graph(5)) + inf + + References + ---------- + .. [1] `Wikipedia: Girth `_ + + """ + girth = depth_limit = inf + tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE + level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE + for n in G: + # run a BFS from source n, keeping track of distances; since we want + # the shortest cycle, no need to explore beyond the current minimum length + depth = {n: 0} + for u, v, label in nx.bfs_labeled_edges(G, n): + du = depth[u] + if du > depth_limit: + break + if label is tree_edge: + depth[v] = du + 1 + else: + # if (u, v) is a level edge, the length is du + du + 1 (odd) + # otherwise, it's a forward edge; length is du + (du + 1) + 1 (even) + delta = label is level_edge + length = du + du + 2 - delta + if length < girth: + girth = length + depth_limit = du - delta + + return girth diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/d_separation.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/d_separation.py new file mode 100644 index 0000000000000000000000000000000000000000..3d85a2c724fa0a1e14ab99db2be7a3c46b404b4a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/d_separation.py @@ -0,0 +1,677 @@ +""" +Algorithm for testing d-separation in DAGs. + +*d-separation* is a test for conditional independence in probability +distributions that can be factorized using DAGs. It is a purely +graphical test that uses the underlying graph and makes no reference +to the actual distribution parameters. See [1]_ for a formal +definition. + +The implementation is based on the conceptually simple linear time +algorithm presented in [2]_. Refer to [3]_, [4]_ for a couple of +alternative algorithms. + +The functional interface in NetworkX consists of three functions: + +- `find_minimal_d_separator` returns a minimal d-separator set ``z``. + That is, removing any node or nodes from it makes it no longer a d-separator. +- `is_d_separator` checks if a given set is a d-separator. +- `is_minimal_d_separator` checks if a given set is a minimal d-separator. + +D-separators +------------ + +Here, we provide a brief overview of d-separation and related concepts that +are relevant for understanding it: + +The ideas of d-separation and d-connection relate to paths being open or blocked. + +- A "path" is a sequence of nodes connected in order by edges. Unlike for most + graph theory analysis, the direction of the edges is ignored. Thus the path + can be thought of as a traditional path on the undirected version of the graph. +- A "candidate d-separator" ``z`` is a set of nodes being considered as + possibly blocking all paths between two prescribed sets ``x`` and ``y`` of nodes. + We refer to each node in the candidate d-separator as "known". +- A "collider" node on a path is a node that is a successor of its two neighbor + nodes on the path. That is, ``c`` is a collider if the edge directions + along the path look like ``... u -> c <- v ...``. +- If a collider node or any of its descendants are "known", the collider + is called an "open collider". Otherwise it is a "blocking collider". +- Any path can be "blocked" in two ways. If the path contains a "known" node + that is not a collider, the path is blocked. Also, if the path contains a + collider that is not a "known" node, the path is blocked. +- A path is "open" if it is not blocked. That is, it is open if every node is + either an open collider or not a "known". Said another way, every + "known" in the path is a collider and every collider is open (has a + "known" as a inclusive descendant). The concept of "open path" is meant to + demonstrate a probabilistic conditional dependence between two nodes given + prescribed knowledge ("known" nodes). +- Two sets ``x`` and ``y`` of nodes are "d-separated" by a set of nodes ``z`` + if all paths between nodes in ``x`` and nodes in ``y`` are blocked. That is, + if there are no open paths from any node in ``x`` to any node in ``y``. + Such a set ``z`` is a "d-separator" of ``x`` and ``y``. +- A "minimal d-separator" is a d-separator ``z`` for which no node or subset + of nodes can be removed with it still being a d-separator. + +The d-separator blocks some paths between ``x`` and ``y`` but opens others. +Nodes in the d-separator block paths if the nodes are not colliders. +But if a collider or its descendant nodes are in the d-separation set, the +colliders are open, allowing a path through that collider. + +Illustration of D-separation with examples +------------------------------------------ + +A pair of two nodes, ``u`` and ``v``, are d-connected if there is a path +from ``u`` to ``v`` that is not blocked. That means, there is an open +path from ``u`` to ``v``. + +For example, if the d-separating set is the empty set, then the following paths are +open between ``u`` and ``v``: + +- u <- n -> v +- u -> w -> ... -> n -> v + +If on the other hand, ``n`` is in the d-separating set, then ``n`` blocks +those paths between ``u`` and ``v``. + +Colliders block a path if they and their descendants are not included +in the d-separating set. An example of a path that is blocked when the +d-separating set is empty is: + +- u -> w -> ... -> n <- v + +The node ``n`` is a collider in this path and is not in the d-separating set. +So ``n`` blocks this path. However, if ``n`` or a descendant of ``n`` is +included in the d-separating set, then the path through the collider +at ``n`` (... -> n <- ...) is "open". + +D-separation is concerned with blocking all paths between nodes from ``x`` to ``y``. +A d-separating set between ``x`` and ``y`` is one where all paths are blocked. + +D-separation and its applications in probability +------------------------------------------------ + +D-separation is commonly used in probabilistic causal-graph models. D-separation +connects the idea of probabilistic "dependence" with separation in a graph. If +one assumes the causal Markov condition [5]_, (every node is conditionally +independent of its non-descendants, given its parents) then d-separation implies +conditional independence in probability distributions. +Symmetrically, d-connection implies dependence. + +The intuition is as follows. The edges on a causal graph indicate which nodes +influence the outcome of other nodes directly. An edge from u to v +implies that the outcome of event ``u`` influences the probabilities for +the outcome of event ``v``. Certainly knowing ``u`` changes predictions for ``v``. +But also knowing ``v`` changes predictions for ``u``. The outcomes are dependent. +Furthermore, an edge from ``v`` to ``w`` would mean that ``w`` and ``v`` are dependent +and thus that ``u`` could indirectly influence ``w``. + +Without any knowledge about the system (candidate d-separating set is empty) +a causal graph ``u -> v -> w`` allows all three nodes to be dependent. But +if we know the outcome of ``v``, the conditional probabilities of outcomes for +``u`` and ``w`` are independent of each other. That is, once we know the outcome +for ``v``, the probabilities for ``w`` do not depend on the outcome for ``u``. +This is the idea behind ``v`` blocking the path if it is "known" (in the candidate +d-separating set). + +The same argument works whether the direction of the edges are both +left-going and when both arrows head out from the middle. Having a "known" +node on a path blocks the collider-free path because those relationships +make the conditional probabilities independent. + +The direction of the causal edges does impact dependence precisely in the +case of a collider e.g. ``u -> v <- w``. In that situation, both ``u`` and ``w`` +influence ``v``. But they do not directly influence each other. So without any +knowledge of any outcomes, ``u`` and ``w`` are independent. That is the idea behind +colliders blocking the path. But, if ``v`` is known, the conditional probabilities +of ``u`` and ``w`` can be dependent. This is the heart of Berkson's Paradox [6]_. +For example, suppose ``u`` and ``w`` are boolean events (they either happen or do not) +and ``v`` represents the outcome "at least one of ``u`` and ``w`` occur". Then knowing +``v`` is true makes the conditional probabilities of ``u`` and ``w`` dependent. +Essentially, knowing that at least one of them is true raises the probability of +each. But further knowledge that ``w`` is true (or false) change the conditional +probability of ``u`` to either the original value or 1. So the conditional +probability of ``u`` depends on the outcome of ``w`` even though there is no +causal relationship between them. When a collider is known, dependence can +occur across paths through that collider. This is the reason open colliders +do not block paths. + +Furthermore, even if ``v`` is not "known", if one of its descendants is "known" +we can use that information to know more about ``v`` which again makes +``u`` and ``w`` potentially dependent. Suppose the chance of ``n`` occurring +is much higher when ``v`` occurs ("at least one of ``u`` and ``w`` occur"). +Then if we know ``n`` occurred, it is more likely that ``v`` occurred and that +makes the chance of ``u`` and ``w`` dependent. This is the idea behind why +a collider does no block a path if any descendant of the collider is "known". + +When two sets of nodes ``x`` and ``y`` are d-separated by a set ``z``, +it means that given the outcomes of the nodes in ``z``, the probabilities +of outcomes of the nodes in ``x`` are independent of the outcomes of the +nodes in ``y`` and vice versa. + +Examples +-------- +A Hidden Markov Model with 5 observed states and 5 hidden states +where the hidden states have causal relationships resulting in +a path results in the following causal network. We check that +early states along the path are separated from late state in +the path by the d-separator of the middle hidden state. +Thus if we condition on the middle hidden state, the early +state probabilities are independent of the late state outcomes. + +>>> G = nx.DiGraph() +>>> G.add_edges_from( +... [ +... ("H1", "H2"), +... ("H2", "H3"), +... ("H3", "H4"), +... ("H4", "H5"), +... ("H1", "O1"), +... ("H2", "O2"), +... ("H3", "O3"), +... ("H4", "O4"), +... ("H5", "O5"), +... ] +... ) +>>> x, y, z = ({"H1", "O1"}, {"H5", "O5"}, {"H3"}) +>>> nx.is_d_separator(G, x, y, z) +True +>>> nx.is_minimal_d_separator(G, x, y, z) +True +>>> nx.is_minimal_d_separator(G, x, y, z | {"O3"}) +False +>>> z = nx.find_minimal_d_separator(G, x | y, {"O2", "O3", "O4"}) +>>> z == {"H2", "H4"} +True + +If no minimal_d_separator exists, `None` is returned + +>>> other_z = nx.find_minimal_d_separator(G, x | y, {"H2", "H3"}) +>>> other_z is None +True + + +References +---------- + +.. [1] Pearl, J. (2009). Causality. Cambridge: Cambridge University Press. + +.. [2] Darwiche, A. (2009). Modeling and reasoning with Bayesian networks. + Cambridge: Cambridge University Press. + +.. [3] Shachter, Ross D. "Bayes-ball: The rational pastime (for + determining irrelevance and requisite information in belief networks + and influence diagrams)." In Proceedings of the Fourteenth Conference + on Uncertainty in Artificial Intelligence (UAI), (pp. 480–487). 1998. + +.. [4] Koller, D., & Friedman, N. (2009). + Probabilistic graphical models: principles and techniques. The MIT Press. + +.. [5] https://en.wikipedia.org/wiki/Causal_Markov_condition + +.. [6] https://en.wikipedia.org/wiki/Berkson%27s_paradox + +""" + +from collections import deque +from itertools import chain + +import networkx as nx +from networkx.utils import UnionFind, not_implemented_for + +__all__ = [ + "is_d_separator", + "is_minimal_d_separator", + "find_minimal_d_separator", +] + + +@not_implemented_for("undirected") +@nx._dispatchable +def is_d_separator(G, x, y, z): + """Return whether node sets `x` and `y` are d-separated by `z`. + + Parameters + ---------- + G : nx.DiGraph + A NetworkX DAG. + + x : node or set of nodes + First node or set of nodes in `G`. + + y : node or set of nodes + Second node or set of nodes in `G`. + + z : node or set of nodes + Potential separator (set of conditioning nodes in `G`). Can be empty set. + + Returns + ------- + b : bool + A boolean that is true if `x` is d-separated from `y` given `z` in `G`. + + Raises + ------ + NetworkXError + The *d-separation* test is commonly used on disjoint sets of + nodes in acyclic directed graphs. Accordingly, the algorithm + raises a :exc:`NetworkXError` if the node sets are not + disjoint or if the input graph is not a DAG. + + NodeNotFound + If any of the input nodes are not found in the graph, + a :exc:`NodeNotFound` exception is raised + + Notes + ----- + A d-separating set in a DAG is a set of nodes that + blocks all paths between the two sets. Nodes in `z` + block a path if they are part of the path and are not a collider, + or a descendant of a collider. Also colliders that are not in `z` + block a path. A collider structure along a path + is ``... -> c <- ...`` where ``c`` is the collider node. + + https://en.wikipedia.org/wiki/Bayesian_network#d-separation + """ + try: + x = {x} if x in G else x + y = {y} if y in G else y + z = {z} if z in G else z + + intersection = x & y or x & z or y & z + if intersection: + raise nx.NetworkXError( + f"The sets are not disjoint, with intersection {intersection}" + ) + + set_v = x | y | z + if set_v - G.nodes: + raise nx.NodeNotFound(f"The node(s) {set_v - G.nodes} are not found in G") + except TypeError: + raise nx.NodeNotFound("One of x, y, or z is not a node or a set of nodes in G") + + if not nx.is_directed_acyclic_graph(G): + raise nx.NetworkXError("graph should be directed acyclic") + + # contains -> and <-> edges from starting node T + forward_deque = deque([]) + forward_visited = set() + + # contains <- and - edges from starting node T + backward_deque = deque(x) + backward_visited = set() + + ancestors_or_z = set().union(*[nx.ancestors(G, node) for node in x]) | z | x + + while forward_deque or backward_deque: + if backward_deque: + node = backward_deque.popleft() + backward_visited.add(node) + if node in y: + return False + if node in z: + continue + + # add <- edges to backward deque + backward_deque.extend(G.pred[node].keys() - backward_visited) + # add -> edges to forward deque + forward_deque.extend(G.succ[node].keys() - forward_visited) + + if forward_deque: + node = forward_deque.popleft() + forward_visited.add(node) + if node in y: + return False + + # Consider if -> node <- is opened due to ancestor of node in z + if node in ancestors_or_z: + # add <- edges to backward deque + backward_deque.extend(G.pred[node].keys() - backward_visited) + if node not in z: + # add -> edges to forward deque + forward_deque.extend(G.succ[node].keys() - forward_visited) + + return True + + +@not_implemented_for("undirected") +@nx._dispatchable +def find_minimal_d_separator(G, x, y, *, included=None, restricted=None): + """Returns a minimal d-separating set between `x` and `y` if possible + + A d-separating set in a DAG is a set of nodes that blocks all + paths between the two sets of nodes, `x` and `y`. This function + constructs a d-separating set that is "minimal", meaning no nodes can + be removed without it losing the d-separating property for `x` and `y`. + If no d-separating sets exist for `x` and `y`, this returns `None`. + + In a DAG there may be more than one minimal d-separator between two + sets of nodes. Minimal d-separators are not always unique. This function + returns one minimal d-separator, or `None` if no d-separator exists. + + Uses the algorithm presented in [1]_. The complexity of the algorithm + is :math:`O(m)`, where :math:`m` stands for the number of edges in + the subgraph of G consisting of only the ancestors of `x` and `y`. + For full details, see [1]_. + + Parameters + ---------- + G : graph + A networkx DAG. + x : set | node + A node or set of nodes in the graph. + y : set | node + A node or set of nodes in the graph. + included : set | node | None + A node or set of nodes which must be included in the found separating set, + default is None, which means the empty set. + restricted : set | node | None + Restricted node or set of nodes to consider. Only these nodes can be in + the found separating set, default is None meaning all nodes in ``G``. + + Returns + ------- + z : set | None + The minimal d-separating set, if at least one d-separating set exists, + otherwise None. + + Raises + ------ + NetworkXError + Raises a :exc:`NetworkXError` if the input graph is not a DAG + or if node sets `x`, `y`, and `included` are not disjoint. + + NodeNotFound + If any of the input nodes are not found in the graph, + a :exc:`NodeNotFound` exception is raised. + + References + ---------- + .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding + minimal d-separators in linear time and applications." In + Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020. + """ + if not nx.is_directed_acyclic_graph(G): + raise nx.NetworkXError("graph should be directed acyclic") + + try: + x = {x} if x in G else x + y = {y} if y in G else y + + if included is None: + included = set() + elif included in G: + included = {included} + + if restricted is None: + restricted = set(G) + elif restricted in G: + restricted = {restricted} + + set_y = x | y | included | restricted + if set_y - G.nodes: + raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G") + except TypeError: + raise nx.NodeNotFound( + "One of x, y, included or restricted is not a node or set of nodes in G" + ) + + if not included <= restricted: + raise nx.NetworkXError( + f"Included nodes {included} must be in restricted nodes {restricted}" + ) + + intersection = x & y or x & included or y & included + if intersection: + raise nx.NetworkXError( + f"The sets x, y, included are not disjoint. Overlap: {intersection}" + ) + + nodeset = x | y | included + ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, node) for node in nodeset]) + + z_init = restricted & (ancestors_x_y_included - (x | y)) + + x_closure = _reachable(G, x, ancestors_x_y_included, z_init) + if x_closure & y: + return None + + z_updated = z_init & (x_closure | included) + y_closure = _reachable(G, y, ancestors_x_y_included, z_updated) + return z_updated & (y_closure | included) + + +@not_implemented_for("undirected") +@nx._dispatchable +def is_minimal_d_separator(G, x, y, z, *, included=None, restricted=None): + """Determine if `z` is a minimal d-separator for `x` and `y`. + + A d-separator, `z`, in a DAG is a set of nodes that blocks + all paths from nodes in set `x` to nodes in set `y`. + A minimal d-separator is a d-separator `z` such that removing + any subset of nodes makes it no longer a d-separator. + + Note: This function checks whether `z` is a d-separator AND is + minimal. One can use the function `is_d_separator` to only check if + `z` is a d-separator. See examples below. + + Parameters + ---------- + G : nx.DiGraph + A NetworkX DAG. + x : node | set + A node or set of nodes in the graph. + y : node | set + A node or set of nodes in the graph. + z : node | set + The node or set of nodes to check if it is a minimal d-separating set. + The function :func:`is_d_separator` is called inside this function + to verify that `z` is in fact a d-separator. + included : set | node | None + A node or set of nodes which must be included in the found separating set, + default is ``None``, which means the empty set. + restricted : set | node | None + Restricted node or set of nodes to consider. Only these nodes can be in + the found separating set, default is ``None`` meaning all nodes in ``G``. + + Returns + ------- + bool + Whether or not the set `z` is a minimal d-separator subject to + `restricted` nodes and `included` node constraints. + + Examples + -------- + >>> G = nx.path_graph([0, 1, 2, 3], create_using=nx.DiGraph) + >>> G.add_node(4) + >>> nx.is_minimal_d_separator(G, 0, 2, {1}) + True + >>> # since {1} is the minimal d-separator, {1, 3, 4} is not minimal + >>> nx.is_minimal_d_separator(G, 0, 2, {1, 3, 4}) + False + >>> # alternatively, if we only want to check that {1, 3, 4} is a d-separator + >>> nx.is_d_separator(G, 0, 2, {1, 3, 4}) + True + + Raises + ------ + NetworkXError + Raises a :exc:`NetworkXError` if the input graph is not a DAG. + + NodeNotFound + If any of the input nodes are not found in the graph, + a :exc:`NodeNotFound` exception is raised. + + References + ---------- + .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding + minimal d-separators in linear time and applications." In + Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020. + + Notes + ----- + This function works on verifying that a set is minimal and + d-separating between two nodes. Uses criterion (a), (b), (c) on + page 4 of [1]_. a) closure(`x`) and `y` are disjoint. b) `z` contains + all nodes from `included` and is contained in the `restricted` + nodes and in the union of ancestors of `x`, `y`, and `included`. + c) the nodes in `z` not in `included` are contained in both + closure(x) and closure(y). The closure of a set is the set of nodes + connected to the set by a directed path in G. + + The complexity is :math:`O(m)`, where :math:`m` stands for the + number of edges in the subgraph of G consisting of only the + ancestors of `x` and `y`. + + For full details, see [1]_. + """ + if not nx.is_directed_acyclic_graph(G): + raise nx.NetworkXError("graph should be directed acyclic") + + try: + x = {x} if x in G else x + y = {y} if y in G else y + z = {z} if z in G else z + + if included is None: + included = set() + elif included in G: + included = {included} + + if restricted is None: + restricted = set(G) + elif restricted in G: + restricted = {restricted} + + set_y = x | y | included | restricted + if set_y - G.nodes: + raise nx.NodeNotFound(f"The node(s) {set_y - G.nodes} are not found in G") + except TypeError: + raise nx.NodeNotFound( + "One of x, y, z, included or restricted is not a node or set of nodes in G" + ) + + if not included <= z: + raise nx.NetworkXError( + f"Included nodes {included} must be in proposed separating set z {x}" + ) + if not z <= restricted: + raise nx.NetworkXError( + f"Separating set {z} must be contained in restricted set {restricted}" + ) + + intersection = x.intersection(y) or x.intersection(z) or y.intersection(z) + if intersection: + raise nx.NetworkXError( + f"The sets are not disjoint, with intersection {intersection}" + ) + + nodeset = x | y | included + ancestors_x_y_included = nodeset.union(*[nx.ancestors(G, n) for n in nodeset]) + + # criterion (a) -- check that z is actually a separator + x_closure = _reachable(G, x, ancestors_x_y_included, z) + if x_closure & y: + return False + + # criterion (b) -- basic constraint; included and restricted already checked above + if not (z <= ancestors_x_y_included): + return False + + # criterion (c) -- check that z is minimal + y_closure = _reachable(G, y, ancestors_x_y_included, z) + if not ((z - included) <= (x_closure & y_closure)): + return False + return True + + +@not_implemented_for("undirected") +def _reachable(G, x, a, z): + """Modified Bayes-Ball algorithm for finding d-connected nodes. + + Find all nodes in `a` that are d-connected to those in `x` by + those in `z`. This is an implementation of the function + `REACHABLE` in [1]_ (which is itself a modification of the + Bayes-Ball algorithm [2]_) when restricted to DAGs. + + Parameters + ---------- + G : nx.DiGraph + A NetworkX DAG. + x : node | set + A node in the DAG, or a set of nodes. + a : node | set + A (set of) node(s) in the DAG containing the ancestors of `x`. + z : node | set + The node or set of nodes conditioned on when checking d-connectedness. + + Returns + ------- + w : set + The closure of `x` in `a` with respect to d-connectedness + given `z`. + + References + ---------- + .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding + minimal d-separators in linear time and applications." In + Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020. + + .. [2] Shachter, Ross D. "Bayes-ball: The rational pastime + (for determining irrelevance and requisite information in + belief networks and influence diagrams)." In Proceedings of the + Fourteenth Conference on Uncertainty in Artificial Intelligence + (UAI), (pp. 480–487). 1998. + """ + + def _pass(e, v, f, n): + """Whether a ball entering node `v` along edge `e` passes to `n` along `f`. + + Boolean function defined on page 6 of [1]_. + + Parameters + ---------- + e : bool + Directed edge by which the ball got to node `v`; `True` iff directed into `v`. + v : node + Node where the ball is. + f : bool + Directed edge connecting nodes `v` and `n`; `True` iff directed `n`. + n : node + Checking whether the ball passes to this node. + + Returns + ------- + b : bool + Whether the ball passes or not. + + References + ---------- + .. [1] van der Zander, Benito, and Maciej Liśkiewicz. "Finding + minimal d-separators in linear time and applications." In + Uncertainty in Artificial Intelligence, pp. 637-647. PMLR, 2020. + """ + is_element_of_A = n in a + # almost_definite_status = True # always true for DAGs; not so for RCGs + collider_if_in_Z = v not in z or (e and not f) + return is_element_of_A and collider_if_in_Z # and almost_definite_status + + queue = deque([]) + for node in x: + if bool(G.pred[node]): + queue.append((True, node)) + if bool(G.succ[node]): + queue.append((False, node)) + processed = queue.copy() + + while any(queue): + e, v = queue.popleft() + preds = ((False, n) for n in G.pred[v]) + succs = ((True, n) for n in G.succ[v]) + f_n_pairs = chain(preds, succs) + for f, n in f_n_pairs: + if (f, n) not in processed and _pass(e, v, f, n): + queue.append((f, n)) + processed.append((f, n)) + + return {w for (_, w) in processed} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dag.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dag.py new file mode 100644 index 0000000000000000000000000000000000000000..b7f07eca4f16c05391df34f0637132aae5da2866 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dag.py @@ -0,0 +1,1392 @@ +"""Algorithms for directed acyclic graphs (DAGs). + +Note that most of these functions are only guaranteed to work for DAGs. +In general, these functions do not check for acyclic-ness, so it is up +to the user to check for that. +""" + +import heapq +from collections import deque +from functools import partial +from itertools import chain, combinations, product, starmap +from math import gcd + +import networkx as nx +from networkx.utils import arbitrary_element, not_implemented_for, pairwise + +__all__ = [ + "descendants", + "ancestors", + "topological_sort", + "lexicographical_topological_sort", + "all_topological_sorts", + "topological_generations", + "is_directed_acyclic_graph", + "is_aperiodic", + "transitive_closure", + "transitive_closure_dag", + "transitive_reduction", + "antichains", + "dag_longest_path", + "dag_longest_path_length", + "dag_to_branching", +] + +chaini = chain.from_iterable + + +@nx._dispatchable +def descendants(G, source): + """Returns all nodes reachable from `source` in `G`. + + Parameters + ---------- + G : NetworkX Graph + source : node in `G` + + Returns + ------- + set() + The descendants of `source` in `G` + + Raises + ------ + NetworkXError + If node `source` is not in `G`. + + Examples + -------- + >>> DG = nx.path_graph(5, create_using=nx.DiGraph) + >>> sorted(nx.descendants(DG, 2)) + [3, 4] + + The `source` node is not a descendant of itself, but can be included manually: + + >>> sorted(nx.descendants(DG, 2) | {2}) + [2, 3, 4] + + See also + -------- + ancestors + """ + return {child for parent, child in nx.bfs_edges(G, source)} + + +@nx._dispatchable +def ancestors(G, source): + """Returns all nodes having a path to `source` in `G`. + + Parameters + ---------- + G : NetworkX Graph + source : node in `G` + + Returns + ------- + set() + The ancestors of `source` in `G` + + Raises + ------ + NetworkXError + If node `source` is not in `G`. + + Examples + -------- + >>> DG = nx.path_graph(5, create_using=nx.DiGraph) + >>> sorted(nx.ancestors(DG, 2)) + [0, 1] + + The `source` node is not an ancestor of itself, but can be included manually: + + >>> sorted(nx.ancestors(DG, 2) | {2}) + [0, 1, 2] + + See also + -------- + descendants + """ + return {child for parent, child in nx.bfs_edges(G, source, reverse=True)} + + +@nx._dispatchable +def has_cycle(G): + """Decides whether the directed graph has a cycle.""" + try: + # Feed the entire iterator into a zero-length deque. + deque(topological_sort(G), maxlen=0) + except nx.NetworkXUnfeasible: + return True + else: + return False + + +@nx._dispatchable +def is_directed_acyclic_graph(G): + """Returns True if the graph `G` is a directed acyclic graph (DAG) or + False if not. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + bool + True if `G` is a DAG, False otherwise + + Examples + -------- + Undirected graph:: + + >>> G = nx.Graph([(1, 2), (2, 3)]) + >>> nx.is_directed_acyclic_graph(G) + False + + Directed graph with cycle:: + + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + >>> nx.is_directed_acyclic_graph(G) + False + + Directed acyclic graph:: + + >>> G = nx.DiGraph([(1, 2), (2, 3)]) + >>> nx.is_directed_acyclic_graph(G) + True + + See also + -------- + topological_sort + """ + return G.is_directed() and not has_cycle(G) + + +@nx._dispatchable +def topological_generations(G): + """Stratifies a DAG into generations. + + A topological generation is node collection in which ancestors of a node in each + generation are guaranteed to be in a previous generation, and any descendants of + a node are guaranteed to be in a following generation. Nodes are guaranteed to + be in the earliest possible generation that they can belong to. + + Parameters + ---------- + G : NetworkX digraph + A directed acyclic graph (DAG) + + Yields + ------ + sets of nodes + Yields sets of nodes representing each generation. + + Raises + ------ + NetworkXError + Generations are defined for directed graphs only. If the graph + `G` is undirected, a :exc:`NetworkXError` is raised. + + NetworkXUnfeasible + If `G` is not a directed acyclic graph (DAG) no topological generations + exist and a :exc:`NetworkXUnfeasible` exception is raised. This can also + be raised if `G` is changed while the returned iterator is being processed + + RuntimeError + If `G` is changed while the returned iterator is being processed. + + Examples + -------- + >>> DG = nx.DiGraph([(2, 1), (3, 1)]) + >>> [sorted(generation) for generation in nx.topological_generations(DG)] + [[2, 3], [1]] + + Notes + ----- + The generation in which a node resides can also be determined by taking the + max-path-distance from the node to the farthest leaf node. That value can + be obtained with this function using `enumerate(topological_generations(G))`. + + See also + -------- + topological_sort + """ + if not G.is_directed(): + raise nx.NetworkXError("Topological sort not defined on undirected graphs.") + + multigraph = G.is_multigraph() + indegree_map = {v: d for v, d in G.in_degree() if d > 0} + zero_indegree = [v for v, d in G.in_degree() if d == 0] + + while zero_indegree: + this_generation = zero_indegree + zero_indegree = [] + for node in this_generation: + if node not in G: + raise RuntimeError("Graph changed during iteration") + for child in G.neighbors(node): + try: + indegree_map[child] -= len(G[node][child]) if multigraph else 1 + except KeyError as err: + raise RuntimeError("Graph changed during iteration") from err + if indegree_map[child] == 0: + zero_indegree.append(child) + del indegree_map[child] + yield this_generation + + if indegree_map: + raise nx.NetworkXUnfeasible( + "Graph contains a cycle or graph changed during iteration" + ) + + +@nx._dispatchable +def topological_sort(G): + """Returns a generator of nodes in topologically sorted order. + + A topological sort is a nonunique permutation of the nodes of a + directed graph such that an edge from u to v implies that u + appears before v in the topological sort order. This ordering is + valid only if the graph has no directed cycles. + + Parameters + ---------- + G : NetworkX digraph + A directed acyclic graph (DAG) + + Yields + ------ + nodes + Yields the nodes in topological sorted order. + + Raises + ------ + NetworkXError + Topological sort is defined for directed graphs only. If the graph `G` + is undirected, a :exc:`NetworkXError` is raised. + + NetworkXUnfeasible + If `G` is not a directed acyclic graph (DAG) no topological sort exists + and a :exc:`NetworkXUnfeasible` exception is raised. This can also be + raised if `G` is changed while the returned iterator is being processed + + RuntimeError + If `G` is changed while the returned iterator is being processed. + + Examples + -------- + To get the reverse order of the topological sort: + + >>> DG = nx.DiGraph([(1, 2), (2, 3)]) + >>> list(reversed(list(nx.topological_sort(DG)))) + [3, 2, 1] + + If your DiGraph naturally has the edges representing tasks/inputs + and nodes representing people/processes that initiate tasks, then + topological_sort is not quite what you need. You will have to change + the tasks to nodes with dependence reflected by edges. The result is + a kind of topological sort of the edges. This can be done + with :func:`networkx.line_graph` as follows: + + >>> list(nx.topological_sort(nx.line_graph(DG))) + [(1, 2), (2, 3)] + + Notes + ----- + This algorithm is based on a description and proof in + "Introduction to Algorithms: A Creative Approach" [1]_ . + + See also + -------- + is_directed_acyclic_graph, lexicographical_topological_sort + + References + ---------- + .. [1] Manber, U. (1989). + *Introduction to Algorithms - A Creative Approach.* Addison-Wesley. + """ + for generation in nx.topological_generations(G): + yield from generation + + +@nx._dispatchable +def lexicographical_topological_sort(G, key=None): + """Generate the nodes in the unique lexicographical topological sort order. + + Generates a unique ordering of nodes by first sorting topologically (for which there are often + multiple valid orderings) and then additionally by sorting lexicographically. + + A topological sort arranges the nodes of a directed graph so that the + upstream node of each directed edge precedes the downstream node. + It is always possible to find a solution for directed graphs that have no cycles. + There may be more than one valid solution. + + Lexicographical sorting is just sorting alphabetically. It is used here to break ties in the + topological sort and to determine a single, unique ordering. This can be useful in comparing + sort results. + + The lexicographical order can be customized by providing a function to the `key=` parameter. + The definition of the key function is the same as used in python's built-in `sort()`. + The function takes a single argument and returns a key to use for sorting purposes. + + Lexicographical sorting can fail if the node names are un-sortable. See the example below. + The solution is to provide a function to the `key=` argument that returns sortable keys. + + + Parameters + ---------- + G : NetworkX digraph + A directed acyclic graph (DAG) + + key : function, optional + A function of one argument that converts a node name to a comparison key. + It defines and resolves ambiguities in the sort order. Defaults to the identity function. + + Yields + ------ + nodes + Yields the nodes of G in lexicographical topological sort order. + + Raises + ------ + NetworkXError + Topological sort is defined for directed graphs only. If the graph `G` + is undirected, a :exc:`NetworkXError` is raised. + + NetworkXUnfeasible + If `G` is not a directed acyclic graph (DAG) no topological sort exists + and a :exc:`NetworkXUnfeasible` exception is raised. This can also be + raised if `G` is changed while the returned iterator is being processed + + RuntimeError + If `G` is changed while the returned iterator is being processed. + + TypeError + Results from un-sortable node names. + Consider using `key=` parameter to resolve ambiguities in the sort order. + + Examples + -------- + >>> DG = nx.DiGraph([(2, 1), (2, 5), (1, 3), (1, 4), (5, 4)]) + >>> list(nx.lexicographical_topological_sort(DG)) + [2, 1, 3, 5, 4] + >>> list(nx.lexicographical_topological_sort(DG, key=lambda x: -x)) + [2, 5, 1, 4, 3] + + The sort will fail for any graph with integer and string nodes. Comparison of integer to strings + is not defined in python. Is 3 greater or less than 'red'? + + >>> DG = nx.DiGraph([(1, "red"), (3, "red"), (1, "green"), (2, "blue")]) + >>> list(nx.lexicographical_topological_sort(DG)) + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'str' and 'int' + ... + + Incomparable nodes can be resolved using a `key` function. This example function + allows comparison of integers and strings by returning a tuple where the first + element is True for `str`, False otherwise. The second element is the node name. + This groups the strings and integers separately so they can be compared only among themselves. + + >>> key = lambda node: (isinstance(node, str), node) + >>> list(nx.lexicographical_topological_sort(DG, key=key)) + [1, 2, 3, 'blue', 'green', 'red'] + + Notes + ----- + This algorithm is based on a description and proof in + "Introduction to Algorithms: A Creative Approach" [1]_ . + + See also + -------- + topological_sort + + References + ---------- + .. [1] Manber, U. (1989). + *Introduction to Algorithms - A Creative Approach.* Addison-Wesley. + """ + if not G.is_directed(): + msg = "Topological sort not defined on undirected graphs." + raise nx.NetworkXError(msg) + + if key is None: + + def key(node): + return node + + nodeid_map = {n: i for i, n in enumerate(G)} + + def create_tuple(node): + return key(node), nodeid_map[node], node + + indegree_map = {v: d for v, d in G.in_degree() if d > 0} + # These nodes have zero indegree and ready to be returned. + zero_indegree = [create_tuple(v) for v, d in G.in_degree() if d == 0] + heapq.heapify(zero_indegree) + + while zero_indegree: + _, _, node = heapq.heappop(zero_indegree) + + if node not in G: + raise RuntimeError("Graph changed during iteration") + for _, child in G.edges(node): + try: + indegree_map[child] -= 1 + except KeyError as err: + raise RuntimeError("Graph changed during iteration") from err + if indegree_map[child] == 0: + try: + heapq.heappush(zero_indegree, create_tuple(child)) + except TypeError as err: + raise TypeError( + f"{err}\nConsider using `key=` parameter to resolve ambiguities in the sort order." + ) + del indegree_map[child] + + yield node + + if indegree_map: + msg = "Graph contains a cycle or graph changed during iteration" + raise nx.NetworkXUnfeasible(msg) + + +@not_implemented_for("undirected") +@nx._dispatchable +def all_topological_sorts(G): + """Returns a generator of _all_ topological sorts of the directed graph G. + + A topological sort is a nonunique permutation of the nodes such that an + edge from u to v implies that u appears before v in the topological sort + order. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + Yields + ------ + topological_sort_order : list + a list of nodes in `G`, representing one of the topological sort orders + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed + NetworkXUnfeasible + If `G` is not acyclic + + Examples + -------- + To enumerate all topological sorts of directed graph: + + >>> DG = nx.DiGraph([(1, 2), (2, 3), (2, 4)]) + >>> list(nx.all_topological_sorts(DG)) + [[1, 2, 4, 3], [1, 2, 3, 4]] + + Notes + ----- + Implements an iterative version of the algorithm given in [1]. + + References + ---------- + .. [1] Knuth, Donald E., Szwarcfiter, Jayme L. (1974). + "A Structured Program to Generate All Topological Sorting Arrangements" + Information Processing Letters, Volume 2, Issue 6, 1974, Pages 153-157, + ISSN 0020-0190, + https://doi.org/10.1016/0020-0190(74)90001-5. + Elsevier (North-Holland), Amsterdam + """ + if not G.is_directed(): + raise nx.NetworkXError("Topological sort not defined on undirected graphs.") + + # the names of count and D are chosen to match the global variables in [1] + # number of edges originating in a vertex v + count = dict(G.in_degree()) + # vertices with indegree 0 + D = deque([v for v, d in G.in_degree() if d == 0]) + # stack of first value chosen at a position k in the topological sort + bases = [] + current_sort = [] + + # do-while construct + while True: + assert all(count[v] == 0 for v in D) + + if len(current_sort) == len(G): + yield list(current_sort) + + # clean-up stack + while len(current_sort) > 0: + assert len(bases) == len(current_sort) + q = current_sort.pop() + + # "restores" all edges (q, x) + # NOTE: it is important to iterate over edges instead + # of successors, so count is updated correctly in multigraphs + for _, j in G.out_edges(q): + count[j] += 1 + assert count[j] >= 0 + # remove entries from D + while len(D) > 0 and count[D[-1]] > 0: + D.pop() + + # corresponds to a circular shift of the values in D + # if the first value chosen (the base) is in the first + # position of D again, we are done and need to consider the + # previous condition + D.appendleft(q) + if D[-1] == bases[-1]: + # all possible values have been chosen at current position + # remove corresponding marker + bases.pop() + else: + # there are still elements that have not been fixed + # at the current position in the topological sort + # stop removing elements, escape inner loop + break + + else: + if len(D) == 0: + raise nx.NetworkXUnfeasible("Graph contains a cycle.") + + # choose next node + q = D.pop() + # "erase" all edges (q, x) + # NOTE: it is important to iterate over edges instead + # of successors, so count is updated correctly in multigraphs + for _, j in G.out_edges(q): + count[j] -= 1 + assert count[j] >= 0 + if count[j] == 0: + D.append(j) + current_sort.append(q) + + # base for current position might _not_ be fixed yet + if len(bases) < len(current_sort): + bases.append(q) + + if len(bases) == 0: + break + + +@nx._dispatchable +def is_aperiodic(G): + """Returns True if `G` is aperiodic. + + A strongly connected directed graph is aperiodic if there is no integer ``k > 1`` + that divides the length of every cycle in the graph. + + This function requires the graph `G` to be strongly connected and will raise + an error if it's not. For graphs that are not strongly connected, you should + first identify their strongly connected components + (using :func:`~networkx.algorithms.components.strongly_connected_components`) + or attracting components + (using :func:`~networkx.algorithms.components.attracting_components`), + and then apply this function to those individual components. + + Parameters + ---------- + G : NetworkX DiGraph + A directed graph + + Returns + ------- + bool + True if the graph is aperiodic False otherwise + + Raises + ------ + NetworkXError + If `G` is not directed + NetworkXError + If `G` is not strongly connected + NetworkXPointlessConcept + If `G` has no nodes + + Examples + -------- + A graph consisting of one cycle, the length of which is 2. Therefore ``k = 2`` + divides the length of every cycle in the graph and thus the graph + is *not aperiodic*:: + + >>> DG = nx.DiGraph([(1, 2), (2, 1)]) + >>> nx.is_aperiodic(DG) + False + + A graph consisting of two cycles: one of length 2 and the other of length 3. + The cycle lengths are coprime, so there is no single value of k where ``k > 1`` + that divides each cycle length and therefore the graph is *aperiodic*:: + + >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1), (1, 4), (4, 1)]) + >>> nx.is_aperiodic(DG) + True + + A graph created from cycles of the same length can still be aperiodic since + the cycles can overlap and form new cycles of different lengths. For example, + the following graph contains a cycle ``[4, 2, 3, 1]`` of length 4, which is coprime + with the explicitly added cycles of length 3, so the graph is aperiodic:: + + >>> DG = nx.DiGraph() + >>> nx.add_cycle(DG, [1, 2, 3]) + >>> nx.add_cycle(DG, [2, 1, 4]) + >>> nx.is_aperiodic(DG) + True + + A single-node graph's aperiodicity depends on whether it has a self-loop: + it is aperiodic if a self-loop exists, and periodic otherwise:: + + >>> G = nx.DiGraph() + >>> G.add_node(1) + >>> nx.is_aperiodic(G) + False + >>> G.add_edge(1, 1) + >>> nx.is_aperiodic(G) + True + + A Markov chain can be modeled as a directed graph, with nodes representing + states and edges representing transitions with non-zero probability. + Aperiodicity is typically considered for irreducible Markov chains, + which are those that are *strongly connected* as graphs. + + The following Markov chain is irreducible and aperiodic, and thus + ergodic. It is guaranteed to have a unique stationary distribution:: + + >>> G = nx.DiGraph() + >>> nx.add_cycle(G, [1, 2, 3, 4]) + >>> G.add_edge(1, 3) + >>> nx.is_aperiodic(G) + True + + Reducible Markov chains can sometimes have a unique stationary distribution. + This occurs if the chain has exactly one closed communicating class and + that class itself is aperiodic (see [1]_). You can use + :func:`~networkx.algorithms.components.attracting_components` + to find these closed communicating classes:: + + >>> G = nx.DiGraph([(1, 3), (2, 3)]) + >>> nx.add_cycle(G, [3, 4, 5, 6]) + >>> nx.add_cycle(G, [3, 5, 6]) + >>> communicating_classes = list(nx.strongly_connected_components(G)) + >>> len(communicating_classes) + 3 + >>> closed_communicating_classes = list(nx.attracting_components(G)) + >>> len(closed_communicating_classes) + 1 + >>> nx.is_aperiodic(G.subgraph(closed_communicating_classes[0])) + True + + Notes + ----- + This uses the method outlined in [1]_, which runs in $O(m)$ time + given $m$ edges in `G`. + + References + ---------- + .. [1] Jarvis, J. P.; Shier, D. R. (1996), + "Graph-theoretic analysis of finite Markov chains," + in Shier, D. R.; Wallenius, K. T., Applied Mathematical Modeling: + A Multidisciplinary Approach, CRC Press. + """ + if not G.is_directed(): + raise nx.NetworkXError("is_aperiodic not defined for undirected graphs") + if len(G) == 0: + raise nx.NetworkXPointlessConcept("Graph has no nodes.") + if not nx.is_strongly_connected(G): + raise nx.NetworkXError("Graph is not strongly connected.") + s = arbitrary_element(G) + levels = {s: 0} + this_level = [s] + g = 0 + lev = 1 + while this_level: + next_level = [] + for u in this_level: + for v in G[u]: + if v in levels: # Non-Tree Edge + g = gcd(g, levels[u] - levels[v] + 1) + else: # Tree Edge + next_level.append(v) + levels[v] = lev + this_level = next_level + lev += 1 + return g == 1 + + +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def transitive_closure(G, reflexive=False): + """Returns transitive closure of a graph + + The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that + for all v, w in V there is an edge (v, w) in E+ if and only if there + is a path from v to w in G. + + Handling of paths from v to v has some flexibility within this definition. + A reflexive transitive closure creates a self-loop for the path + from v to v of length 0. The usual transitive closure creates a + self-loop only if a cycle exists (a path from v to v with length > 0). + We also allow an option for no self-loops. + + Parameters + ---------- + G : NetworkX Graph + A directed/undirected graph/multigraph. + reflexive : Bool or None, optional (default: False) + Determines when cycles create self-loops in the Transitive Closure. + If True, trivial cycles (length 0) create self-loops. The result + is a reflexive transitive closure of G. + If False (the default) non-trivial cycles create self-loops. + If None, self-loops are not created. + + Returns + ------- + NetworkX graph + The transitive closure of `G` + + Raises + ------ + NetworkXError + If `reflexive` not in `{None, True, False}` + + Examples + -------- + The treatment of trivial (i.e. length 0) cycles is controlled by the + `reflexive` parameter. + + Trivial (i.e. length 0) cycles do not create self-loops when + ``reflexive=False`` (the default):: + + >>> DG = nx.DiGraph([(1, 2), (2, 3)]) + >>> TC = nx.transitive_closure(DG, reflexive=False) + >>> TC.edges() + OutEdgeView([(1, 2), (1, 3), (2, 3)]) + + However, nontrivial (i.e. length greater than 0) cycles create self-loops + when ``reflexive=False`` (the default):: + + >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + >>> TC = nx.transitive_closure(DG, reflexive=False) + >>> TC.edges() + OutEdgeView([(1, 2), (1, 3), (1, 1), (2, 3), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) + + Trivial cycles (length 0) create self-loops when ``reflexive=True``:: + + >>> DG = nx.DiGraph([(1, 2), (2, 3)]) + >>> TC = nx.transitive_closure(DG, reflexive=True) + >>> TC.edges() + OutEdgeView([(1, 2), (1, 1), (1, 3), (2, 3), (2, 2), (3, 3)]) + + And the third option is not to create self-loops at all when ``reflexive=None``:: + + >>> DG = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + >>> TC = nx.transitive_closure(DG, reflexive=None) + >>> TC.edges() + OutEdgeView([(1, 2), (1, 3), (2, 3), (2, 1), (3, 1), (3, 2)]) + + References + ---------- + .. [1] https://www.ics.uci.edu/~eppstein/PADS/PartialOrder.py + """ + TC = G.copy() + + if reflexive not in {None, True, False}: + raise nx.NetworkXError("Incorrect value for the parameter `reflexive`") + + for v in G: + if reflexive is None: + TC.add_edges_from((v, u) for u in nx.descendants(G, v) if u not in TC[v]) + elif reflexive is True: + TC.add_edges_from( + (v, u) for u in nx.descendants(G, v) | {v} if u not in TC[v] + ) + elif reflexive is False: + TC.add_edges_from((v, e[1]) for e in nx.edge_bfs(G, v) if e[1] not in TC[v]) + + return TC + + +@not_implemented_for("undirected") +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def transitive_closure_dag(G, topo_order=None): + """Returns the transitive closure of a directed acyclic graph. + + This function is faster than the function `transitive_closure`, but fails + if the graph has a cycle. + + The transitive closure of G = (V,E) is a graph G+ = (V,E+) such that + for all v, w in V there is an edge (v, w) in E+ if and only if there + is a non-null path from v to w in G. + + Parameters + ---------- + G : NetworkX DiGraph + A directed acyclic graph (DAG) + + topo_order: list or tuple, optional + A topological order for G (if None, the function will compute one) + + Returns + ------- + NetworkX DiGraph + The transitive closure of `G` + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed + NetworkXUnfeasible + If `G` has a cycle + + Examples + -------- + >>> DG = nx.DiGraph([(1, 2), (2, 3)]) + >>> TC = nx.transitive_closure_dag(DG) + >>> TC.edges() + OutEdgeView([(1, 2), (1, 3), (2, 3)]) + + Notes + ----- + This algorithm is probably simple enough to be well-known but I didn't find + a mention in the literature. + """ + if topo_order is None: + topo_order = list(topological_sort(G)) + + TC = G.copy() + + # idea: traverse vertices following a reverse topological order, connecting + # each vertex to its descendants at distance 2 as we go + for v in reversed(topo_order): + TC.add_edges_from((v, u) for u in nx.descendants_at_distance(TC, v, 2)) + + return TC + + +@not_implemented_for("undirected") +@nx._dispatchable(returns_graph=True) +def transitive_reduction(G): + """Returns transitive reduction of a directed graph + + The transitive reduction of G = (V,E) is a graph G- = (V,E-) such that + for all v,w in V there is an edge (v,w) in E- if and only if (v,w) is + in E and there is no path from v to w in G with length greater than 1. + + Parameters + ---------- + G : NetworkX DiGraph + A directed acyclic graph (DAG) + + Returns + ------- + NetworkX DiGraph + The transitive reduction of `G` + + Raises + ------ + NetworkXError + If `G` is not a directed acyclic graph (DAG) transitive reduction is + not uniquely defined and a :exc:`NetworkXError` exception is raised. + + Examples + -------- + To perform transitive reduction on a DiGraph: + + >>> DG = nx.DiGraph([(1, 2), (2, 3), (1, 3)]) + >>> TR = nx.transitive_reduction(DG) + >>> list(TR.edges) + [(1, 2), (2, 3)] + + To avoid unnecessary data copies, this implementation does not return a + DiGraph with node/edge data. + To perform transitive reduction on a DiGraph and transfer node/edge data: + + >>> DG = nx.DiGraph() + >>> DG.add_edges_from([(1, 2), (2, 3), (1, 3)], color="red") + >>> TR = nx.transitive_reduction(DG) + >>> TR.add_nodes_from(DG.nodes(data=True)) + >>> TR.add_edges_from((u, v, DG.edges[u, v]) for u, v in TR.edges) + >>> list(TR.edges(data=True)) + [(1, 2, {'color': 'red'}), (2, 3, {'color': 'red'})] + + References + ---------- + https://en.wikipedia.org/wiki/Transitive_reduction + + """ + if not is_directed_acyclic_graph(G): + msg = "Directed Acyclic Graph required for transitive_reduction" + raise nx.NetworkXError(msg) + TR = nx.DiGraph() + TR.add_nodes_from(G.nodes()) + descendants = {} + # count before removing set stored in descendants + check_count = dict(G.in_degree) + for u in G: + u_nbrs = set(G[u]) + for v in G[u]: + if v in u_nbrs: + if v not in descendants: + descendants[v] = {y for x, y in nx.dfs_edges(G, v)} + u_nbrs -= descendants[v] + check_count[v] -= 1 + if check_count[v] == 0: + del descendants[v] + TR.add_edges_from((u, v) for v in u_nbrs) + return TR + + +@not_implemented_for("undirected") +@nx._dispatchable +def antichains(G, topo_order=None): + """Generates antichains from a directed acyclic graph (DAG). + + An antichain is a subset of a partially ordered set such that any + two elements in the subset are incomparable. + + Parameters + ---------- + G : NetworkX DiGraph + A directed acyclic graph (DAG) + + topo_order: list or tuple, optional + A topological order for G (if None, the function will compute one) + + Yields + ------ + antichain : list + a list of nodes in `G` representing an antichain + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed + + NetworkXUnfeasible + If `G` contains a cycle + + Examples + -------- + >>> DG = nx.DiGraph([(1, 2), (1, 3)]) + >>> list(nx.antichains(DG)) + [[], [3], [2], [2, 3], [1]] + + Notes + ----- + This function was originally developed by Peter Jipsen and Franco Saliola + for the SAGE project. It's included in NetworkX with permission from the + authors. Original SAGE code at: + + https://github.com/sagemath/sage/blob/master/src/sage/combinat/posets/hasse_diagram.py + + References + ---------- + .. [1] Free Lattices, by R. Freese, J. Jezek and J. B. Nation, + AMS, Vol 42, 1995, p. 226. + """ + if topo_order is None: + topo_order = list(nx.topological_sort(G)) + + TC = nx.transitive_closure_dag(G, topo_order) + antichains_stacks = [([], list(reversed(topo_order)))] + + while antichains_stacks: + (antichain, stack) = antichains_stacks.pop() + # Invariant: + # - the elements of antichain are independent + # - the elements of stack are independent from those of antichain + yield antichain + while stack: + x = stack.pop() + new_antichain = antichain + [x] + new_stack = [t for t in stack if not ((t in TC[x]) or (x in TC[t]))] + antichains_stacks.append((new_antichain, new_stack)) + + +@not_implemented_for("undirected") +@nx._dispatchable(edge_attrs={"weight": "default_weight"}) +def dag_longest_path(G, weight="weight", default_weight=1, topo_order=None): + """Returns the longest path in a directed acyclic graph (DAG). + + If `G` has edges with `weight` attribute the edge data are used as + weight values. + + Parameters + ---------- + G : NetworkX DiGraph + A directed acyclic graph (DAG) + + weight : str, optional + Edge data key to use for weight + + default_weight : int, optional + The weight of edges that do not have a weight attribute + + topo_order: list or tuple, optional + A topological order for `G` (if None, the function will compute one) + + Returns + ------- + list + Longest path + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed + + Examples + -------- + >>> DG = nx.DiGraph( + ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})] + ... ) + >>> list(nx.all_simple_paths(DG, 0, 2)) + [[0, 1, 2], [0, 2]] + >>> nx.dag_longest_path(DG) + [0, 1, 2] + >>> nx.dag_longest_path(DG, weight="cost") + [0, 2] + + In the case where multiple valid topological orderings exist, `topo_order` + can be used to specify a specific ordering: + + >>> DG = nx.DiGraph([(0, 1), (0, 2)]) + >>> sorted(nx.all_topological_sorts(DG)) # Valid topological orderings + [[0, 1, 2], [0, 2, 1]] + >>> nx.dag_longest_path(DG, topo_order=[0, 1, 2]) + [0, 1] + >>> nx.dag_longest_path(DG, topo_order=[0, 2, 1]) + [0, 2] + + See also + -------- + dag_longest_path_length + + """ + if not G: + return [] + + if topo_order is None: + topo_order = nx.topological_sort(G) + + dist = {} # stores {v : (length, u)} + for v in topo_order: + us = [ + ( + dist[u][0] + + ( + max(data.values(), key=lambda x: x.get(weight, default_weight)) + if G.is_multigraph() + else data + ).get(weight, default_weight), + u, + ) + for u, data in G.pred[v].items() + ] + + # Use the best predecessor if there is one and its distance is + # non-negative, otherwise terminate. + maxu = max(us, key=lambda x: x[0]) if us else (0, v) + dist[v] = maxu if maxu[0] >= 0 else (0, v) + + u = None + v = max(dist, key=lambda x: dist[x][0]) + path = [] + while u != v: + path.append(v) + u = v + v = dist[v][1] + + path.reverse() + return path + + +@not_implemented_for("undirected") +@nx._dispatchable(edge_attrs={"weight": "default_weight"}) +def dag_longest_path_length(G, weight="weight", default_weight=1): + """Returns the longest path length in a DAG + + Parameters + ---------- + G : NetworkX DiGraph + A directed acyclic graph (DAG) + + weight : string, optional + Edge data key to use for weight + + default_weight : int, optional + The weight of edges that do not have a weight attribute + + Returns + ------- + int + Longest path length + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed + + Examples + -------- + >>> DG = nx.DiGraph( + ... [(0, 1, {"cost": 1}), (1, 2, {"cost": 1}), (0, 2, {"cost": 42})] + ... ) + >>> list(nx.all_simple_paths(DG, 0, 2)) + [[0, 1, 2], [0, 2]] + >>> nx.dag_longest_path_length(DG) + 2 + >>> nx.dag_longest_path_length(DG, weight="cost") + 42 + + See also + -------- + dag_longest_path + """ + path = nx.dag_longest_path(G, weight, default_weight) + path_length = 0 + if G.is_multigraph(): + for u, v in pairwise(path): + i = max(G[u][v], key=lambda x: G[u][v][x].get(weight, default_weight)) + path_length += G[u][v][i].get(weight, default_weight) + else: + for u, v in pairwise(path): + path_length += G[u][v].get(weight, default_weight) + + return path_length + + +@nx._dispatchable +def root_to_leaf_paths(G): + """Yields root-to-leaf paths in a directed acyclic graph. + + `G` must be a directed acyclic graph. If not, the behavior of this + function is undefined. A "root" in this graph is a node of in-degree + zero and a "leaf" a node of out-degree zero. + + When invoked, this function iterates over each path from any root to + any leaf. A path is a list of nodes. + + """ + roots = (v for v, d in G.in_degree() if d == 0) + leaves = (v for v, d in G.out_degree() if d == 0) + all_paths = partial(nx.all_simple_paths, G) + # TODO In Python 3, this would be better as `yield from ...`. + return chaini(starmap(all_paths, product(roots, leaves))) + + +@not_implemented_for("multigraph") +@not_implemented_for("undirected") +@nx._dispatchable(returns_graph=True) +def dag_to_branching(G): + """Returns a branching representing all (overlapping) paths from + root nodes to leaf nodes in the given directed acyclic graph. + + As described in :mod:`networkx.algorithms.tree.recognition`, a + *branching* is a directed forest in which each node has at most one + parent. In other words, a branching is a disjoint union of + *arborescences*. For this function, each node of in-degree zero in + `G` becomes a root of one of the arborescences, and there will be + one leaf node for each distinct path from that root to a leaf node + in `G`. + + Each node `v` in `G` with *k* parents becomes *k* distinct nodes in + the returned branching, one for each parent, and the sub-DAG rooted + at `v` is duplicated for each copy. The algorithm then recurses on + the children of each copy of `v`. + + Parameters + ---------- + G : NetworkX graph + A directed acyclic graph. + + Returns + ------- + DiGraph + The branching in which there is a bijection between root-to-leaf + paths in `G` (in which multiple paths may share the same leaf) + and root-to-leaf paths in the branching (in which there is a + unique path from a root to a leaf). + + Each node has an attribute 'source' whose value is the original + node to which this node corresponds. No other graph, node, or + edge attributes are copied into this new graph. + + Raises + ------ + NetworkXNotImplemented + If `G` is not directed, or if `G` is a multigraph. + + HasACycle + If `G` is not acyclic. + + Examples + -------- + To examine which nodes in the returned branching were produced by + which original node in the directed acyclic graph, we can collect + the mapping from source node to new nodes into a dictionary. For + example, consider the directed diamond graph:: + + >>> from collections import defaultdict + >>> from operator import itemgetter + >>> + >>> G = nx.DiGraph(nx.utils.pairwise("abd")) + >>> G.add_edges_from(nx.utils.pairwise("acd")) + >>> B = nx.dag_to_branching(G) + >>> + >>> sources = defaultdict(set) + >>> for v, source in B.nodes(data="source"): + ... sources[source].add(v) + >>> len(sources["a"]) + 1 + >>> len(sources["d"]) + 2 + + To copy node attributes from the original graph to the new graph, + you can use a dictionary like the one constructed in the above + example:: + + >>> for source, nodes in sources.items(): + ... for v in nodes: + ... B.nodes[v].update(G.nodes[source]) + + Notes + ----- + This function is not idempotent in the sense that the node labels in + the returned branching may be uniquely generated each time the + function is invoked. In fact, the node labels may not be integers; + in order to relabel the nodes to be more readable, you can use the + :func:`networkx.convert_node_labels_to_integers` function. + + The current implementation of this function uses + :func:`networkx.prefix_tree`, so it is subject to the limitations of + that function. + + """ + if has_cycle(G): + msg = "dag_to_branching is only defined for acyclic graphs" + raise nx.HasACycle(msg) + paths = root_to_leaf_paths(G) + B = nx.prefix_tree(paths) + # Remove the synthetic `root`(0) and `NIL`(-1) nodes from the tree + B.remove_node(0) + B.remove_node(-1) + return B + + +@not_implemented_for("undirected") +@nx._dispatchable +def v_structures(G): + """Yields 3-node tuples that represent the v-structures in `G`. + + Colliders are triples in the directed acyclic graph (DAG) where two parent nodes + point to the same child node. V-structures are colliders where the two parent + nodes are not adjacent. In a causal graph setting, the parents do not directly + depend on each other, but conditioning on the child node provides an association. + + Parameters + ---------- + G : graph + A networkx `~networkx.DiGraph`. + + Yields + ------ + A 3-tuple representation of a v-structure + Each v-structure is a 3-tuple with the parent, collider, and other parent. + + Raises + ------ + NetworkXNotImplemented + If `G` is an undirected graph. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)]) + >>> nx.is_directed_acyclic_graph(G) + True + >>> list(nx.dag.v_structures(G)) + [(0, 4, 2), (0, 5, 1), (4, 5, 1)] + + See Also + -------- + colliders + + Notes + ----- + This function was written to be used on DAGs, however it works on cyclic graphs + too. Since colliders are referred to in the cyclic causal graph literature + [2]_ we allow cyclic graphs in this function. It is suggested that you test if + your input graph is acyclic as in the example if you want that property. + + References + ---------- + .. [1] `Pearl's PRIMER `_ + Ch-2 page 50: v-structures def. + .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013) + "Discovering cyclic causal models with latent variables: + a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth + Conference on Uncertainty in Artificial Intelligence, pg 301–310, + `doi:10.5555/3023638.3023669 `_ + """ + for p1, c, p2 in colliders(G): + if not (G.has_edge(p1, p2) or G.has_edge(p2, p1)): + yield (p1, c, p2) + + +@not_implemented_for("undirected") +@nx._dispatchable +def colliders(G): + """Yields 3-node tuples that represent the colliders in `G`. + + In a Directed Acyclic Graph (DAG), if you have three nodes A, B, and C, and + there are edges from A to C and from B to C, then C is a collider [1]_ . In + a causal graph setting, this means that both events A and B are "causing" C, + and conditioning on C provide an association between A and B even if + no direct causal relationship exists between A and B. + + Parameters + ---------- + G : graph + A networkx `~networkx.DiGraph`. + + Yields + ------ + A 3-tuple representation of a collider + Each collider is a 3-tuple with the parent, collider, and other parent. + + Raises + ------ + NetworkXNotImplemented + If `G` is an undirected graph. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (0, 4), (3, 1), (2, 4), (0, 5), (4, 5), (1, 5)]) + >>> nx.is_directed_acyclic_graph(G) + True + >>> list(nx.dag.colliders(G)) + [(0, 4, 2), (0, 5, 4), (0, 5, 1), (4, 5, 1)] + + See Also + -------- + v_structures + + Notes + ----- + This function was written to be used on DAGs, however it works on cyclic graphs + too. Since colliders are referred to in the cyclic causal graph literature + [2]_ we allow cyclic graphs in this function. It is suggested that you test if + your input graph is acyclic as in the example if you want that property. + + References + ---------- + .. [1] `Wikipedia: Collider in causal graphs `_ + .. [2] A Hyttinen, P.O. Hoyer, F. Eberhardt, M J ̈arvisalo, (2013) + "Discovering cyclic causal models with latent variables: + a general SAT-based procedure", UAI'13: Proceedings of the Twenty-Ninth + Conference on Uncertainty in Artificial Intelligence, pg 301–310, + `doi:10.5555/3023638.3023669 `_ + """ + for node in G.nodes: + for p1, p2 in combinations(G.predecessors(node), 2): + yield (p1, node, p2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_measures.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_measures.py new file mode 100644 index 0000000000000000000000000000000000000000..728cc5f1c2af07eaeb8e96a91603be231fa69230 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_measures.py @@ -0,0 +1,1095 @@ +"""Graph diameter, radius, eccentricity and other properties.""" + +import math + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "eccentricity", + "diameter", + "harmonic_diameter", + "radius", + "periphery", + "center", + "barycenter", + "resistance_distance", + "kemeny_constant", + "effective_graph_resistance", +] + + +def _extrema_bounding(G, compute="diameter", weight=None): + """Compute requested extreme distance metric of undirected graph G + + Computation is based on smart lower and upper bounds, and in practice + linear in the number of nodes, rather than quadratic (except for some + border cases such as complete graphs or circle shaped graphs). + + Parameters + ---------- + G : NetworkX graph + An undirected graph + + compute : string denoting the requesting metric + "diameter" for the maximal eccentricity value, + "radius" for the minimal eccentricity value, + "periphery" for the set of nodes with eccentricity equal to the diameter, + "center" for the set of nodes with eccentricity equal to the radius, + "eccentricities" for the maximum distance from each node to all other nodes in G + + weight : string, function, or None + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + value : value of the requested metric + int for "diameter" and "radius" or + list of nodes for "center" and "periphery" or + dictionary of eccentricity values keyed by node for "eccentricities" + + Raises + ------ + NetworkXError + If the graph consists of multiple components + ValueError + If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities". + + Notes + ----- + This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_. + + References + ---------- + .. [1] F. W. Takes, W. A. Kosters, + "Determining the diameter of small world networks." + Proceedings of the 20th ACM international conference on Information and + knowledge management, 2011 + https://dl.acm.org/doi/abs/10.1145/2063576.2063748 + .. [2] F. W. Takes, W. A. Kosters, + "Computing the Eccentricity Distribution of Large Graphs." + Algorithms, 2013 + https://www.mdpi.com/1999-4893/6/1/100 + .. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes, + "Fast diameter and radius BFS-based computation in (weakly connected) + real-world graphs: With an application to the six degrees of separation + games." + Theoretical Computer Science, 2015 + https://www.sciencedirect.com/science/article/pii/S0304397515001644 + """ + # init variables + degrees = dict(G.degree()) # start with the highest degree node + minlowernode = max(degrees, key=degrees.get) + N = len(degrees) # number of nodes + # alternate between smallest lower and largest upper bound + high = False + # status variables + ecc_lower = dict.fromkeys(G, 0) + ecc_upper = dict.fromkeys(G, math.inf) + candidates = set(G) + + # (re)set bound extremes + minlower = math.inf + maxlower = 0 + minupper = math.inf + maxupper = 0 + + # repeat the following until there are no more candidates + while candidates: + if high: + current = maxuppernode # select node with largest upper bound + else: + current = minlowernode # select node with smallest lower bound + high = not high + + # get distances from/to current node and derive eccentricity + dist = nx.shortest_path_length(G, source=current, weight=weight) + + if len(dist) != N: + msg = "Cannot compute metric because graph is not connected." + raise nx.NetworkXError(msg) + current_ecc = max(dist.values()) + + # print status update + # print ("ecc of " + str(current) + " (" + str(ecc_lower[current]) + "/" + # + str(ecc_upper[current]) + ", deg: " + str(dist[current]) + ") is " + # + str(current_ecc)) + # print(ecc_upper) + + # (re)set bound extremes + maxuppernode = None + minlowernode = None + + # update node bounds + for i in candidates: + # update eccentricity bounds + d = dist[i] + ecc_lower[i] = low = max(ecc_lower[i], max(d, (current_ecc - d))) + ecc_upper[i] = upp = min(ecc_upper[i], current_ecc + d) + + # update min/max values of lower and upper bounds + minlower = min(ecc_lower[i], minlower) + maxlower = max(ecc_lower[i], maxlower) + minupper = min(ecc_upper[i], minupper) + maxupper = max(ecc_upper[i], maxupper) + + # update candidate set + if compute == "diameter": + ruled_out = { + i + for i in candidates + if ecc_upper[i] <= maxlower and 2 * ecc_lower[i] >= maxupper + } + elif compute == "radius": + ruled_out = { + i + for i in candidates + if ecc_lower[i] >= minupper and ecc_upper[i] + 1 <= 2 * minlower + } + elif compute == "periphery": + ruled_out = { + i + for i in candidates + if ecc_upper[i] < maxlower + and (maxlower == maxupper or ecc_lower[i] > maxupper) + } + elif compute == "center": + ruled_out = { + i + for i in candidates + if ecc_lower[i] > minupper + and (minlower == minupper or ecc_upper[i] + 1 < 2 * minlower) + } + elif compute == "eccentricities": + ruled_out = set() + else: + msg = "compute must be one of 'diameter', 'radius', 'periphery', 'center', 'eccentricities'" + raise ValueError(msg) + + ruled_out.update(i for i in candidates if ecc_lower[i] == ecc_upper[i]) + candidates -= ruled_out + + # for i in ruled_out: + # print("removing %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"% + # (i,ecc_upper[i],maxlower,ecc_lower[i],maxupper)) + # print("node %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"% + # (4,ecc_upper[4],maxlower,ecc_lower[4],maxupper)) + # print("NODE 4: %g"%(ecc_upper[4] <= maxlower)) + # print("NODE 4: %g"%(2 * ecc_lower[4] >= maxupper)) + # print("NODE 4: %g"%(ecc_upper[4] <= maxlower + # and 2 * ecc_lower[4] >= maxupper)) + + # updating maxuppernode and minlowernode for selection in next round + for i in candidates: + if ( + minlowernode is None + or ( + ecc_lower[i] == ecc_lower[minlowernode] + and degrees[i] > degrees[minlowernode] + ) + or (ecc_lower[i] < ecc_lower[minlowernode]) + ): + minlowernode = i + + if ( + maxuppernode is None + or ( + ecc_upper[i] == ecc_upper[maxuppernode] + and degrees[i] > degrees[maxuppernode] + ) + or (ecc_upper[i] > ecc_upper[maxuppernode]) + ): + maxuppernode = i + + # print status update + # print (" min=" + str(minlower) + "/" + str(minupper) + + # " max=" + str(maxlower) + "/" + str(maxupper) + + # " candidates: " + str(len(candidates))) + # print("cand:",candidates) + # print("ecc_l",ecc_lower) + # print("ecc_u",ecc_upper) + # wait = input("press Enter to continue") + + # return the correct value of the requested metric + if compute == "diameter": + return maxlower + if compute == "radius": + return minupper + if compute == "periphery": + p = [v for v in G if ecc_lower[v] == maxlower] + return p + if compute == "center": + c = [v for v in G if ecc_upper[v] == minupper] + return c + if compute == "eccentricities": + return ecc_lower + return None + + +@nx._dispatchable(edge_attrs="weight") +def eccentricity(G, v=None, sp=None, weight=None): + """Returns the eccentricity of nodes in G. + + The eccentricity of a node v is the maximum distance from v to + all other nodes in G. + + Parameters + ---------- + G : NetworkX graph + A graph + + v : node, optional + Return value of specified node + + sp : dict of dicts, optional + All pairs shortest path lengths as a dictionary of dictionaries + + weight : string, function, or None (default=None) + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + ecc : dictionary + A dictionary of eccentricity values keyed by node. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> dict(nx.eccentricity(G)) + {1: 2, 2: 3, 3: 2, 4: 2, 5: 3} + + >>> dict( + ... nx.eccentricity(G, v=[1, 5]) + ... ) # This returns the eccentricity of node 1 & 5 + {1: 2, 5: 3} + + """ + # if v is None: # none, use entire graph + # nodes=G.nodes() + # elif v in G: # is v a single node + # nodes=[v] + # else: # assume v is a container of nodes + # nodes=v + order = G.order() + e = {} + for n in G.nbunch_iter(v): + if sp is None: + length = nx.shortest_path_length(G, source=n, weight=weight) + + L = len(length) + else: + try: + length = sp[n] + L = len(length) + except TypeError as err: + raise nx.NetworkXError('Format of "sp" is invalid.') from err + if L != order: + if G.is_directed(): + msg = ( + "Found infinite path length because the digraph is not" + " strongly connected" + ) + else: + msg = "Found infinite path length because the graph is not connected" + raise nx.NetworkXError(msg) + + e[n] = max(length.values()) + + if v in G: + return e[v] # return single value + return e + + +@nx._dispatchable(edge_attrs="weight") +def diameter(G, e=None, usebounds=False, weight=None): + """Returns the diameter of the graph G. + + The diameter is the maximum eccentricity. + + Parameters + ---------- + G : NetworkX graph + A graph + + e : eccentricity dictionary, optional + A precomputed dictionary of eccentricities. + + usebounds : bool, optional + If `True`, use extrema bounding (see Notes) when computing the diameter + for undirected graphs. Extrema bounding may accelerate the + distance calculation for some graphs. `usebounds` is ignored if `G` is + directed or if `e` is not `None`. Default is `False`. + + weight : string, function, or None + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + d : integer + Diameter of graph + + Notes + ----- + When ``usebounds=True``, the computation makes use of smart lower + and upper bounds and is often linear in the number of nodes, rather than + quadratic (except for some border cases such as complete graphs or circle + shaped-graphs). + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> nx.diameter(G) + 3 + + See Also + -------- + eccentricity + """ + if usebounds is True and e is None and not G.is_directed(): + return _extrema_bounding(G, compute="diameter", weight=weight) + if e is None: + e = eccentricity(G, weight=weight) + return max(e.values()) + + +@nx._dispatchable(edge_attrs="weight") +def harmonic_diameter(G, sp=None, *, weight=None): + """Returns the harmonic diameter of the graph G. + + The harmonic diameter of a graph is the harmonic mean of the distances + between all pairs of distinct vertices. Graphs that are not strongly + connected have infinite diameter and mean distance, making such + measures not useful. Restricting the diameter or mean distance to + finite distances yields paradoxical values (e.g., a perfect match + would have diameter one). The harmonic mean handles gracefully + infinite distances (e.g., a perfect match has harmonic diameter equal + to the number of vertices minus one), making it possible to assign a + meaningful value to all graphs. + + Note that in [1] the harmonic diameter is called "connectivity length": + however, "harmonic diameter" is a more standard name from the + theory of metric spaces. The name "harmonic mean distance" is perhaps + a more descriptive name, but is not used in the literature, so we use the + name "harmonic diameter" here. + + Parameters + ---------- + G : NetworkX graph + A graph + + sp : dict of dicts, optional + All-pairs shortest path lengths as a dictionary of dictionaries + + weight : string, function, or None (default=None) + If None, every edge has weight/distance 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If a function, the weight of an edge is the value returned by the function. + The function must accept exactly three positional arguments: + the two endpoints of an edge and the dictionary of edge attributes for + that edge. The function must return a number. + + Returns + ------- + hd : float + Harmonic diameter of graph + + References + ---------- + .. [1] Massimo Marchiori and Vito Latora, "Harmony in the small-world". + *Physica A: Statistical Mechanics and Its Applications* + 285(3-4), pages 539-546, 2000. + + """ + order = G.order() + + sum_invd = 0 + for n in G: + if sp is None: + length = nx.single_source_dijkstra_path_length(G, n, weight=weight) + else: + try: + length = sp[n] + L = len(length) + except TypeError as err: + raise nx.NetworkXError('Format of "sp" is invalid.') from err + + for d in length.values(): + # Note that this will skip the zero distance from n to itself, + # as it should be, but also zero-weight paths in weighted graphs. + if d != 0: + sum_invd += 1 / d + + if sum_invd != 0: + return order * (order - 1) / sum_invd + if order > 1: + return math.inf + return math.nan + + +@nx._dispatchable(edge_attrs="weight") +def periphery(G, e=None, usebounds=False, weight=None): + """Returns the periphery of the graph G. + + The periphery is the set of nodes with eccentricity equal to the diameter. + + Parameters + ---------- + G : NetworkX graph + A graph + + e : eccentricity dictionary, optional + A precomputed dictionary of eccentricities. + + usebounds : bool, optional + If `True`, use extrema bounding (see Notes) when computing the periphery + for undirected graphs. Extrema bounding may accelerate the + distance calculation for some graphs. `usebounds` is ignored if `G` is + directed or if `e` is not `None`. Default is `False`. + + weight : string, function, or None + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + p : list + List of nodes in periphery + + Notes + ----- + When ``usebounds=True``, the computation makes use of smart lower + and upper bounds and is often linear in the number of nodes, rather than + quadratic (except for some border cases such as complete graphs or circle + shaped-graphs). + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> nx.periphery(G) + [2, 5] + + See Also + -------- + barycenter + center + """ + if usebounds is True and e is None and not G.is_directed(): + return _extrema_bounding(G, compute="periphery", weight=weight) + if e is None: + e = eccentricity(G, weight=weight) + diameter = max(e.values()) + p = [v for v in e if e[v] == diameter] + return p + + +@nx._dispatchable(edge_attrs="weight") +def radius(G, e=None, usebounds=False, weight=None): + """Returns the radius of the graph G. + + The radius is the minimum eccentricity. + + Parameters + ---------- + G : NetworkX graph + A graph + + e : eccentricity dictionary, optional + A precomputed dictionary of eccentricities. + + usebounds : bool, optional + If `True`, use extrema bounding (see Notes) when computing the radius + for undirected graphs. Extrema bounding may accelerate the + distance calculation for some graphs. `usebounds` is ignored if `G` is + directed or if `e` is not `None`. Default is `False`. + + weight : string, function, or None + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + r : integer + Radius of graph + + Notes + ----- + When ``usebounds=True``, the computation makes use of smart lower + and upper bounds and is often linear in the number of nodes, rather than + quadratic (except for some border cases such as complete graphs or circle + shaped-graphs). + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> nx.radius(G) + 2 + + """ + if usebounds is True and e is None and not G.is_directed(): + return _extrema_bounding(G, compute="radius", weight=weight) + if e is None: + e = eccentricity(G, weight=weight) + return min(e.values()) + + +@nx._dispatchable(edge_attrs="weight") +def center(G, e=None, usebounds=False, weight=None): + """Returns the center of the graph G. + + The center is the set of nodes with eccentricity equal to radius. + + Parameters + ---------- + G : NetworkX graph + A graph + + e : eccentricity dictionary, optional + A precomputed dictionary of eccentricities. + + usebounds : bool, optional + If `True`, use extrema bounding (see Notes) when computing the center + for undirected graphs. Extrema bounding may accelerate the + distance calculation for some graphs. `usebounds` is ignored if `G` is + directed or if `e` is not `None`. Default is `False`. + + weight : string, function, or None + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + If this is None, every edge has weight/distance/cost 1. + + Weights stored as floating point values can lead to small round-off + errors in distances. Use integer weights to avoid this. + + Weights should be positive, since they are distances. + + Returns + ------- + c : list + List of nodes in center + + Notes + ----- + When ``usebounds=True``, the computation makes use of smart lower + and upper bounds and is often linear in the number of nodes, rather than + quadratic (except for some border cases such as complete graphs or circle + shaped-graphs). + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> list(nx.center(G)) + [1, 3, 4] + + See Also + -------- + :func:`~networkx.algorithms.tree.distance_measures.center` : tree center + barycenter + periphery + :func:`~networkx.algorithms.tree.distance_measures.centroid` : tree centroid + """ + if usebounds is True and e is None and not G.is_directed(): + return _extrema_bounding(G, compute="center", weight=weight) + if e is None and weight is None and not G.is_directed() and nx.is_tree(G): + return nx.tree.center(G) + if e is None: + e = eccentricity(G, weight=weight) + radius = min(e.values()) + p = [v for v in e if e[v] == radius] + return p + + +@nx._dispatchable(edge_attrs="weight", mutates_input={"attr": 2}) +def barycenter(G, weight=None, attr=None, sp=None): + r"""Calculate barycenter of a connected graph, optionally with edge weights. + + The :dfn:`barycenter` a + :func:`connected ` graph + :math:`G` is the subgraph induced by the set of its nodes :math:`v` + minimizing the objective function + + .. math:: + + \sum_{u \in V(G)} d_G(u, v), + + where :math:`d_G` is the (possibly weighted) :func:`path length + `. + The barycenter is also called the :dfn:`median`. See [West01]_, p. 78. + + Parameters + ---------- + G : :class:`networkx.Graph` + The connected graph :math:`G`. + weight : :class:`str`, optional + Passed through to + :func:`~networkx.algorithms.shortest_paths.generic.shortest_path_length`. + attr : :class:`str`, optional + If given, write the value of the objective function to each node's + `attr` attribute. Otherwise do not store the value. + sp : dict of dicts, optional + All pairs shortest path lengths as a dictionary of dictionaries + + Returns + ------- + list + Nodes of `G` that induce the barycenter of `G`. + + Raises + ------ + NetworkXNoPath + If `G` is disconnected. `G` may appear disconnected to + :func:`barycenter` if `sp` is given but is missing shortest path + lengths for any pairs. + ValueError + If `sp` and `weight` are both given. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> nx.barycenter(G) + [1, 3, 4] + + See Also + -------- + center + periphery + :func:`~networkx.algorithms.tree.distance_measures.centroid` : tree centroid + """ + if weight is None and attr is None and sp is None: + if not G.is_directed() and nx.is_tree(G): + return nx.tree.centroid(G) + + if sp is None: + sp = nx.shortest_path_length(G, weight=weight) + else: + sp = sp.items() + if weight is not None: + raise ValueError("Cannot use both sp, weight arguments together") + smallest, barycenter_vertices, n = float("inf"), [], len(G) + for v, dists in sp: + if len(dists) < n: + raise nx.NetworkXNoPath( + f"Input graph {G} is disconnected, so every induced subgraph " + "has infinite barycentricity." + ) + barycentricity = sum(dists.values()) + if attr is not None: + G.nodes[v][attr] = barycentricity + if barycentricity < smallest: + smallest = barycentricity + barycenter_vertices = [v] + elif barycentricity == smallest: + barycenter_vertices.append(v) + if attr is not None: + nx._clear_cache(G) + return barycenter_vertices + + +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def resistance_distance(G, nodeA=None, nodeB=None, weight=None, invert_weight=True): + """Returns the resistance distance between pairs of nodes in graph G. + + The resistance distance between two nodes of a graph is akin to treating + the graph as a grid of resistors with a resistance equal to the provided + weight [1]_, [2]_. + + If weight is not provided, then a weight of 1 is used for all edges. + + If two nodes are the same, the resistance distance is zero. + + Parameters + ---------- + G : NetworkX graph + A graph + + nodeA : node or None, optional (default=None) + A node within graph G. + If None, compute resistance distance using all nodes as source nodes. + + nodeB : node or None, optional (default=None) + A node within graph G. + If None, compute resistance distance using all nodes as target nodes. + + weight : string or None, optional (default=None) + The edge data key used to compute the resistance distance. + If None, then each edge has weight 1. + + invert_weight : boolean (default=True) + Proper calculation of resistance distance requires building the + Laplacian matrix with the reciprocal of the weight. Not required + if the weight is already inverted. Weight cannot be zero. + + Returns + ------- + rd : dict or float + If `nodeA` and `nodeB` are given, resistance distance between `nodeA` + and `nodeB`. If `nodeA` or `nodeB` is unspecified (the default), a + dictionary of nodes with resistance distances as the value. + + Raises + ------ + NetworkXNotImplemented + If `G` is a directed graph. + + NetworkXError + If `G` is not connected, or contains no nodes, + or `nodeA` is not in `G` or `nodeB` is not in `G`. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> round(nx.resistance_distance(G, 1, 3), 10) + 0.625 + + Notes + ----- + The implementation is based on Theorem A in [2]_. Self-loops are ignored. + Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights. + + References + ---------- + .. [1] Wikipedia + "Resistance distance." + https://en.wikipedia.org/wiki/Resistance_distance + .. [2] D. J. Klein and M. Randic. + Resistance distance. + J. of Math. Chem. 12:81-95, 1993. + """ + import numpy as np + + if len(G) == 0: + raise nx.NetworkXError("Graph G must contain at least one node.") + if not nx.is_connected(G): + raise nx.NetworkXError("Graph G must be strongly connected.") + if nodeA is not None and nodeA not in G: + raise nx.NetworkXError("Node A is not in graph G.") + if nodeB is not None and nodeB not in G: + raise nx.NetworkXError("Node B is not in graph G.") + + G = G.copy() + node_list = list(G) + + # Invert weights + if invert_weight and weight is not None: + if G.is_multigraph(): + for u, v, k, d in G.edges(keys=True, data=True): + d[weight] = 1 / d[weight] + else: + for u, v, d in G.edges(data=True): + d[weight] = 1 / d[weight] + + # Compute resistance distance using the Pseudo-inverse of the Laplacian + # Self-loops are ignored + L = nx.laplacian_matrix(G, weight=weight).todense() + Linv = np.linalg.pinv(L, hermitian=True) + + # Return relevant distances + if nodeA is not None and nodeB is not None: + i = node_list.index(nodeA) + j = node_list.index(nodeB) + return Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) + + elif nodeA is not None: + i = node_list.index(nodeA) + d = {} + for n in G: + j = node_list.index(n) + d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) + return d + + elif nodeB is not None: + j = node_list.index(nodeB) + d = {} + for n in G: + i = node_list.index(n) + d[n] = Linv.item(i, i) + Linv.item(j, j) - Linv.item(i, j) - Linv.item(j, i) + return d + + else: + d = {} + for n in G: + i = node_list.index(n) + d[n] = {} + for n2 in G: + j = node_list.index(n2) + d[n][n2] = ( + Linv.item(i, i) + + Linv.item(j, j) + - Linv.item(i, j) + - Linv.item(j, i) + ) + return d + + +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def effective_graph_resistance(G, weight=None, invert_weight=True): + """Returns the Effective graph resistance of G. + + Also known as the Kirchhoff index. + + The effective graph resistance is defined as the sum + of the resistance distance of every node pair in G [1]_. + + If weight is not provided, then a weight of 1 is used for all edges. + + The effective graph resistance of a disconnected graph is infinite. + + Parameters + ---------- + G : NetworkX graph + A graph + + weight : string or None, optional (default=None) + The edge data key used to compute the effective graph resistance. + If None, then each edge has weight 1. + + invert_weight : boolean (default=True) + Proper calculation of resistance distance requires building the + Laplacian matrix with the reciprocal of the weight. Not required + if the weight is already inverted. Weight cannot be zero. + + Returns + ------- + RG : float + The effective graph resistance of `G`. + + Raises + ------ + NetworkXNotImplemented + If `G` is a directed graph. + + NetworkXError + If `G` does not contain any nodes. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)]) + >>> round(nx.effective_graph_resistance(G), 10) + 10.25 + + Notes + ----- + The implementation is based on Theorem 2.2 in [2]_. Self-loops are ignored. + Multi-edges are contracted in one edge with weight equal to the harmonic sum of the weights. + + References + ---------- + .. [1] Wolfram + "Kirchhoff Index." + https://mathworld.wolfram.com/KirchhoffIndex.html + .. [2] W. Ellens, F. M. Spieksma, P. Van Mieghem, A. Jamakovic, R. E. Kooij. + Effective graph resistance. + Lin. Alg. Appl. 435:2491-2506, 2011. + """ + import numpy as np + + if len(G) == 0: + raise nx.NetworkXError("Graph G must contain at least one node.") + + # Disconnected graphs have infinite Effective graph resistance + if not nx.is_connected(G): + return float("inf") + + # Invert weights + G = G.copy() + if invert_weight and weight is not None: + if G.is_multigraph(): + for u, v, k, d in G.edges(keys=True, data=True): + d[weight] = 1 / d[weight] + else: + for u, v, d in G.edges(data=True): + d[weight] = 1 / d[weight] + + # Get Laplacian eigenvalues + mu = np.sort(nx.laplacian_spectrum(G, weight=weight)) + + # Compute Effective graph resistance based on spectrum of the Laplacian + # Self-loops are ignored + return float(np.sum(1 / mu[1:]) * G.number_of_nodes()) + + +@nx.utils.not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def kemeny_constant(G, *, weight=None): + """Returns the Kemeny constant of the given graph. + + The *Kemeny constant* (or Kemeny's constant) of a graph `G` + can be computed by regarding the graph as a Markov chain. + The Kemeny constant is then the expected number of time steps + to transition from a starting state i to a random destination state + sampled from the Markov chain's stationary distribution. + The Kemeny constant is independent of the chosen initial state [1]_. + + The Kemeny constant measures the time needed for spreading + across a graph. Low values indicate a closely connected graph + whereas high values indicate a spread-out graph. + + If weight is not provided, then a weight of 1 is used for all edges. + + Since `G` represents a Markov chain, the weights must be positive. + + Parameters + ---------- + G : NetworkX graph + + weight : string or None, optional (default=None) + The edge data key used to compute the Kemeny constant. + If None, then each edge has weight 1. + + Returns + ------- + float + The Kemeny constant of the graph `G`. + + Raises + ------ + NetworkXNotImplemented + If the graph `G` is directed. + + NetworkXError + If the graph `G` is not connected, or contains no nodes, + or has edges with negative weights. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> round(nx.kemeny_constant(G), 10) + 3.2 + + Notes + ----- + The implementation is based on equation (3.3) in [2]_. + Self-loops are allowed and indicate a Markov chain where + the state can remain the same. Multi-edges are contracted + in one edge with weight equal to the sum of the weights. + + References + ---------- + .. [1] Wikipedia + "Kemeny's constant." + https://en.wikipedia.org/wiki/Kemeny%27s_constant + .. [2] Lovász L. + Random walks on graphs: A survey. + Paul Erdös is Eighty, vol. 2, Bolyai Society, + Mathematical Studies, Keszthely, Hungary (1993), pp. 1-46 + """ + import numpy as np + import scipy as sp + + if len(G) == 0: + raise nx.NetworkXError("Graph G must contain at least one node.") + if not nx.is_connected(G): + raise nx.NetworkXError("Graph G must be connected.") + if nx.is_negatively_weighted(G, weight=weight): + raise nx.NetworkXError("The weights of graph G must be nonnegative.") + + # Compute matrix H = D^-1/2 A D^-1/2 + A = nx.adjacency_matrix(G, weight=weight) + n, m = A.shape + diags = A.sum(axis=1) + with np.errstate(divide="ignore"): + diags_sqrt = 1.0 / np.sqrt(diags) + diags_sqrt[np.isinf(diags_sqrt)] = 0 + DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(m, n)).tocsr() + H = DH @ (A @ DH) + + # Compute eigenvalues of H + eig = np.sort(sp.linalg.eigvalsh(H.todense())) + + # Compute the Kemeny constant + return float(np.sum(1 / (1 - eig[:-1]))) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_regular.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_regular.py new file mode 100644 index 0000000000000000000000000000000000000000..e97b0f843b91220f183f3df4554d5bb25619cfc5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/distance_regular.py @@ -0,0 +1,272 @@ +""" +======================= +Distance-regular graphs +======================= +""" + +from collections import defaultdict +from itertools import combinations_with_replacement +from math import log + +import networkx as nx +from networkx.utils import not_implemented_for + +from .distance_measures import diameter + +__all__ = [ + "is_distance_regular", + "is_strongly_regular", + "intersection_array", + "global_parameters", +] + + +@nx._dispatchable +def is_distance_regular(G): + """Returns True if the graph is distance regular, False otherwise. + + A connected graph G is distance-regular if for any nodes x,y + and any integers i,j=0,1,...,d (where d is the graph + diameter), the number of vertices at distance i from x and + distance j from y depends only on i,j and the graph distance + between x and y, independently of the choice of x and y. + + Parameters + ---------- + G: Networkx graph (undirected) + + Returns + ------- + bool + True if the graph is Distance Regular, False otherwise + + Examples + -------- + >>> G = nx.hypercube_graph(6) + >>> nx.is_distance_regular(G) + True + + See Also + -------- + intersection_array, global_parameters + + Notes + ----- + For undirected and simple graphs only + + References + ---------- + .. [1] Brouwer, A. E.; Cohen, A. M.; and Neumaier, A. + Distance-Regular Graphs. New York: Springer-Verlag, 1989. + .. [2] Weisstein, Eric W. "Distance-Regular Graph." + http://mathworld.wolfram.com/Distance-RegularGraph.html + + """ + try: + intersection_array(G) + return True + except nx.NetworkXError: + return False + + +def global_parameters(b, c): + """Returns global parameters for a given intersection array. + + Given a distance-regular graph G with diameter d and integers b_i, + c_i,i = 0,....,d such that for any 2 vertices x,y in G at a distance + i=d(x,y), there are exactly c_i neighbors of y at a distance of i-1 from x + and b_i neighbors of y at a distance of i+1 from x. + + Thus, a distance regular graph has the global parameters, + [[c_0,a_0,b_0],[c_1,a_1,b_1],......,[c_d,a_d,b_d]] for the + intersection array [b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d] + where a_i+b_i+c_i=k , k= degree of every vertex. + + Parameters + ---------- + b : list + + c : list + + Returns + ------- + iterable + An iterable over three tuples. + + Examples + -------- + >>> G = nx.dodecahedral_graph() + >>> b, c = nx.intersection_array(G) + >>> list(nx.global_parameters(b, c)) + [(0, 0, 3), (1, 0, 2), (1, 1, 1), (1, 1, 1), (2, 0, 1), (3, 0, 0)] + + References + ---------- + .. [1] Weisstein, Eric W. "Global Parameters." + From MathWorld--A Wolfram Web Resource. + http://mathworld.wolfram.com/GlobalParameters.html + + See Also + -------- + intersection_array + """ + return ((y, b[0] - x - y, x) for x, y in zip(b + [0], [0] + c)) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def intersection_array(G): + """Returns the intersection array of a distance-regular graph. + + Given a distance-regular graph G with integers b_i, c_i,i = 0,....,d + such that for any 2 vertices x,y in G at a distance i=d(x,y), there + are exactly c_i neighbors of y at a distance of i-1 from x and b_i + neighbors of y at a distance of i+1 from x. + + A distance regular graph's intersection array is given by, + [b_0,b_1,.....b_{d-1};c_1,c_2,.....c_d] + + Parameters + ---------- + G: Networkx graph (undirected) + + Returns + ------- + b,c: tuple of lists + + Examples + -------- + >>> G = nx.icosahedral_graph() + >>> nx.intersection_array(G) + ([5, 2, 1], [1, 2, 5]) + + References + ---------- + .. [1] Weisstein, Eric W. "Intersection Array." + From MathWorld--A Wolfram Web Resource. + http://mathworld.wolfram.com/IntersectionArray.html + + See Also + -------- + global_parameters + """ + # the input graph is very unlikely to be distance-regular: here are the + # number a(n) of connected simple graphs, and the number b(n) of + # distance-regular graphs among them: + # + # n | 1 2 3 4 5 6 7 8 9 10 + # -----+------------------------------------------------------------------ + # a(n) | 1 1 2 6 21 112 853 11117 261080 11716571 https://oeis.org/A001349 + # b(n) | 1 1 1 2 2 4 2 5 4 7 https://oeis.org/A241814 + # + # in light of this, let's compute shortest path lengths as we go instead of + # precomputing them all + # test for regular graph (all degrees must be equal) + if not nx.is_regular(G) or not nx.is_connected(G): + raise nx.NetworkXError("Graph is not distance regular.") + + path_length = defaultdict(dict) + bint = {} # 'b' intersection array + cint = {} # 'c' intersection array + + # see https://doi.org/10.1016/j.ejc.2004.07.004, Theorem 1.5, page 81: + # the diameter of a distance-regular graph is at most (8 log_2 n) / 3, + # so let's compute it as we go in the hope that we can stop early + diam = 0 + max_diameter_for_dr_graphs = (8 * log(len(G), 2)) / 3 + for u, v in combinations_with_replacement(G, 2): + # compute needed shortest path lengths + pl_u = path_length[u] + if v not in pl_u: + pl_u.update(nx.single_source_shortest_path_length(G, u)) + for x, distance in pl_u.items(): + path_length[x][u] = distance + + i = path_length[u][v] + diam = max(diam, i) + + # diameter too large: graph can't be distance-regular + if diam > max_diameter_for_dr_graphs: + raise nx.NetworkXError("Graph is not distance regular.") + + vnbrs = G[v] + # compute needed path lengths + for n in vnbrs: + pl_n = path_length[n] + if u not in pl_n: + pl_n.update(nx.single_source_shortest_path_length(G, n)) + for x, distance in pl_n.items(): + path_length[x][n] = distance + + # number of neighbors of v at a distance of i-1 from u + c = sum(1 for n in vnbrs if pl_u[n] == i - 1) + # number of neighbors of v at a distance of i+1 from u + b = sum(1 for n in vnbrs if pl_u[n] == i + 1) + # b, c are independent of u and v + if cint.get(i, c) != c or bint.get(i, b) != b: + raise nx.NetworkXError("Graph is not distance regular") + bint[i] = b + cint[i] = c + + return ( + [bint.get(j, 0) for j in range(diam)], + [cint.get(j + 1, 0) for j in range(diam)], + ) + + +# TODO There is a definition for directed strongly regular graphs. +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_strongly_regular(G): + """Returns True if and only if the given graph is strongly + regular. + + An undirected graph is *strongly regular* if + + * it is regular, + * each pair of adjacent vertices has the same number of neighbors in + common, + * each pair of nonadjacent vertices has the same number of neighbors + in common. + + Each strongly regular graph is a distance-regular graph. + Conversely, if a distance-regular graph has diameter two, then it is + a strongly regular graph. For more information on distance-regular + graphs, see :func:`is_distance_regular`. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + Returns + ------- + bool + Whether `G` is strongly regular. + + Examples + -------- + + The cycle graph on five vertices is strongly regular. It is + two-regular, each pair of adjacent vertices has no shared neighbors, + and each pair of nonadjacent vertices has one shared neighbor:: + + >>> G = nx.cycle_graph(5) + >>> nx.is_strongly_regular(G) + True + + """ + # Here is an alternate implementation based directly on the + # definition of strongly regular graphs: + # + # return (all_equal(G.degree().values()) + # and all_equal(len(common_neighbors(G, u, v)) + # for u, v in G.edges()) + # and all_equal(len(common_neighbors(G, u, v)) + # for u, v in non_edges(G))) + # + # We instead use the fact that a distance-regular graph of diameter + # two is strongly regular. + return is_distance_regular(G) and diameter(G) == 2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominance.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominance.py new file mode 100644 index 0000000000000000000000000000000000000000..6429498ad5a22c8423cf3f62a17d0f429c2d286a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominance.py @@ -0,0 +1,142 @@ +""" +Dominance algorithms. +""" + +from functools import reduce + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["immediate_dominators", "dominance_frontiers"] + + +@not_implemented_for("undirected") +@nx._dispatchable +def immediate_dominators(G, start): + """Returns the immediate dominators of all nodes of a directed graph. + + Parameters + ---------- + G : a DiGraph or MultiDiGraph + The graph where dominance is to be computed. + + start : node + The start node of dominance computation. + + Returns + ------- + idom : dict keyed by nodes + A dict containing the immediate dominators of each node reachable from + `start`, except for `start` itself. + + Raises + ------ + NetworkXNotImplemented + If `G` is undirected. + + NetworkXError + If `start` is not in `G`. + + Notes + ----- + The immediate dominators are the parents of their corresponding nodes in + the dominator tree. Every node reachable from `start` has an immediate + dominator, except for `start` itself. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)]) + >>> sorted(nx.immediate_dominators(G, 1).items()) + [(2, 1), (3, 1), (4, 3), (5, 1)] + + References + ---------- + .. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken. + "A simple, fast dominance algorithm." (2006). + https://hdl.handle.net/1911/96345 + .. [2] Lengauer, Thomas; Tarjan, Robert Endre (July 1979). + "A fast algorithm for finding dominators in a flowgraph". + ACM Transactions on Programming Languages and Systems. 1 (1): 121--141. + https://dl.acm.org/doi/10.1145/357062.357071 + """ + if start not in G: + raise nx.NetworkXError("start is not in G") + + idom = {start: None} + + order = list(nx.dfs_postorder_nodes(G, start)) + dfn = {u: i for i, u in enumerate(order)} + order.pop() + order.reverse() + + def intersect(u, v): + while u != v: + while dfn[u] < dfn[v]: + u = idom[u] + while dfn[u] > dfn[v]: + v = idom[v] + return u + + changed = True + while changed: + changed = False + for u in order: + new_idom = reduce(intersect, (v for v in G.pred[u] if v in idom)) + if u not in idom or idom[u] != new_idom: + idom[u] = new_idom + changed = True + + del idom[start] + return idom + + +@not_implemented_for("undirected") +@nx._dispatchable +def dominance_frontiers(G, start): + """Returns the dominance frontiers of all nodes of a directed graph. + + Parameters + ---------- + G : a DiGraph or MultiDiGraph + The graph where dominance is to be computed. + + start : node + The start node of dominance computation. + + Returns + ------- + df : dict keyed by nodes + A dict containing the dominance frontiers of each node reachable from + `start` as lists. + + Raises + ------ + NetworkXNotImplemented + If `G` is undirected. + + NetworkXError + If `start` is not in `G`. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 5), (3, 4), (4, 5)]) + >>> sorted((u, sorted(df)) for u, df in nx.dominance_frontiers(G, 1).items()) + [(1, []), (2, [5]), (3, [5]), (4, [5]), (5, [])] + + References + ---------- + .. [1] Cooper, Keith D., Harvey, Timothy J. and Kennedy, Ken. + "A simple, fast dominance algorithm." (2006). + https://hdl.handle.net/1911/96345 + """ + idom = nx.immediate_dominators(G, start) | {start: None} + + df = {u: set() for u in idom} + for u in idom: + if u == start or len(G.pred[u]) >= 2: + for v in G.pred[u]: + if v in idom: + while v != idom[u]: + df[v].add(u) + v = idom[v] + return df diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominating.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominating.py new file mode 100644 index 0000000000000000000000000000000000000000..41c102a5bc10805a67e8bf26a8348b19abf7fea2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/dominating.py @@ -0,0 +1,268 @@ +"""Functions for computing dominating sets in a graph.""" + +import math +from heapq import heappop, heappush +from itertools import chain, count + +import networkx as nx + +__all__ = [ + "dominating_set", + "is_dominating_set", + "connected_dominating_set", + "is_connected_dominating_set", +] + + +@nx._dispatchable +def dominating_set(G, start_with=None): + r"""Finds a dominating set for the graph G. + + A *dominating set* for a graph with node set *V* is a subset *D* of + *V* such that every node not in *D* is adjacent to at least one + member of *D* [1]_. + + Parameters + ---------- + G : NetworkX graph + + start_with : node (default=None) + Node to use as a starting point for the algorithm. + + Returns + ------- + D : set + A dominating set for G. + + Notes + ----- + This function is an implementation of algorithm 7 in [2]_ which + finds some dominating set, not necessarily the smallest one. + + See also + -------- + is_dominating_set + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Dominating_set + + .. [2] Abdol-Hossein Esfahanian. Connectivity Algorithms. + http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf + + """ + all_nodes = set(G) + if start_with is None: + start_with = nx.utils.arbitrary_element(all_nodes) + if start_with not in G: + raise nx.NetworkXError(f"node {start_with} is not in G") + dominating_set = {start_with} + dominated_nodes = set(G[start_with]) + remaining_nodes = all_nodes - dominated_nodes - dominating_set + while remaining_nodes: + # Choose an arbitrary node and determine its undominated neighbors. + v = remaining_nodes.pop() + undominated_nbrs = set(G[v]) - dominating_set + # Add the node to the dominating set and the neighbors to the + # dominated set. Finally, remove all of those nodes from the set + # of remaining nodes. + dominating_set.add(v) + dominated_nodes |= undominated_nbrs + remaining_nodes -= undominated_nbrs + return dominating_set + + +@nx._dispatchable +def is_dominating_set(G, nbunch): + """Checks if `nbunch` is a dominating set for `G`. + + A *dominating set* for a graph with node set *V* is a subset *D* of + *V* such that every node not in *D* is adjacent to at least one + member of *D* [1]_. + + Parameters + ---------- + G : NetworkX graph + + nbunch : iterable + An iterable of nodes in the graph `G`. + + Returns + ------- + dominating : bool + True if `nbunch` is a dominating set of `G`, false otherwise. + + See also + -------- + dominating_set + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Dominating_set + + """ + testset = {n for n in nbunch if n in G} + nbrs = set(chain.from_iterable(G[n] for n in testset)) + return len(set(G) - testset - nbrs) == 0 + + +@nx.utils.not_implemented_for("directed") +@nx._dispatchable +def connected_dominating_set(G): + """Returns a connected dominating set. + + A *dominating set* for a graph *G* with node set *V* is a subset *D* of *V* + such that every node not in *D* is adjacent to at least one member of *D* + [1]_. A *connected dominating set* is a dominating set *C* that induces a + connected subgraph of *G* [2]_. + Note that connected dominating sets are not unique in general and that there + may be other connected dominating sets. + + Parameters + ---------- + G : NewtorkX graph + Undirected connected graph. + + Returns + ------- + connected_dominating_set : set + A dominating set of nodes which induces a connected subgraph of G. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + NetworkXError + If G is disconnected. + + Examples + ________ + >>> G = nx.Graph( + ... [ + ... (1, 2), + ... (1, 3), + ... (1, 4), + ... (1, 5), + ... (1, 6), + ... (2, 7), + ... (3, 8), + ... (4, 9), + ... (5, 10), + ... (6, 11), + ... (7, 12), + ... (8, 12), + ... (9, 12), + ... (10, 12), + ... (11, 12), + ... ] + ... ) + >>> nx.connected_dominating_set(G) + {1, 2, 3, 4, 5, 6, 7} + + Notes + ----- + This function implements Algorithm I in its basic version as described + in [3]_. The idea behind the algorithm is the following: grow a tree *T*, + starting from a node with maximum degree. Throughout the growing process, + nonleaf nodes in *T* are our connected dominating set (CDS), leaf nodes in + *T* are marked as "seen" and nodes in G that are not yet in *T* are marked as + "unseen". We maintain a max-heap of all "seen" nodes, and track the number + of "unseen" neighbors for each node. At each step we pop the heap top -- a + "seen" (leaf) node with maximal number of "unseen" neighbors, add it to the + CDS and mark all its "unseen" neighbors as "seen". For each one of the newly + created "seen" nodes, we also decrement the number of "unseen" neighbors for + all its neighbors. The algorithm terminates when there are no more "unseen" + nodes. + Runtime complexity of this implementation is $O(|E|*log|V|)$ (amortized). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Dominating_set + .. [2] https://en.wikipedia.org/wiki/Connected_dominating_set + .. [3] Guha, S. and Khuller, S. + *Approximation Algorithms for Connected Dominating Sets*, + Algorithmica, 20, 374-387, 1998. + + """ + if len(G) == 0: + return set() + + if not nx.is_connected(G): + raise nx.NetworkXError("G must be a connected graph") + + if len(G) == 1: + return set(G) + + G_succ = G._adj # For speed-up + + # Use the count c to avoid comparing nodes + c = count() + + # Keep track of the number of unseen nodes adjacent to each node + unseen_degree = dict(G.degree) + + # Find node with highest degree and update its neighbors + (max_deg_node, max_deg) = max(unseen_degree.items(), key=lambda x: x[1]) + for nbr in G_succ[max_deg_node]: + unseen_degree[nbr] -= 1 + + # Initially all nodes except max_deg_node are unseen + unseen = set(G) - {max_deg_node} + + # We want a max-heap of the unseen-degree using heapq, which is a min-heap + # So we store the negative of the unseen-degree + seen = [(-max_deg, next(c), max_deg_node)] + + connected_dominating_set = set() + + # Main loop + while unseen: + (neg_deg, cnt, u) = heappop(seen) + # Check if u's unseen-degree changed while in the heap + if -neg_deg > unseen_degree[u]: + heappush(seen, (-unseen_degree[u], cnt, u)) + continue + # Mark all u's unseen neighbors as seen and add them to the heap + for v in G_succ[u]: + if v in unseen: + unseen.remove(v) + for nbr in G_succ[v]: + unseen_degree[nbr] -= 1 + heappush(seen, (-unseen_degree[v], next(c), v)) + # Add u to the dominating set + connected_dominating_set.add(u) + + return connected_dominating_set + + +@nx.utils.not_implemented_for("directed") +@nx._dispatchable +def is_connected_dominating_set(G, nbunch): + """Checks if `nbunch` is a connected dominating set for `G`. + + A *dominating set* for a graph *G* with node set *V* is a subset *D* of + *V* such that every node not in *D* is adjacent to at least one + member of *D* [1]_. A *connected dominating set* is a dominating + set *C* that induces a connected subgraph of *G* [2]_. + + Parameters + ---------- + G : NetworkX graph + Undirected graph. + + nbunch : iterable + An iterable of nodes in the graph `G`. + + Returns + ------- + connected_dominating : bool + True if `nbunch` is connected dominating set of `G`, false otherwise. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Dominating_set + .. [2] https://en.wikipedia.org/wiki/Connected_dominating_set + + """ + return nx.is_dominating_set(G, nbunch) and nx.is_connected(nx.subgraph(G, nbunch)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/efficiency_measures.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/efficiency_measures.py new file mode 100644 index 0000000000000000000000000000000000000000..b8e9d7a9e680e7db5d61b87e067c03a6d603c3af --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/efficiency_measures.py @@ -0,0 +1,167 @@ +"""Provides functions for computing the efficiency of nodes and graphs.""" + +import networkx as nx +from networkx.exception import NetworkXNoPath + +from ..utils import not_implemented_for + +__all__ = ["efficiency", "local_efficiency", "global_efficiency"] + + +@not_implemented_for("directed") +@nx._dispatchable +def efficiency(G, u, v): + """Returns the efficiency of a pair of nodes in a graph. + + The *efficiency* of a pair of nodes is the multiplicative inverse of the + shortest path distance between the nodes [1]_. Returns 0 if no path + between nodes. + + Parameters + ---------- + G : :class:`networkx.Graph` + An undirected graph for which to compute the average local efficiency. + u, v : node + Nodes in the graph ``G``. + + Returns + ------- + float + Multiplicative inverse of the shortest path distance between the nodes. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]) + >>> nx.efficiency(G, 2, 3) # this gives efficiency for node 2 and 3 + 0.5 + + Notes + ----- + Edge weights are ignored when computing the shortest path distances. + + See also + -------- + local_efficiency + global_efficiency + + References + ---------- + .. [1] Latora, Vito, and Massimo Marchiori. + "Efficient behavior of small-world networks." + *Physical Review Letters* 87.19 (2001): 198701. + + + """ + try: + eff = 1 / nx.shortest_path_length(G, u, v) + except NetworkXNoPath: + eff = 0 + return eff + + +@not_implemented_for("directed") +@nx._dispatchable +def global_efficiency(G): + """Returns the average global efficiency of the graph. + + The *efficiency* of a pair of nodes in a graph is the multiplicative + inverse of the shortest path distance between the nodes. The *average + global efficiency* of a graph is the average efficiency of all pairs of + nodes [1]_. + + Parameters + ---------- + G : :class:`networkx.Graph` + An undirected graph for which to compute the average global efficiency. + + Returns + ------- + float + The average global efficiency of the graph. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]) + >>> round(nx.global_efficiency(G), 12) + 0.916666666667 + + Notes + ----- + Edge weights are ignored when computing the shortest path distances. + + See also + -------- + local_efficiency + + References + ---------- + .. [1] Latora, Vito, and Massimo Marchiori. + "Efficient behavior of small-world networks." + *Physical Review Letters* 87.19 (2001): 198701. + + + """ + n = len(G) + denom = n * (n - 1) + if denom != 0: + lengths = nx.all_pairs_shortest_path_length(G) + g_eff = 0 + for source, targets in lengths: + for target, distance in targets.items(): + if distance > 0: + g_eff += 1 / distance + g_eff /= denom + # g_eff = sum(1 / d for s, tgts in lengths + # for t, d in tgts.items() if d > 0) / denom + else: + g_eff = 0 + # TODO This can be made more efficient by computing all pairs shortest + # path lengths in parallel. + return g_eff + + +@not_implemented_for("directed") +@nx._dispatchable +def local_efficiency(G): + """Returns the average local efficiency of the graph. + + The *efficiency* of a pair of nodes in a graph is the multiplicative + inverse of the shortest path distance between the nodes. The *local + efficiency* of a node in the graph is the average global efficiency of the + subgraph induced by the neighbors of the node. The *average local + efficiency* is the average of the local efficiencies of each node [1]_. + + Parameters + ---------- + G : :class:`networkx.Graph` + An undirected graph for which to compute the average local efficiency. + + Returns + ------- + float + The average local efficiency of the graph. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)]) + >>> nx.local_efficiency(G) + 0.9166666666666667 + + Notes + ----- + Edge weights are ignored when computing the shortest path distances. + + See also + -------- + global_efficiency + + References + ---------- + .. [1] Latora, Vito, and Massimo Marchiori. + "Efficient behavior of small-world networks." + *Physical Review Letters* 87.19 (2001): 198701. + + + """ + efficiency_list = (global_efficiency(G.subgraph(G[v])) for v in G) + return sum(efficiency_list) / len(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/euler.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/euler.py new file mode 100644 index 0000000000000000000000000000000000000000..2c308e380c774a6450d4ce275118ccffd65defaa --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/euler.py @@ -0,0 +1,470 @@ +""" +Eulerian circuits and graphs. +""" + +from itertools import combinations + +import networkx as nx + +from ..utils import arbitrary_element, not_implemented_for + +__all__ = [ + "is_eulerian", + "eulerian_circuit", + "eulerize", + "is_semieulerian", + "has_eulerian_path", + "eulerian_path", +] + + +@nx._dispatchable +def is_eulerian(G): + """Returns True if and only if `G` is Eulerian. + + A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian + circuit* is a closed walk that includes each edge of a graph exactly + once. + + Graphs with isolated vertices (i.e. vertices with zero degree) are not + considered to have Eulerian circuits. Therefore, if the graph is not + connected (or not strongly connected, for directed graphs), this function + returns False. + + Parameters + ---------- + G : NetworkX graph + A graph, either directed or undirected. + + Examples + -------- + >>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]})) + True + >>> nx.is_eulerian(nx.complete_graph(5)) + True + >>> nx.is_eulerian(nx.petersen_graph()) + False + + If you prefer to allow graphs with isolated vertices to have Eulerian circuits, + you can first remove such vertices and then call `is_eulerian` as below example shows. + + >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)]) + >>> G.add_node(3) + >>> nx.is_eulerian(G) + False + + >>> G.remove_nodes_from(list(nx.isolates(G))) + >>> nx.is_eulerian(G) + True + + + """ + if G.is_directed(): + # Every node must have equal in degree and out degree and the + # graph must be strongly connected + return all( + G.in_degree(n) == G.out_degree(n) for n in G + ) and nx.is_strongly_connected(G) + # An undirected Eulerian graph has no vertices of odd degree and + # must be connected. + return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G) + + +@nx._dispatchable +def is_semieulerian(G): + """Return True iff `G` is semi-Eulerian. + + G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit. + + See Also + -------- + has_eulerian_path + is_eulerian + """ + return has_eulerian_path(G) and not is_eulerian(G) + + +def _find_path_start(G): + """Return a suitable starting vertex for an Eulerian path. + + If no path exists, return None. + """ + if not has_eulerian_path(G): + return None + + if is_eulerian(G): + return arbitrary_element(G) + + if G.is_directed(): + v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v)) + # Determines which is the 'start' node (as opposed to the 'end') + if G.out_degree(v1) > G.in_degree(v1): + return v1 + else: + return v2 + + else: + # In an undirected graph randomly choose one of the possibilities + start = [v for v in G if G.degree(v) % 2 != 0][0] + return start + + +def _simplegraph_eulerian_circuit(G, source): + if G.is_directed(): + degree = G.out_degree + edges = G.out_edges + else: + degree = G.degree + edges = G.edges + vertex_stack = [source] + last_vertex = None + while vertex_stack: + current_vertex = vertex_stack[-1] + if degree(current_vertex) == 0: + if last_vertex is not None: + yield (last_vertex, current_vertex) + last_vertex = current_vertex + vertex_stack.pop() + else: + _, next_vertex = arbitrary_element(edges(current_vertex)) + vertex_stack.append(next_vertex) + G.remove_edge(current_vertex, next_vertex) + + +def _multigraph_eulerian_circuit(G, source): + if G.is_directed(): + degree = G.out_degree + edges = G.out_edges + else: + degree = G.degree + edges = G.edges + vertex_stack = [(source, None)] + last_vertex = None + last_key = None + while vertex_stack: + current_vertex, current_key = vertex_stack[-1] + if degree(current_vertex) == 0: + if last_vertex is not None: + yield (last_vertex, current_vertex, last_key) + last_vertex, last_key = current_vertex, current_key + vertex_stack.pop() + else: + triple = arbitrary_element(edges(current_vertex, keys=True)) + _, next_vertex, next_key = triple + vertex_stack.append((next_vertex, next_key)) + G.remove_edge(current_vertex, next_vertex, next_key) + + +@nx._dispatchable +def eulerian_circuit(G, source=None, keys=False): + """Returns an iterator over the edges of an Eulerian circuit in `G`. + + An *Eulerian circuit* is a closed walk that includes each edge of a + graph exactly once. + + Parameters + ---------- + G : NetworkX graph + A graph, either directed or undirected. + + source : node, optional + Starting node for circuit. + + keys : bool + If False, edges generated by this function will be of the form + ``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``. + This option is ignored unless `G` is a multigraph. + + Returns + ------- + edges : iterator + An iterator over edges in the Eulerian circuit. + + Raises + ------ + NetworkXError + If the graph is not Eulerian. + + See Also + -------- + is_eulerian + + Notes + ----- + This is a linear time implementation of an algorithm adapted from [1]_. + + For general information about Euler tours, see [2]_. + + References + ---------- + .. [1] J. Edmonds, E. L. Johnson. + Matching, Euler tours and the Chinese postman. + Mathematical programming, Volume 5, Issue 1 (1973), 111-114. + .. [2] https://en.wikipedia.org/wiki/Eulerian_path + + Examples + -------- + To get an Eulerian circuit in an undirected graph:: + + >>> G = nx.complete_graph(3) + >>> list(nx.eulerian_circuit(G)) + [(0, 2), (2, 1), (1, 0)] + >>> list(nx.eulerian_circuit(G, source=1)) + [(1, 2), (2, 0), (0, 1)] + + To get the sequence of vertices in an Eulerian circuit:: + + >>> [u for u, v in nx.eulerian_circuit(G)] + [0, 2, 1] + + """ + if not is_eulerian(G): + raise nx.NetworkXError("G is not Eulerian.") + if G.is_directed(): + G = G.reverse() + else: + G = G.copy() + if source is None: + source = arbitrary_element(G) + if G.is_multigraph(): + for u, v, k in _multigraph_eulerian_circuit(G, source): + if keys: + yield u, v, k + else: + yield u, v + else: + yield from _simplegraph_eulerian_circuit(G, source) + + +@nx._dispatchable +def has_eulerian_path(G, source=None): + """Return True iff `G` has an Eulerian path. + + An Eulerian path is a path in a graph which uses each edge of a graph + exactly once. If `source` is specified, then this function checks + whether an Eulerian path that starts at node `source` exists. + + A directed graph has an Eulerian path iff: + - at most one vertex has out_degree - in_degree = 1, + - at most one vertex has in_degree - out_degree = 1, + - every other vertex has equal in_degree and out_degree, + - and all of its vertices belong to a single connected + component of the underlying undirected graph. + + If `source` is not None, an Eulerian path starting at `source` exists if no + other node has out_degree - in_degree = 1. This is equivalent to either + there exists an Eulerian circuit or `source` has out_degree - in_degree = 1 + and the conditions above hold. + + An undirected graph has an Eulerian path iff: + - exactly zero or two vertices have odd degree, + - and all of its vertices belong to a single connected component. + + If `source` is not None, an Eulerian path starting at `source` exists if + either there exists an Eulerian circuit or `source` has an odd degree and the + conditions above hold. + + Graphs with isolated vertices (i.e. vertices with zero degree) are not considered + to have an Eulerian path. Therefore, if the graph is not connected (or not strongly + connected, for directed graphs), this function returns False. + + Parameters + ---------- + G : NetworkX Graph + The graph to find an euler path in. + + source : node, optional + Starting node for path. + + Returns + ------- + Bool : True if G has an Eulerian path. + + Examples + -------- + If you prefer to allow graphs with isolated vertices to have Eulerian path, + you can first remove such vertices and then call `has_eulerian_path` as below example shows. + + >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)]) + >>> G.add_node(3) + >>> nx.has_eulerian_path(G) + False + + >>> G.remove_nodes_from(list(nx.isolates(G))) + >>> nx.has_eulerian_path(G) + True + + See Also + -------- + is_eulerian + eulerian_path + """ + if nx.is_eulerian(G): + return True + + if G.is_directed(): + ins = G.in_degree + outs = G.out_degree + # Since we know it is not eulerian, outs - ins must be 1 for source + if source is not None and outs[source] - ins[source] != 1: + return False + + unbalanced_ins = 0 + unbalanced_outs = 0 + for v in G: + if ins[v] - outs[v] == 1: + unbalanced_ins += 1 + elif outs[v] - ins[v] == 1: + unbalanced_outs += 1 + elif ins[v] != outs[v]: + return False + + return ( + unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G) + ) + else: + # We know it is not eulerian, so degree of source must be odd. + if source is not None and G.degree[source] % 2 != 1: + return False + + # Sum is 2 since we know it is not eulerian (which implies sum is 0) + return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G) + + +@nx._dispatchable +def eulerian_path(G, source=None, keys=False): + """Return an iterator over the edges of an Eulerian path in `G`. + + Parameters + ---------- + G : NetworkX Graph + The graph in which to look for an eulerian path. + source : node or None (default: None) + The node at which to start the search. None means search over all + starting nodes. + keys : Bool (default: False) + Indicates whether to yield edge 3-tuples (u, v, edge_key). + The default yields edge 2-tuples + + Yields + ------ + Edge tuples along the eulerian path. + + Warning: If `source` provided is not the start node of an Euler path + will raise error even if an Euler Path exists. + """ + if not has_eulerian_path(G, source): + raise nx.NetworkXError("Graph has no Eulerian paths.") + if G.is_directed(): + G = G.reverse() + if source is None or nx.is_eulerian(G) is False: + source = _find_path_start(G) + if G.is_multigraph(): + for u, v, k in _multigraph_eulerian_circuit(G, source): + if keys: + yield u, v, k + else: + yield u, v + else: + yield from _simplegraph_eulerian_circuit(G, source) + else: + G = G.copy() + if source is None: + source = _find_path_start(G) + if G.is_multigraph(): + if keys: + yield from reversed( + [(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)] + ) + else: + yield from reversed( + [(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)] + ) + else: + yield from reversed( + [(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)] + ) + + +@not_implemented_for("directed") +@nx._dispatchable(returns_graph=True) +def eulerize(G): + """Transforms a graph into an Eulerian graph. + + If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest + (in terms of the number of edges) multigraph whose underlying simple graph is `G`. + + Parameters + ---------- + G : NetworkX graph + An undirected graph + + Returns + ------- + G : NetworkX multigraph + + Raises + ------ + NetworkXError + If the graph is not connected. + + See Also + -------- + is_eulerian + eulerian_circuit + + References + ---------- + .. [1] J. Edmonds, E. L. Johnson. + Matching, Euler tours and the Chinese postman. + Mathematical programming, Volume 5, Issue 1 (1973), 111-114. + .. [2] https://en.wikipedia.org/wiki/Eulerian_path + .. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf + + Examples + -------- + >>> G = nx.complete_graph(10) + >>> H = nx.eulerize(G) + >>> nx.is_eulerian(H) + True + + """ + if G.order() == 0: + raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph") + if not nx.is_connected(G): + raise nx.NetworkXError("G is not connected") + odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1] + G = nx.MultiGraph(G) + if len(odd_degree_nodes) == 0: + return G + + # get all shortest paths between vertices of odd degree + odd_deg_pairs_paths = [ + (m, {n: nx.shortest_path(G, source=m, target=n)}) + for m, n in combinations(odd_degree_nodes, 2) + ] + + # use the number of vertices in a graph + 1 as an upper bound on + # the maximum length of a path in G + upper_bound_on_max_path_length = len(G) + 1 + + # use "len(G) + 1 - len(P)", + # where P is a shortest path between vertices n and m, + # as edge-weights in a new graph + # store the paths in the graph for easy indexing later + Gp = nx.Graph() + for n, Ps in odd_deg_pairs_paths: + for m, P in Ps.items(): + if n != m: + Gp.add_edge( + m, n, weight=upper_bound_on_max_path_length - len(P), path=P + ) + + # find the minimum weight matching of edges in the weighted graph + best_matching = nx.Graph(list(nx.max_weight_matching(Gp))) + + # duplicate each edge along each path in the set of paths in Gp + for m, n in best_matching.edges(): + path = Gp[m][n]["path"] + G.add_edges_from(nx.utils.pairwise(path)) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d19abed99501086359c87670edc31a680fe36c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/__init__.py @@ -0,0 +1,11 @@ +from .maxflow import * +from .mincost import * +from .boykovkolmogorov import * +from .dinitz_alg import * +from .edmondskarp import * +from .gomory_hu import * +from .preflowpush import * +from .shortestaugmentingpath import * +from .capacityscaling import * +from .networksimplex import * +from .utils import build_flow_dict, build_residual_network diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/boykovkolmogorov.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/boykovkolmogorov.py new file mode 100644 index 0000000000000000000000000000000000000000..30899c6c33e7ff508cfb13886a13ec96fef4ba44 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/boykovkolmogorov.py @@ -0,0 +1,370 @@ +""" +Boykov-Kolmogorov algorithm for maximum flow problems. +""" + +from collections import deque +from operator import itemgetter + +import networkx as nx +from networkx.algorithms.flow.utils import build_residual_network + +__all__ = ["boykov_kolmogorov"] + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def boykov_kolmogorov( + G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None +): + r"""Find a maximum single-commodity flow using Boykov-Kolmogorov algorithm. + + This function returns the residual network resulting after computing + the maximum flow. See below for details about the conventions + NetworkX uses for defining residual networks. + + This algorithm has worse case complexity $O(n^2 m |C|)$ for $n$ nodes, $m$ + edges, and $|C|$ the cost of the minimum cut [1]_. This implementation + uses the marking heuristic defined in [2]_ which improves its running + time in many practical problems. + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + residual : NetworkX graph + Residual network on which the algorithm is to be executed. If None, a + new residual network is created. Default value: None. + + value_only : bool + If True compute only the value of the maximum flow. This parameter + will be ignored by this algorithm because it is not applicable. + + cutoff : integer, float + If specified, the algorithm will terminate when the flow value reaches + or exceeds the cutoff. In this case, it may be unable to immediately + determine a minimum cut. Default value: None. + + Returns + ------- + R : NetworkX DiGraph + Residual network after computing the maximum flow. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not + specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such + that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Examples + -------- + >>> from networkx.algorithms.flow import boykov_kolmogorov + + The functions that implement flow algorithms and output a residual + network, such as this one, are not imported to the base NetworkX + namespace, so you have to explicitly import them from the flow package. + + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + >>> R = boykov_kolmogorov(G, "x", "y") + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value + 3.0 + >>> flow_value == R.graph["flow_value"] + True + + A nice feature of the Boykov-Kolmogorov algorithm is that a partition + of the nodes that defines a minimum cut can be easily computed based + on the search trees used during the algorithm. These trees are stored + in the graph attribute `trees` of the residual network. + + >>> source_tree, target_tree = R.graph["trees"] + >>> partition = (set(source_tree), set(G) - set(source_tree)) + + Or equivalently: + + >>> partition = (set(G) - set(target_tree), set(target_tree)) + + References + ---------- + .. [1] Boykov, Y., & Kolmogorov, V. (2004). An experimental comparison + of min-cut/max-flow algorithms for energy minimization in vision. + Pattern Analysis and Machine Intelligence, IEEE Transactions on, + 26(9), 1124-1137. + https://doi.org/10.1109/TPAMI.2004.60 + + .. [2] Vladimir Kolmogorov. Graph-based Algorithms for Multi-camera + Reconstruction Problem. PhD thesis, Cornell University, CS Department, + 2003. pp. 109-114. + https://web.archive.org/web/20170809091249/https://pub.ist.ac.at/~vnk/papers/thesis.pdf + + """ + R = boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff) + R.graph["algorithm"] = "boykov_kolmogorov" + nx._clear_cache(R) + return R + + +def boykov_kolmogorov_impl(G, s, t, capacity, residual, cutoff): + if s not in G: + raise nx.NetworkXError(f"node {str(s)} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {str(t)} not in graph") + if s == t: + raise nx.NetworkXError("source and sink are the same node") + + if residual is None: + R = build_residual_network(G, capacity) + else: + R = residual + + # Initialize/reset the residual network. + # This is way too slow + # nx.set_edge_attributes(R, 0, 'flow') + for u in R: + for e in R[u].values(): + e["flow"] = 0 + + # Use an arbitrary high value as infinite. It is computed + # when building the residual network. + INF = R.graph["inf"] + + if cutoff is None: + cutoff = INF + + R_succ = R.succ + R_pred = R.pred + + def grow(): + """Bidirectional breadth-first search for the growth stage. + + Returns a connecting edge, that is and edge that connects + a node from the source search tree with a node from the + target search tree. + The first node in the connecting edge is always from the + source tree and the last node from the target tree. + """ + while active: + u = active[0] + if u in source_tree: + this_tree = source_tree + other_tree = target_tree + neighbors = R_succ + else: + this_tree = target_tree + other_tree = source_tree + neighbors = R_pred + for v, attr in neighbors[u].items(): + if attr["capacity"] - attr["flow"] > 0: + if v not in this_tree: + if v in other_tree: + return (u, v) if this_tree is source_tree else (v, u) + this_tree[v] = u + dist[v] = dist[u] + 1 + timestamp[v] = timestamp[u] + active.append(v) + elif v in this_tree and _is_closer(u, v): + this_tree[v] = u + dist[v] = dist[u] + 1 + timestamp[v] = timestamp[u] + _ = active.popleft() + return None, None + + def augment(u, v): + """Augmentation stage. + + Reconstruct path and determine its residual capacity. + We start from a connecting edge, which links a node + from the source tree to a node from the target tree. + The connecting edge is the output of the grow function + and the input of this function. + """ + attr = R_succ[u][v] + flow = min(INF, attr["capacity"] - attr["flow"]) + path = [u] + # Trace a path from u to s in source_tree. + w = u + while w != s: + n = w + w = source_tree[n] + attr = R_pred[n][w] + flow = min(flow, attr["capacity"] - attr["flow"]) + path.append(w) + path.reverse() + # Trace a path from v to t in target_tree. + path.append(v) + w = v + while w != t: + n = w + w = target_tree[n] + attr = R_succ[n][w] + flow = min(flow, attr["capacity"] - attr["flow"]) + path.append(w) + # Augment flow along the path and check for saturated edges. + it = iter(path) + u = next(it) + these_orphans = [] + for v in it: + R_succ[u][v]["flow"] += flow + R_succ[v][u]["flow"] -= flow + if R_succ[u][v]["flow"] == R_succ[u][v]["capacity"]: + if v in source_tree: + source_tree[v] = None + these_orphans.append(v) + if u in target_tree: + target_tree[u] = None + these_orphans.append(u) + u = v + orphans.extend(sorted(these_orphans, key=dist.get)) + return flow + + def adopt(): + """Adoption stage. + + Reconstruct search trees by adopting or discarding orphans. + During augmentation stage some edges got saturated and thus + the source and target search trees broke down to forests, with + orphans as roots of some of its trees. We have to reconstruct + the search trees rooted to source and target before we can grow + them again. + """ + while orphans: + u = orphans.popleft() + if u in source_tree: + tree = source_tree + neighbors = R_pred + else: + tree = target_tree + neighbors = R_succ + nbrs = ((n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree) + for v, attr, d in sorted(nbrs, key=itemgetter(2)): + if attr["capacity"] - attr["flow"] > 0: + if _has_valid_root(v, tree): + tree[u] = v + dist[u] = dist[v] + 1 + timestamp[u] = time + break + else: + nbrs = ( + (n, attr, dist[n]) for n, attr in neighbors[u].items() if n in tree + ) + for v, attr, d in sorted(nbrs, key=itemgetter(2)): + if attr["capacity"] - attr["flow"] > 0: + if v not in active: + active.append(v) + if tree[v] == u: + tree[v] = None + orphans.appendleft(v) + if u in active: + active.remove(u) + del tree[u] + + def _has_valid_root(n, tree): + path = [] + v = n + while v is not None: + path.append(v) + if v in (s, t): + base_dist = 0 + break + elif timestamp[v] == time: + base_dist = dist[v] + break + v = tree[v] + else: + return False + length = len(path) + for i, u in enumerate(path, 1): + dist[u] = base_dist + length - i + timestamp[u] = time + return True + + def _is_closer(u, v): + return timestamp[v] <= timestamp[u] and dist[v] > dist[u] + 1 + + source_tree = {s: None} + target_tree = {t: None} + active = deque([s, t]) + orphans = deque() + flow_value = 0 + # data structures for the marking heuristic + time = 1 + timestamp = {s: time, t: time} + dist = {s: 0, t: 0} + while flow_value < cutoff: + # Growth stage + u, v = grow() + if u is None: + break + time += 1 + # Augmentation stage + flow_value += augment(u, v) + # Adoption stage + adopt() + + if flow_value * 2 > INF: + raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.") + + # Add source and target tree in a graph attribute. + # A partition that defines a minimum cut can be directly + # computed from the search trees as explained in the docstrings. + R.graph["trees"] = (source_tree, target_tree) + # Add the standard flow_value graph attribute. + R.graph["flow_value"] = flow_value + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/capacityscaling.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/capacityscaling.py new file mode 100644 index 0000000000000000000000000000000000000000..bf68565c5486bb7b60e7ddcf6089e448bc6ddef1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/capacityscaling.py @@ -0,0 +1,407 @@ +""" +Capacity scaling minimum cost flow algorithm. +""" + +__all__ = ["capacity_scaling"] + +from itertools import chain +from math import log + +import networkx as nx + +from ...utils import BinaryHeap, arbitrary_element, not_implemented_for + + +def _detect_unboundedness(R): + """Detect infinite-capacity negative cycles.""" + G = nx.DiGraph() + G.add_nodes_from(R) + + # Value simulating infinity. + inf = R.graph["inf"] + # True infinity. + f_inf = float("inf") + for u in R: + for v, e in R[u].items(): + # Compute the minimum weight of infinite-capacity (u, v) edges. + w = f_inf + for k, e in e.items(): + if e["capacity"] == inf: + w = min(w, e["weight"]) + if w != f_inf: + G.add_edge(u, v, weight=w) + + if nx.negative_edge_cycle(G): + raise nx.NetworkXUnbounded( + "Negative cost cycle of infinite capacity found. " + "Min cost flow may be unbounded below." + ) + + +@not_implemented_for("undirected") +def _build_residual_network(G, demand, capacity, weight): + """Build a residual network and initialize a zero flow.""" + if sum(G.nodes[u].get(demand, 0) for u in G) != 0: + raise nx.NetworkXUnfeasible("Sum of the demands should be 0.") + + R = nx.MultiDiGraph() + R.add_nodes_from( + (u, {"excess": -G.nodes[u].get(demand, 0), "potential": 0}) for u in G + ) + + inf = float("inf") + # Detect selfloops with infinite capacities and negative weights. + for u, v, e in nx.selfloop_edges(G, data=True): + if e.get(weight, 0) < 0 and e.get(capacity, inf) == inf: + raise nx.NetworkXUnbounded( + "Negative cost cycle of infinite capacity found. " + "Min cost flow may be unbounded below." + ) + + # Extract edges with positive capacities. Self loops excluded. + if G.is_multigraph(): + edge_list = [ + (u, v, k, e) + for u, v, k, e in G.edges(data=True, keys=True) + if u != v and e.get(capacity, inf) > 0 + ] + else: + edge_list = [ + (u, v, 0, e) + for u, v, e in G.edges(data=True) + if u != v and e.get(capacity, inf) > 0 + ] + # Simulate infinity with the larger of the sum of absolute node imbalances + # the sum of finite edge capacities or any positive value if both sums are + # zero. This allows the infinite-capacity edges to be distinguished for + # unboundedness detection and directly participate in residual capacity + # calculation. + inf = ( + max( + sum(abs(R.nodes[u]["excess"]) for u in R), + 2 + * sum( + e[capacity] + for u, v, k, e in edge_list + if capacity in e and e[capacity] != inf + ), + ) + or 1 + ) + for u, v, k, e in edge_list: + r = min(e.get(capacity, inf), inf) + w = e.get(weight, 0) + # Add both (u, v) and (v, u) into the residual network marked with the + # original key. (key[1] == True) indicates the (u, v) is in the + # original network. + R.add_edge(u, v, key=(k, True), capacity=r, weight=w, flow=0) + R.add_edge(v, u, key=(k, False), capacity=0, weight=-w, flow=0) + + # Record the value simulating infinity. + R.graph["inf"] = inf + + _detect_unboundedness(R) + + return R + + +def _build_flow_dict(G, R, capacity, weight): + """Build a flow dictionary from a residual network.""" + inf = float("inf") + flow_dict = {} + if G.is_multigraph(): + for u in G: + flow_dict[u] = {} + for v, es in G[u].items(): + flow_dict[u][v] = { + # Always saturate negative selfloops. + k: ( + 0 + if ( + u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0 + ) + else e[capacity] + ) + for k, e in es.items() + } + for v, es in R[u].items(): + if v in flow_dict[u]: + flow_dict[u][v].update( + (k[0], e["flow"]) for k, e in es.items() if e["flow"] > 0 + ) + else: + for u in G: + flow_dict[u] = { + # Always saturate negative selfloops. + v: ( + 0 + if (u != v or e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0) + else e[capacity] + ) + for v, e in G[u].items() + } + flow_dict[u].update( + (v, e["flow"]) + for v, es in R[u].items() + for e in es.values() + if e["flow"] > 0 + ) + return flow_dict + + +@nx._dispatchable( + node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0} +) +def capacity_scaling( + G, demand="demand", capacity="capacity", weight="weight", heap=BinaryHeap +): + r"""Find a minimum cost flow satisfying all demands in digraph G. + + This is a capacity scaling successive shortest augmenting path algorithm. + + G is a digraph with edge costs and capacities and in which nodes + have demand, i.e., they want to send or receive some amount of + flow. A negative demand means that the node wants to send flow, a + positive demand means that the node want to receive flow. A flow on + the digraph G satisfies all demand if the net flow into each node + is equal to the demand of that node. + + Parameters + ---------- + G : NetworkX graph + DiGraph or MultiDiGraph on which a minimum cost flow satisfying all + demands is to be found. + + demand : string + Nodes of the graph G are expected to have an attribute demand + that indicates how much flow a node wants to send (negative + demand) or receive (positive demand). Note that the sum of the + demands should be 0 otherwise the problem in not feasible. If + this attribute is not present, a node is considered to have 0 + demand. Default value: 'demand'. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + weight : string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + heap : class + Type of heap to be used in the algorithm. It should be a subclass of + :class:`MinHeap` or implement a compatible interface. + + If a stock heap implementation is to be used, :class:`BinaryHeap` is + recommended over :class:`PairingHeap` for Python implementations without + optimized attribute accesses (e.g., CPython) despite a slower + asymptotic running time. For Python implementations with optimized + attribute accesses (e.g., PyPy), :class:`PairingHeap` provides better + performance. Default value: :class:`BinaryHeap`. + + Returns + ------- + flowCost : integer + Cost of a minimum cost flow satisfying all demands. + + flowDict : dictionary + If G is a digraph, a dict-of-dicts keyed by nodes such that + flowDict[u][v] is the flow on edge (u, v). + If G is a MultiDiGraph, a dict-of-dicts-of-dicts keyed by nodes + so that flowDict[u][v][key] is the flow on edge (u, v, key). + + Raises + ------ + NetworkXError + This exception is raised if the input graph is not directed, + not connected. + + NetworkXUnfeasible + This exception is raised in the following situations: + + * The sum of the demands is not zero. Then, there is no + flow satisfying all demands. + * There is no flow satisfying all demand. + + NetworkXUnbounded + This exception is raised if the digraph G has a cycle of + negative cost and infinite capacity. Then, the cost of a flow + satisfying all demands is unbounded below. + + Notes + ----- + This algorithm does not work if edge weights are floating-point numbers. + + See also + -------- + :meth:`network_simplex` + + Examples + -------- + A simple example of a min cost flow problem. + + >>> G = nx.DiGraph() + >>> G.add_node("a", demand=-5) + >>> G.add_node("d", demand=5) + >>> G.add_edge("a", "b", weight=3, capacity=4) + >>> G.add_edge("a", "c", weight=6, capacity=10) + >>> G.add_edge("b", "d", weight=1, capacity=9) + >>> G.add_edge("c", "d", weight=2, capacity=5) + >>> flowCost, flowDict = nx.capacity_scaling(G) + >>> flowCost + 24 + >>> flowDict + {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}} + + It is possible to change the name of the attributes used for the + algorithm. + + >>> G = nx.DiGraph() + >>> G.add_node("p", spam=-4) + >>> G.add_node("q", spam=2) + >>> G.add_node("a", spam=-2) + >>> G.add_node("d", spam=-1) + >>> G.add_node("t", spam=2) + >>> G.add_node("w", spam=3) + >>> G.add_edge("p", "q", cost=7, vacancies=5) + >>> G.add_edge("p", "a", cost=1, vacancies=4) + >>> G.add_edge("q", "d", cost=2, vacancies=3) + >>> G.add_edge("t", "q", cost=1, vacancies=2) + >>> G.add_edge("a", "t", cost=2, vacancies=4) + >>> G.add_edge("d", "w", cost=3, vacancies=4) + >>> G.add_edge("t", "w", cost=4, vacancies=1) + >>> flowCost, flowDict = nx.capacity_scaling( + ... G, demand="spam", capacity="vacancies", weight="cost" + ... ) + >>> flowCost + 37 + >>> flowDict + {'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}} + """ + R = _build_residual_network(G, demand, capacity, weight) + + inf = float("inf") + # Account cost of negative selfloops. + flow_cost = sum( + 0 + if e.get(capacity, inf) <= 0 or e.get(weight, 0) >= 0 + else e[capacity] * e[weight] + for u, v, e in nx.selfloop_edges(G, data=True) + ) + + # Determine the maximum edge capacity. + wmax = max(chain([-inf], (e["capacity"] for u, v, e in R.edges(data=True)))) + if wmax == -inf: + # Residual network has no edges. + return flow_cost, _build_flow_dict(G, R, capacity, weight) + + R_nodes = R.nodes + R_succ = R.succ + + delta = 2 ** int(log(wmax, 2)) + while delta >= 1: + # Saturate Δ-residual edges with negative reduced costs to achieve + # Δ-optimality. + for u in R: + p_u = R_nodes[u]["potential"] + for v, es in R_succ[u].items(): + for k, e in es.items(): + flow = e["capacity"] - e["flow"] + if e["weight"] - p_u + R_nodes[v]["potential"] < 0: + flow = e["capacity"] - e["flow"] + if flow >= delta: + e["flow"] += flow + R_succ[v][u][(k[0], not k[1])]["flow"] -= flow + R_nodes[u]["excess"] -= flow + R_nodes[v]["excess"] += flow + # Determine the Δ-active nodes. + S = set() + T = set() + S_add = S.add + S_remove = S.remove + T_add = T.add + T_remove = T.remove + for u in R: + excess = R_nodes[u]["excess"] + if excess >= delta: + S_add(u) + elif excess <= -delta: + T_add(u) + # Repeatedly augment flow from S to T along shortest paths until + # Δ-feasibility is achieved. + while S and T: + s = arbitrary_element(S) + t = None + # Search for a shortest path in terms of reduce costs from s to + # any t in T in the Δ-residual network. + d = {} + pred = {s: None} + h = heap() + h_insert = h.insert + h_get = h.get + h_insert(s, 0) + while h: + u, d_u = h.pop() + d[u] = d_u + if u in T: + # Path found. + t = u + break + p_u = R_nodes[u]["potential"] + for v, es in R_succ[u].items(): + if v in d: + continue + wmin = inf + # Find the minimum-weighted (u, v) Δ-residual edge. + for k, e in es.items(): + if e["capacity"] - e["flow"] >= delta: + w = e["weight"] + if w < wmin: + wmin = w + kmin = k + emin = e + if wmin == inf: + continue + # Update the distance label of v. + d_v = d_u + wmin - p_u + R_nodes[v]["potential"] + if h_insert(v, d_v): + pred[v] = (u, kmin, emin) + if t is not None: + # Augment Δ units of flow from s to t. + while u != s: + v = u + u, k, e = pred[v] + e["flow"] += delta + R_succ[v][u][(k[0], not k[1])]["flow"] -= delta + # Account node excess and deficit. + R_nodes[s]["excess"] -= delta + R_nodes[t]["excess"] += delta + if R_nodes[s]["excess"] < delta: + S_remove(s) + if R_nodes[t]["excess"] > -delta: + T_remove(t) + # Update node potentials. + d_t = d[t] + for u, d_u in d.items(): + R_nodes[u]["potential"] -= d_u - d_t + else: + # Path not found. + S_remove(s) + delta //= 2 + + if any(R.nodes[u]["excess"] != 0 for u in R): + raise nx.NetworkXUnfeasible("No flow satisfying all demands.") + + # Calculate the flow cost. + for u in R: + for v, es in R_succ[u].items(): + for e in es.values(): + flow = e["flow"] + if flow > 0: + flow_cost += flow * e["weight"] + + return flow_cost, _build_flow_dict(G, R, capacity, weight) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/dinitz_alg.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/dinitz_alg.py new file mode 100644 index 0000000000000000000000000000000000000000..f369642af2968094184741132a843f5dde81e428 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/dinitz_alg.py @@ -0,0 +1,238 @@ +""" +Dinitz' algorithm for maximum flow problems. +""" + +from collections import deque + +import networkx as nx +from networkx.algorithms.flow.utils import build_residual_network +from networkx.utils import pairwise + +__all__ = ["dinitz"] + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def dinitz(G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None): + """Find a maximum single-commodity flow using Dinitz' algorithm. + + This function returns the residual network resulting after computing + the maximum flow. See below for details about the conventions + NetworkX uses for defining residual networks. + + This algorithm has a running time of $O(n^2 m)$ for $n$ nodes and $m$ + edges [1]_. + + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + residual : NetworkX graph + Residual network on which the algorithm is to be executed. If None, a + new residual network is created. Default value: None. + + value_only : bool + If True compute only the value of the maximum flow. This parameter + will be ignored by this algorithm because it is not applicable. + + cutoff : integer, float + If specified, the algorithm will terminate when the flow value reaches + or exceeds the cutoff. In this case, it may be unable to immediately + determine a minimum cut. Default value: None. + + Returns + ------- + R : NetworkX DiGraph + Residual network after computing the maximum flow. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not + specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such + that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Examples + -------- + >>> from networkx.algorithms.flow import dinitz + + The functions that implement flow algorithms and output a residual + network, such as this one, are not imported to the base NetworkX + namespace, so you have to explicitly import them from the flow package. + + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + >>> R = dinitz(G, "x", "y") + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value + 3.0 + >>> flow_value == R.graph["flow_value"] + True + + References + ---------- + .. [1] Dinitz' Algorithm: The Original Version and Even's Version. + 2006. Yefim Dinitz. In Theoretical Computer Science. Lecture + Notes in Computer Science. Volume 3895. pp 218-240. + https://doi.org/10.1007/11685654_10 + + """ + R = dinitz_impl(G, s, t, capacity, residual, cutoff) + R.graph["algorithm"] = "dinitz" + nx._clear_cache(R) + return R + + +def dinitz_impl(G, s, t, capacity, residual, cutoff): + if s not in G: + raise nx.NetworkXError(f"node {str(s)} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {str(t)} not in graph") + if s == t: + raise nx.NetworkXError("source and sink are the same node") + + if residual is None: + R = build_residual_network(G, capacity) + else: + R = residual + + # Initialize/reset the residual network. + for u in R: + for e in R[u].values(): + e["flow"] = 0 + + # Use an arbitrary high value as infinite. It is computed + # when building the residual network. + INF = R.graph["inf"] + + if cutoff is None: + cutoff = INF + + R_succ = R.succ + R_pred = R.pred + + def breath_first_search(): + parents = {} + vertex_dist = {s: 0} + queue = deque([(s, 0)]) + # Record all the potential edges of shortest augmenting paths + while queue: + if t in parents: + break + u, dist = queue.popleft() + for v, attr in R_succ[u].items(): + if attr["capacity"] - attr["flow"] > 0: + if v in parents: + if vertex_dist[v] == dist + 1: + parents[v].append(u) + else: + parents[v] = deque([u]) + vertex_dist[v] = dist + 1 + queue.append((v, dist + 1)) + return parents + + def depth_first_search(parents): + # DFS to find all the shortest augmenting paths + """Build a path using DFS starting from the sink""" + total_flow = 0 + u = t + # path also functions as a stack + path = [u] + # The loop ends with no augmenting path left in the layered graph + while True: + if len(parents[u]) > 0: + v = parents[u][0] + path.append(v) + else: + path.pop() + if len(path) == 0: + break + v = path[-1] + parents[v].popleft() + # Augment the flow along the path found + if v == s: + flow = INF + for u, v in pairwise(path): + flow = min(flow, R_pred[u][v]["capacity"] - R_pred[u][v]["flow"]) + for u, v in pairwise(reversed(path)): + R_pred[v][u]["flow"] += flow + R_pred[u][v]["flow"] -= flow + # Find the proper node to continue the search + if R_pred[v][u]["capacity"] - R_pred[v][u]["flow"] == 0: + parents[v].popleft() + while path[-1] != v: + path.pop() + total_flow += flow + v = path[-1] + u = v + return total_flow + + flow_value = 0 + while flow_value < cutoff: + parents = breath_first_search() + if t not in parents: + break + this_flow = depth_first_search(parents) + if this_flow * 2 > INF: + raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.") + flow_value += this_flow + + R.graph["flow_value"] = flow_value + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/edmondskarp.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/edmondskarp.py new file mode 100644 index 0000000000000000000000000000000000000000..50063268355ccc2e2ecbdf7f1a6704e7404475ec --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/edmondskarp.py @@ -0,0 +1,241 @@ +""" +Edmonds-Karp algorithm for maximum flow problems. +""" + +import networkx as nx +from networkx.algorithms.flow.utils import build_residual_network + +__all__ = ["edmonds_karp"] + + +def edmonds_karp_core(R, s, t, cutoff): + """Implementation of the Edmonds-Karp algorithm.""" + R_nodes = R.nodes + R_pred = R.pred + R_succ = R.succ + + inf = R.graph["inf"] + + def augment(path): + """Augment flow along a path from s to t.""" + # Determine the path residual capacity. + flow = inf + it = iter(path) + u = next(it) + for v in it: + attr = R_succ[u][v] + flow = min(flow, attr["capacity"] - attr["flow"]) + u = v + if flow * 2 > inf: + raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.") + # Augment flow along the path. + it = iter(path) + u = next(it) + for v in it: + R_succ[u][v]["flow"] += flow + R_succ[v][u]["flow"] -= flow + u = v + return flow + + def bidirectional_bfs(): + """Bidirectional breadth-first search for an augmenting path.""" + pred = {s: None} + q_s = [s] + succ = {t: None} + q_t = [t] + while True: + q = [] + if len(q_s) <= len(q_t): + for u in q_s: + for v, attr in R_succ[u].items(): + if v not in pred and attr["flow"] < attr["capacity"]: + pred[v] = u + if v in succ: + return v, pred, succ + q.append(v) + if not q: + return None, None, None + q_s = q + else: + for u in q_t: + for v, attr in R_pred[u].items(): + if v not in succ and attr["flow"] < attr["capacity"]: + succ[v] = u + if v in pred: + return v, pred, succ + q.append(v) + if not q: + return None, None, None + q_t = q + + # Look for shortest augmenting paths using breadth-first search. + flow_value = 0 + while flow_value < cutoff: + v, pred, succ = bidirectional_bfs() + if pred is None: + break + path = [v] + # Trace a path from s to v. + u = v + while u != s: + u = pred[u] + path.append(u) + path.reverse() + # Trace a path from v to t. + u = v + while u != t: + u = succ[u] + path.append(u) + flow_value += augment(path) + + return flow_value + + +def edmonds_karp_impl(G, s, t, capacity, residual, cutoff): + """Implementation of the Edmonds-Karp algorithm.""" + if s not in G: + raise nx.NetworkXError(f"node {str(s)} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {str(t)} not in graph") + if s == t: + raise nx.NetworkXError("source and sink are the same node") + + if residual is None: + R = build_residual_network(G, capacity) + else: + R = residual + + # Initialize/reset the residual network. + for u in R: + for e in R[u].values(): + e["flow"] = 0 + + if cutoff is None: + cutoff = float("inf") + R.graph["flow_value"] = edmonds_karp_core(R, s, t, cutoff) + + return R + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def edmonds_karp( + G, s, t, capacity="capacity", residual=None, value_only=False, cutoff=None +): + """Find a maximum single-commodity flow using the Edmonds-Karp algorithm. + + This function returns the residual network resulting after computing + the maximum flow. See below for details about the conventions + NetworkX uses for defining residual networks. + + This algorithm has a running time of $O(n m^2)$ for $n$ nodes and $m$ + edges. + + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + residual : NetworkX graph + Residual network on which the algorithm is to be executed. If None, a + new residual network is created. Default value: None. + + value_only : bool + If True compute only the value of the maximum flow. This parameter + will be ignored by this algorithm because it is not applicable. + + cutoff : integer, float + If specified, the algorithm will terminate when the flow value reaches + or exceeds the cutoff. In this case, it may be unable to immediately + determine a minimum cut. Default value: None. + + Returns + ------- + R : NetworkX DiGraph + Residual network after computing the maximum flow. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not + specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such + that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Examples + -------- + >>> from networkx.algorithms.flow import edmonds_karp + + The functions that implement flow algorithms and output a residual + network, such as this one, are not imported to the base NetworkX + namespace, so you have to explicitly import them from the flow package. + + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + >>> R = edmonds_karp(G, "x", "y") + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value + 3.0 + >>> flow_value == R.graph["flow_value"] + True + + """ + R = edmonds_karp_impl(G, s, t, capacity, residual, cutoff) + R.graph["algorithm"] = "edmonds_karp" + nx._clear_cache(R) + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/gomory_hu.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/gomory_hu.py new file mode 100644 index 0000000000000000000000000000000000000000..69913da904547b3a9fe682467b69e696e9c8e0dc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/gomory_hu.py @@ -0,0 +1,178 @@ +""" +Gomory-Hu tree of undirected Graphs. +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +from .edmondskarp import edmonds_karp +from .utils import build_residual_network + +default_flow_func = edmonds_karp + +__all__ = ["gomory_hu_tree"] + + +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def gomory_hu_tree(G, capacity="capacity", flow_func=None): + r"""Returns the Gomory-Hu tree of an undirected graph G. + + A Gomory-Hu tree of an undirected graph with capacities is a + weighted tree that represents the minimum s-t cuts for all s-t + pairs in the graph. + + It only requires `n-1` minimum cut computations instead of the + obvious `n(n-1)/2`. The tree represents all s-t cuts as the + minimum cut value among any pair of nodes is the minimum edge + weight in the shortest path between the two nodes in the + Gomory-Hu tree. + + The Gomory-Hu tree also has the property that removing the + edge with the minimum weight in the shortest path between + any two nodes leaves two connected components that form + a partition of the nodes in G that defines the minimum s-t + cut. + + See Examples section below for details. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + flow_func : function + Function to perform the underlying flow computations. Default value + :func:`edmonds_karp`. This function performs better in sparse graphs + with right tailed degree distributions. + :func:`shortest_augmenting_path` will perform better in denser + graphs. + + Returns + ------- + Tree : NetworkX graph + A NetworkX graph representing the Gomory-Hu tree of the input graph. + + Raises + ------ + NetworkXNotImplemented + Raised if the input graph is directed. + + NetworkXError + Raised if the input graph is an empty Graph. + + Examples + -------- + >>> G = nx.karate_club_graph() + >>> nx.set_edge_attributes(G, 1, "capacity") + >>> T = nx.gomory_hu_tree(G) + >>> # The value of the minimum cut between any pair + ... # of nodes in G is the minimum edge weight in the + ... # shortest path between the two nodes in the + ... # Gomory-Hu tree. + ... def minimum_edge_weight_in_shortest_path(T, u, v): + ... path = nx.shortest_path(T, u, v, weight="weight") + ... return min((T[u][v]["weight"], (u, v)) for (u, v) in zip(path, path[1:])) + >>> u, v = 0, 33 + >>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v) + >>> cut_value + 10 + >>> nx.minimum_cut_value(G, u, v) + 10 + >>> # The Gomory-Hu tree also has the property that removing the + ... # edge with the minimum weight in the shortest path between + ... # any two nodes leaves two connected components that form + ... # a partition of the nodes in G that defines the minimum s-t + ... # cut. + ... cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v) + >>> T.remove_edge(*edge) + >>> U, V = list(nx.connected_components(T)) + >>> # Thus U and V form a partition that defines a minimum cut + ... # between u and v in G. You can compute the edge cut set, + ... # that is, the set of edges that if removed from G will + ... # disconnect u from v in G, with this information: + ... cutset = set() + >>> for x, nbrs in ((n, G[n]) for n in U): + ... cutset.update((x, y) for y in nbrs if y in V) + >>> # Because we have set the capacities of all edges to 1 + ... # the cutset contains ten edges + ... len(cutset) + 10 + >>> # You can use any maximum flow algorithm for the underlying + ... # flow computations using the argument flow_func + ... from networkx.algorithms import flow + >>> T = nx.gomory_hu_tree(G, flow_func=flow.boykov_kolmogorov) + >>> cut_value, edge = minimum_edge_weight_in_shortest_path(T, u, v) + >>> cut_value + 10 + >>> nx.minimum_cut_value(G, u, v, flow_func=flow.boykov_kolmogorov) + 10 + + Notes + ----- + This implementation is based on Gusfield approach [1]_ to compute + Gomory-Hu trees, which does not require node contractions and has + the same computational complexity than the original method. + + See also + -------- + :func:`minimum_cut` + :func:`maximum_flow` + + References + ---------- + .. [1] Gusfield D: Very simple methods for all pairs network flow analysis. + SIAM J Comput 19(1):143-155, 1990. + + """ + if flow_func is None: + flow_func = default_flow_func + + if len(G) == 0: # empty graph + msg = "Empty Graph does not have a Gomory-Hu tree representation" + raise nx.NetworkXError(msg) + + # Start the tree as a star graph with an arbitrary node at the center + tree = {} + labels = {} + iter_nodes = iter(G) + root = next(iter_nodes) + for n in iter_nodes: + tree[n] = root + + # Reuse residual network + R = build_residual_network(G, capacity) + + # For all the leaves in the star graph tree (that is n-1 nodes). + for source in tree: + # Find neighbor in the tree + target = tree[source] + # compute minimum cut + cut_value, partition = nx.minimum_cut( + G, source, target, capacity=capacity, flow_func=flow_func, residual=R + ) + labels[(source, target)] = cut_value + # Update the tree + # Source will always be in partition[0] and target in partition[1] + for node in partition[0]: + if node != source and node in tree and tree[node] == target: + tree[node] = source + labels[node, source] = labels.get((node, target), cut_value) + # + if target != root and tree[target] in partition[0]: + labels[source, tree[target]] = labels[target, tree[target]] + labels[target, source] = cut_value + tree[source] = tree[target] + tree[target] = source + + # Build the tree + T = nx.Graph() + T.add_nodes_from(G) + T.add_weighted_edges_from(((u, v, labels[u, v]) for u, v in tree.items())) + return T diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/maxflow.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/maxflow.py new file mode 100644 index 0000000000000000000000000000000000000000..93497a473b12bed8c80ffad992552bfeca2d4614 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/maxflow.py @@ -0,0 +1,611 @@ +""" +Maximum flow (and minimum cut) algorithms on capacitated graphs. +""" + +import networkx as nx + +from .boykovkolmogorov import boykov_kolmogorov +from .dinitz_alg import dinitz +from .edmondskarp import edmonds_karp +from .preflowpush import preflow_push +from .shortestaugmentingpath import shortest_augmenting_path +from .utils import build_flow_dict + +# Define the default flow function for computing maximum flow. +default_flow_func = preflow_push + +__all__ = ["maximum_flow", "maximum_flow_value", "minimum_cut", "minimum_cut_value"] + + +@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")}) +def maximum_flow(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs): + """Find a maximum single-commodity flow. + + Parameters + ---------- + flowG : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + _s : node + Source node for the flow. + + _t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + flow_func : function + A function for computing the maximum flow among a pair of nodes + in a capacitated graph. The function has to accept at least three + parameters: a Graph or Digraph, a source node, and a target node. + And return a residual network that follows NetworkX conventions + (see Notes). If flow_func is None, the default maximum + flow function (:meth:`preflow_push`) is used. See below for + alternative algorithms. The choice of the default function may change + from version to version and should not be relied on. Default value: + None. + + kwargs : Any other keyword parameter is passed to the function that + computes the maximum flow. + + Returns + ------- + flow_value : integer, float + Value of the maximum flow, i.e., net outflow from the source. + + flow_dict : dict + A dictionary containing the value of the flow that went through + each edge. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow_value` + :meth:`minimum_cut` + :meth:`minimum_cut_value` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The function used in the flow_func parameter has to return a residual + network that follows NetworkX conventions: + + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using + only edges :samp:`(u, v)` such that + :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Specific algorithms may store extra data in :samp:`R`. + + The function should supports an optional boolean parameter value_only. When + True, it can optionally terminate the algorithm as soon as the maximum flow + value and the minimum cut can be determined. + + Note that the resulting maximum flow may contain flow cycles, + back-flow to the source, or some flow exiting the sink. + These are possible if there are cycles in the network. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + + maximum_flow returns both the value of the maximum flow and a + dictionary with all flows. + + >>> flow_value, flow_dict = nx.maximum_flow(G, "x", "y") + >>> flow_value + 3.0 + >>> print(flow_dict["x"]["b"]) + 1.0 + + You can also use alternative algorithms for computing the + maximum flow by using the flow_func parameter. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> flow_value == nx.maximum_flow(G, "x", "y", flow_func=shortest_augmenting_path)[ + ... 0 + ... ] + True + + """ + if flow_func is None: + if kwargs: + raise nx.NetworkXError( + "You have to explicitly set a flow_func if" + " you need to pass parameters via kwargs." + ) + flow_func = default_flow_func + + if not callable(flow_func): + raise nx.NetworkXError("flow_func has to be callable.") + + R = flow_func(flowG, _s, _t, capacity=capacity, value_only=False, **kwargs) + flow_dict = build_flow_dict(flowG, R) + + return (R.graph["flow_value"], flow_dict) + + +@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")}) +def maximum_flow_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs): + """Find the value of maximum single-commodity flow. + + Parameters + ---------- + flowG : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + _s : node + Source node for the flow. + + _t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + flow_func : function + A function for computing the maximum flow among a pair of nodes + in a capacitated graph. The function has to accept at least three + parameters: a Graph or Digraph, a source node, and a target node. + And return a residual network that follows NetworkX conventions + (see Notes). If flow_func is None, the default maximum + flow function (:meth:`preflow_push`) is used. See below for + alternative algorithms. The choice of the default function may change + from version to version and should not be relied on. Default value: + None. + + kwargs : Any other keyword parameter is passed to the function that + computes the maximum flow. + + Returns + ------- + flow_value : integer, float + Value of the maximum flow, i.e., net outflow from the source. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`minimum_cut_value` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The function used in the flow_func parameter has to return a residual + network that follows NetworkX conventions: + + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using + only edges :samp:`(u, v)` such that + :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Specific algorithms may store extra data in :samp:`R`. + + The function should supports an optional boolean parameter value_only. When + True, it can optionally terminate the algorithm as soon as the maximum flow + value and the minimum cut can be determined. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + + maximum_flow_value computes only the value of the + maximum flow: + + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value + 3.0 + + You can also use alternative algorithms for computing the + maximum flow by using the flow_func parameter. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> flow_value == nx.maximum_flow_value( + ... G, "x", "y", flow_func=shortest_augmenting_path + ... ) + True + + """ + if flow_func is None: + if kwargs: + raise nx.NetworkXError( + "You have to explicitly set a flow_func if" + " you need to pass parameters via kwargs." + ) + flow_func = default_flow_func + + if not callable(flow_func): + raise nx.NetworkXError("flow_func has to be callable.") + + R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs) + + return R.graph["flow_value"] + + +@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")}) +def minimum_cut(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs): + """Compute the value and the node partition of a minimum (s, t)-cut. + + Use the max-flow min-cut theorem, i.e., the capacity of a minimum + capacity cut is equal to the flow value of a maximum flow. + + Parameters + ---------- + flowG : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + _s : node + Source node for the flow. + + _t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + flow_func : function + A function for computing the maximum flow among a pair of nodes + in a capacitated graph. The function has to accept at least three + parameters: a Graph or Digraph, a source node, and a target node. + And return a residual network that follows NetworkX conventions + (see Notes). If flow_func is None, the default maximum + flow function (:meth:`preflow_push`) is used. See below for + alternative algorithms. The choice of the default function may change + from version to version and should not be relied on. Default value: + None. + + kwargs : Any other keyword parameter is passed to the function that + computes the maximum flow. + + Returns + ------- + cut_value : integer, float + Value of the minimum cut. + + partition : pair of node sets + A partitioning of the nodes that defines a minimum cut. + + Raises + ------ + NetworkXUnbounded + If the graph has a path of infinite capacity, all cuts have + infinite capacity and the function raises a NetworkXError. + + See also + -------- + :meth:`maximum_flow` + :meth:`maximum_flow_value` + :meth:`minimum_cut_value` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The function used in the flow_func parameter has to return a residual + network that follows NetworkX conventions: + + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using + only edges :samp:`(u, v)` such that + :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Specific algorithms may store extra data in :samp:`R`. + + The function should supports an optional boolean parameter value_only. When + True, it can optionally terminate the algorithm as soon as the maximum flow + value and the minimum cut can be determined. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + + minimum_cut computes both the value of the + minimum cut and the node partition: + + >>> cut_value, partition = nx.minimum_cut(G, "x", "y") + >>> reachable, non_reachable = partition + + 'partition' here is a tuple with the two sets of nodes that define + the minimum cut. You can compute the cut set of edges that induce + the minimum cut as follows: + + >>> cutset = set() + >>> for u, nbrs in ((n, G[n]) for n in reachable): + ... cutset.update((u, v) for v in nbrs if v in non_reachable) + >>> print(sorted(cutset)) + [('c', 'y'), ('x', 'b')] + >>> cut_value == sum(G.edges[u, v]["capacity"] for (u, v) in cutset) + True + + You can also use alternative algorithms for computing the + minimum cut by using the flow_func parameter. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> cut_value == nx.minimum_cut(G, "x", "y", flow_func=shortest_augmenting_path)[0] + True + + """ + if flow_func is None: + if kwargs: + raise nx.NetworkXError( + "You have to explicitly set a flow_func if" + " you need to pass parameters via kwargs." + ) + flow_func = default_flow_func + + if not callable(flow_func): + raise nx.NetworkXError("flow_func has to be callable.") + + if kwargs.get("cutoff") is not None and flow_func is preflow_push: + raise nx.NetworkXError("cutoff should not be specified.") + + R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs) + # Remove saturated edges from the residual network + cutset = [(u, v, d) for u, v, d in R.edges(data=True) if d["flow"] == d["capacity"]] + R.remove_edges_from(cutset) + + # Then, reachable and non reachable nodes from source in the + # residual network form the node partition that defines + # the minimum cut. + non_reachable = set(nx.shortest_path_length(R, target=_t)) + partition = (set(flowG) - non_reachable, non_reachable) + # Finally add again cutset edges to the residual network to make + # sure that it is reusable. + R.add_edges_from(cutset) + return (R.graph["flow_value"], partition) + + +@nx._dispatchable(graphs="flowG", edge_attrs={"capacity": float("inf")}) +def minimum_cut_value(flowG, _s, _t, capacity="capacity", flow_func=None, **kwargs): + """Compute the value of a minimum (s, t)-cut. + + Use the max-flow min-cut theorem, i.e., the capacity of a minimum + capacity cut is equal to the flow value of a maximum flow. + + Parameters + ---------- + flowG : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + _s : node + Source node for the flow. + + _t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + flow_func : function + A function for computing the maximum flow among a pair of nodes + in a capacitated graph. The function has to accept at least three + parameters: a Graph or Digraph, a source node, and a target node. + And return a residual network that follows NetworkX conventions + (see Notes). If flow_func is None, the default maximum + flow function (:meth:`preflow_push`) is used. See below for + alternative algorithms. The choice of the default function may change + from version to version and should not be relied on. Default value: + None. + + kwargs : Any other keyword parameter is passed to the function that + computes the maximum flow. + + Returns + ------- + cut_value : integer, float + Value of the minimum cut. + + Raises + ------ + NetworkXUnbounded + If the graph has a path of infinite capacity, all cuts have + infinite capacity and the function raises a NetworkXError. + + See also + -------- + :meth:`maximum_flow` + :meth:`maximum_flow_value` + :meth:`minimum_cut` + :meth:`edmonds_karp` + :meth:`preflow_push` + :meth:`shortest_augmenting_path` + + Notes + ----- + The function used in the flow_func parameter has to return a residual + network that follows NetworkX conventions: + + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using + only edges :samp:`(u, v)` such that + :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Specific algorithms may store extra data in :samp:`R`. + + The function should supports an optional boolean parameter value_only. When + True, it can optionally terminate the algorithm as soon as the maximum flow + value and the minimum cut can be determined. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + + minimum_cut_value computes only the value of the + minimum cut: + + >>> cut_value = nx.minimum_cut_value(G, "x", "y") + >>> cut_value + 3.0 + + You can also use alternative algorithms for computing the + minimum cut by using the flow_func parameter. + + >>> from networkx.algorithms.flow import shortest_augmenting_path + >>> cut_value == nx.minimum_cut_value( + ... G, "x", "y", flow_func=shortest_augmenting_path + ... ) + True + + """ + if flow_func is None: + if kwargs: + raise nx.NetworkXError( + "You have to explicitly set a flow_func if" + " you need to pass parameters via kwargs." + ) + flow_func = default_flow_func + + if not callable(flow_func): + raise nx.NetworkXError("flow_func has to be callable.") + + if kwargs.get("cutoff") is not None and flow_func is preflow_push: + raise nx.NetworkXError("cutoff should not be specified.") + + R = flow_func(flowG, _s, _t, capacity=capacity, value_only=True, **kwargs) + + return R.graph["flow_value"] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/mincost.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/mincost.py new file mode 100644 index 0000000000000000000000000000000000000000..2f9390d7a1c1e454ed7c2f8793d591b338115107 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/mincost.py @@ -0,0 +1,356 @@ +""" +Minimum cost flow algorithms on directed connected graphs. +""" + +__all__ = ["min_cost_flow_cost", "min_cost_flow", "cost_of_flow", "max_flow_min_cost"] + +import networkx as nx + + +@nx._dispatchable( + node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0} +) +def min_cost_flow_cost(G, demand="demand", capacity="capacity", weight="weight"): + r"""Find the cost of a minimum cost flow satisfying all demands in digraph G. + + G is a digraph with edge costs and capacities and in which nodes + have demand, i.e., they want to send or receive some amount of + flow. A negative demand means that the node wants to send flow, a + positive demand means that the node want to receive flow. A flow on + the digraph G satisfies all demand if the net flow into each node + is equal to the demand of that node. + + Parameters + ---------- + G : NetworkX graph + DiGraph on which a minimum cost flow satisfying all demands is + to be found. + + demand : string + Nodes of the graph G are expected to have an attribute demand + that indicates how much flow a node wants to send (negative + demand) or receive (positive demand). Note that the sum of the + demands should be 0 otherwise the problem in not feasible. If + this attribute is not present, a node is considered to have 0 + demand. Default value: 'demand'. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + weight : string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + Returns + ------- + flowCost : integer, float + Cost of a minimum cost flow satisfying all demands. + + Raises + ------ + NetworkXError + This exception is raised if the input graph is not directed or + not connected. + + NetworkXUnfeasible + This exception is raised in the following situations: + + * The sum of the demands is not zero. Then, there is no + flow satisfying all demands. + * There is no flow satisfying all demand. + + NetworkXUnbounded + This exception is raised if the digraph G has a cycle of + negative cost and infinite capacity. Then, the cost of a flow + satisfying all demands is unbounded below. + + See also + -------- + cost_of_flow, max_flow_min_cost, min_cost_flow, network_simplex + + Notes + ----- + This algorithm is not guaranteed to work if edge weights or demands + are floating point numbers (overflows and roundoff errors can + cause problems). As a workaround you can use integer numbers by + multiplying the relevant edge attributes by a convenient + constant factor (eg 100). + + Examples + -------- + A simple example of a min cost flow problem. + + >>> G = nx.DiGraph() + >>> G.add_node("a", demand=-5) + >>> G.add_node("d", demand=5) + >>> G.add_edge("a", "b", weight=3, capacity=4) + >>> G.add_edge("a", "c", weight=6, capacity=10) + >>> G.add_edge("b", "d", weight=1, capacity=9) + >>> G.add_edge("c", "d", weight=2, capacity=5) + >>> flowCost = nx.min_cost_flow_cost(G) + >>> flowCost + 24 + """ + return nx.network_simplex(G, demand=demand, capacity=capacity, weight=weight)[0] + + +@nx._dispatchable( + node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0} +) +def min_cost_flow(G, demand="demand", capacity="capacity", weight="weight"): + r"""Returns a minimum cost flow satisfying all demands in digraph G. + + G is a digraph with edge costs and capacities and in which nodes + have demand, i.e., they want to send or receive some amount of + flow. A negative demand means that the node wants to send flow, a + positive demand means that the node want to receive flow. A flow on + the digraph G satisfies all demand if the net flow into each node + is equal to the demand of that node. + + Parameters + ---------- + G : NetworkX graph + DiGraph on which a minimum cost flow satisfying all demands is + to be found. + + demand : string + Nodes of the graph G are expected to have an attribute demand + that indicates how much flow a node wants to send (negative + demand) or receive (positive demand). Note that the sum of the + demands should be 0 otherwise the problem in not feasible. If + this attribute is not present, a node is considered to have 0 + demand. Default value: 'demand'. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + weight : string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + Returns + ------- + flowDict : dictionary + Dictionary of dictionaries keyed by nodes such that + flowDict[u][v] is the flow edge (u, v). + + Raises + ------ + NetworkXError + This exception is raised if the input graph is not directed or + not connected. + + NetworkXUnfeasible + This exception is raised in the following situations: + + * The sum of the demands is not zero. Then, there is no + flow satisfying all demands. + * There is no flow satisfying all demand. + + NetworkXUnbounded + This exception is raised if the digraph G has a cycle of + negative cost and infinite capacity. Then, the cost of a flow + satisfying all demands is unbounded below. + + See also + -------- + cost_of_flow, max_flow_min_cost, min_cost_flow_cost, network_simplex + + Notes + ----- + This algorithm is not guaranteed to work if edge weights or demands + are floating point numbers (overflows and roundoff errors can + cause problems). As a workaround you can use integer numbers by + multiplying the relevant edge attributes by a convenient + constant factor (eg 100). + + Examples + -------- + A simple example of a min cost flow problem. + + >>> G = nx.DiGraph() + >>> G.add_node("a", demand=-5) + >>> G.add_node("d", demand=5) + >>> G.add_edge("a", "b", weight=3, capacity=4) + >>> G.add_edge("a", "c", weight=6, capacity=10) + >>> G.add_edge("b", "d", weight=1, capacity=9) + >>> G.add_edge("c", "d", weight=2, capacity=5) + >>> flowDict = nx.min_cost_flow(G) + >>> flowDict + {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}} + """ + return nx.network_simplex(G, demand=demand, capacity=capacity, weight=weight)[1] + + +@nx._dispatchable(edge_attrs={"weight": 0}) +def cost_of_flow(G, flowDict, weight="weight"): + """Compute the cost of the flow given by flowDict on graph G. + + Note that this function does not check for the validity of the + flow flowDict. This function will fail if the graph G and the + flow don't have the same edge set. + + Parameters + ---------- + G : NetworkX graph + DiGraph on which a minimum cost flow satisfying all demands is + to be found. + + weight : string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + flowDict : dictionary + Dictionary of dictionaries keyed by nodes such that + flowDict[u][v] is the flow edge (u, v). + + Returns + ------- + cost : Integer, float + The total cost of the flow. This is given by the sum over all + edges of the product of the edge's flow and the edge's weight. + + See also + -------- + max_flow_min_cost, min_cost_flow, min_cost_flow_cost, network_simplex + + Notes + ----- + This algorithm is not guaranteed to work if edge weights or demands + are floating point numbers (overflows and roundoff errors can + cause problems). As a workaround you can use integer numbers by + multiplying the relevant edge attributes by a convenient + constant factor (eg 100). + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_node("a", demand=-5) + >>> G.add_node("d", demand=5) + >>> G.add_edge("a", "b", weight=3, capacity=4) + >>> G.add_edge("a", "c", weight=6, capacity=10) + >>> G.add_edge("b", "d", weight=1, capacity=9) + >>> G.add_edge("c", "d", weight=2, capacity=5) + >>> flowDict = nx.min_cost_flow(G) + >>> flowDict + {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}} + >>> nx.cost_of_flow(G, flowDict) + 24 + """ + return sum((flowDict[u][v] * d.get(weight, 0) for u, v, d in G.edges(data=True))) + + +@nx._dispatchable(edge_attrs={"capacity": float("inf"), "weight": 0}) +def max_flow_min_cost(G, s, t, capacity="capacity", weight="weight"): + """Returns a maximum (s, t)-flow of minimum cost. + + G is a digraph with edge costs and capacities. There is a source + node s and a sink node t. This function finds a maximum flow from + s to t whose total cost is minimized. + + Parameters + ---------- + G : NetworkX graph + DiGraph on which a minimum cost flow satisfying all demands is + to be found. + + s: node label + Source of the flow. + + t: node label + Destination of the flow. + + capacity: string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + weight: string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + Returns + ------- + flowDict: dictionary + Dictionary of dictionaries keyed by nodes such that + flowDict[u][v] is the flow edge (u, v). + + Raises + ------ + NetworkXError + This exception is raised if the input graph is not directed or + not connected. + + NetworkXUnbounded + This exception is raised if there is an infinite capacity path + from s to t in G. In this case there is no maximum flow. This + exception is also raised if the digraph G has a cycle of + negative cost and infinite capacity. Then, the cost of a flow + is unbounded below. + + See also + -------- + cost_of_flow, min_cost_flow, min_cost_flow_cost, network_simplex + + Notes + ----- + This algorithm is not guaranteed to work if edge weights or demands + are floating point numbers (overflows and roundoff errors can + cause problems). As a workaround you can use integer numbers by + multiplying the relevant edge attributes by a convenient + constant factor (eg 100). + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edges_from( + ... [ + ... (1, 2, {"capacity": 12, "weight": 4}), + ... (1, 3, {"capacity": 20, "weight": 6}), + ... (2, 3, {"capacity": 6, "weight": -3}), + ... (2, 6, {"capacity": 14, "weight": 1}), + ... (3, 4, {"weight": 9}), + ... (3, 5, {"capacity": 10, "weight": 5}), + ... (4, 2, {"capacity": 19, "weight": 13}), + ... (4, 5, {"capacity": 4, "weight": 0}), + ... (5, 7, {"capacity": 28, "weight": 2}), + ... (6, 5, {"capacity": 11, "weight": 1}), + ... (6, 7, {"weight": 8}), + ... (7, 4, {"capacity": 6, "weight": 6}), + ... ] + ... ) + >>> mincostFlow = nx.max_flow_min_cost(G, 1, 7) + >>> mincost = nx.cost_of_flow(G, mincostFlow) + >>> mincost + 373 + >>> from networkx.algorithms.flow import maximum_flow + >>> maxFlow = maximum_flow(G, 1, 7)[1] + >>> nx.cost_of_flow(G, maxFlow) >= mincost + True + >>> mincostFlowValue = sum((mincostFlow[u][7] for u in G.predecessors(7))) - sum( + ... (mincostFlow[7][v] for v in G.successors(7)) + ... ) + >>> mincostFlowValue == nx.maximum_flow_value(G, 1, 7) + True + + """ + maxFlow = nx.maximum_flow_value(G, s, t, capacity=capacity) + H = nx.DiGraph(G) + H.add_node(s, demand=-maxFlow) + H.add_node(t, demand=maxFlow) + return min_cost_flow(H, capacity=capacity, weight=weight) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/networksimplex.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/networksimplex.py new file mode 100644 index 0000000000000000000000000000000000000000..5baa9766c39c3ac2e02e396879461668cc62bfc7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/networksimplex.py @@ -0,0 +1,662 @@ +""" +Minimum cost flow algorithms on directed connected graphs. +""" + +__all__ = ["network_simplex"] + +from itertools import chain, islice, repeat +from math import ceil, sqrt + +import networkx as nx +from networkx.utils import not_implemented_for + + +class _DataEssentialsAndFunctions: + def __init__( + self, G, multigraph, demand="demand", capacity="capacity", weight="weight" + ): + # Number all nodes and edges and hereafter reference them using ONLY their numbers + self.node_list = list(G) # nodes + self.node_indices = {u: i for i, u in enumerate(self.node_list)} # node indices + self.node_demands = [ + G.nodes[u].get(demand, 0) for u in self.node_list + ] # node demands + + self.edge_sources = [] # edge sources + self.edge_targets = [] # edge targets + if multigraph: + self.edge_keys = [] # edge keys + self.edge_indices = {} # edge indices + self.edge_capacities = [] # edge capacities + self.edge_weights = [] # edge weights + + if not multigraph: + edges = G.edges(data=True) + else: + edges = G.edges(data=True, keys=True) + + inf = float("inf") + edges = (e for e in edges if e[0] != e[1] and e[-1].get(capacity, inf) != 0) + for i, e in enumerate(edges): + self.edge_sources.append(self.node_indices[e[0]]) + self.edge_targets.append(self.node_indices[e[1]]) + if multigraph: + self.edge_keys.append(e[2]) + self.edge_indices[e[:-1]] = i + self.edge_capacities.append(e[-1].get(capacity, inf)) + self.edge_weights.append(e[-1].get(weight, 0)) + + # spanning tree specific data to be initialized + + self.edge_count = None # number of edges + self.edge_flow = None # edge flows + self.node_potentials = None # node potentials + self.parent = None # parent nodes + self.parent_edge = None # edges to parents + self.subtree_size = None # subtree sizes + self.next_node_dft = None # next nodes in depth-first thread + self.prev_node_dft = None # previous nodes in depth-first thread + self.last_descendent_dft = None # last descendants in depth-first thread + self._spanning_tree_initialized = ( + False # False until initialize_spanning_tree() is called + ) + + def initialize_spanning_tree(self, n, faux_inf): + self.edge_count = len(self.edge_indices) # number of edges + self.edge_flow = list( + chain(repeat(0, self.edge_count), (abs(d) for d in self.node_demands)) + ) # edge flows + self.node_potentials = [ + faux_inf if d <= 0 else -faux_inf for d in self.node_demands + ] # node potentials + self.parent = list(chain(repeat(-1, n), [None])) # parent nodes + self.parent_edge = list( + range(self.edge_count, self.edge_count + n) + ) # edges to parents + self.subtree_size = list(chain(repeat(1, n), [n + 1])) # subtree sizes + self.next_node_dft = list( + chain(range(1, n), [-1, 0]) + ) # next nodes in depth-first thread + self.prev_node_dft = list(range(-1, n)) # previous nodes in depth-first thread + self.last_descendent_dft = list( + chain(range(n), [n - 1]) + ) # last descendants in depth-first thread + self._spanning_tree_initialized = True # True only if all the assignments pass + + def find_apex(self, p, q): + """ + Find the lowest common ancestor of nodes p and q in the spanning tree. + """ + size_p = self.subtree_size[p] + size_q = self.subtree_size[q] + while True: + while size_p < size_q: + p = self.parent[p] + size_p = self.subtree_size[p] + while size_p > size_q: + q = self.parent[q] + size_q = self.subtree_size[q] + if size_p == size_q: + if p != q: + p = self.parent[p] + size_p = self.subtree_size[p] + q = self.parent[q] + size_q = self.subtree_size[q] + else: + return p + + def trace_path(self, p, w): + """ + Returns the nodes and edges on the path from node p to its ancestor w. + """ + Wn = [p] + We = [] + while p != w: + We.append(self.parent_edge[p]) + p = self.parent[p] + Wn.append(p) + return Wn, We + + def find_cycle(self, i, p, q): + """ + Returns the nodes and edges on the cycle containing edge i == (p, q) + when the latter is added to the spanning tree. + + The cycle is oriented in the direction from p to q. + """ + w = self.find_apex(p, q) + Wn, We = self.trace_path(p, w) + Wn.reverse() + We.reverse() + if We != [i]: + We.append(i) + WnR, WeR = self.trace_path(q, w) + del WnR[-1] + Wn += WnR + We += WeR + return Wn, We + + def augment_flow(self, Wn, We, f): + """ + Augment f units of flow along a cycle represented by Wn and We. + """ + for i, p in zip(We, Wn): + if self.edge_sources[i] == p: + self.edge_flow[i] += f + else: + self.edge_flow[i] -= f + + def trace_subtree(self, p): + """ + Yield the nodes in the subtree rooted at a node p. + """ + yield p + l = self.last_descendent_dft[p] + while p != l: + p = self.next_node_dft[p] + yield p + + def remove_edge(self, s, t): + """ + Remove an edge (s, t) where parent[t] == s from the spanning tree. + """ + size_t = self.subtree_size[t] + prev_t = self.prev_node_dft[t] + last_t = self.last_descendent_dft[t] + next_last_t = self.next_node_dft[last_t] + # Remove (s, t). + self.parent[t] = None + self.parent_edge[t] = None + # Remove the subtree rooted at t from the depth-first thread. + self.next_node_dft[prev_t] = next_last_t + self.prev_node_dft[next_last_t] = prev_t + self.next_node_dft[last_t] = t + self.prev_node_dft[t] = last_t + # Update the subtree sizes and last descendants of the (old) ancestors + # of t. + while s is not None: + self.subtree_size[s] -= size_t + if self.last_descendent_dft[s] == last_t: + self.last_descendent_dft[s] = prev_t + s = self.parent[s] + + def make_root(self, q): + """ + Make a node q the root of its containing subtree. + """ + ancestors = [] + while q is not None: + ancestors.append(q) + q = self.parent[q] + ancestors.reverse() + for p, q in zip(ancestors, islice(ancestors, 1, None)): + size_p = self.subtree_size[p] + last_p = self.last_descendent_dft[p] + prev_q = self.prev_node_dft[q] + last_q = self.last_descendent_dft[q] + next_last_q = self.next_node_dft[last_q] + # Make p a child of q. + self.parent[p] = q + self.parent[q] = None + self.parent_edge[p] = self.parent_edge[q] + self.parent_edge[q] = None + self.subtree_size[p] = size_p - self.subtree_size[q] + self.subtree_size[q] = size_p + # Remove the subtree rooted at q from the depth-first thread. + self.next_node_dft[prev_q] = next_last_q + self.prev_node_dft[next_last_q] = prev_q + self.next_node_dft[last_q] = q + self.prev_node_dft[q] = last_q + if last_p == last_q: + self.last_descendent_dft[p] = prev_q + last_p = prev_q + # Add the remaining parts of the subtree rooted at p as a subtree + # of q in the depth-first thread. + self.prev_node_dft[p] = last_q + self.next_node_dft[last_q] = p + self.next_node_dft[last_p] = q + self.prev_node_dft[q] = last_p + self.last_descendent_dft[q] = last_p + + def add_edge(self, i, p, q): + """ + Add an edge (p, q) to the spanning tree where q is the root of a subtree. + """ + last_p = self.last_descendent_dft[p] + next_last_p = self.next_node_dft[last_p] + size_q = self.subtree_size[q] + last_q = self.last_descendent_dft[q] + # Make q a child of p. + self.parent[q] = p + self.parent_edge[q] = i + # Insert the subtree rooted at q into the depth-first thread. + self.next_node_dft[last_p] = q + self.prev_node_dft[q] = last_p + self.prev_node_dft[next_last_p] = last_q + self.next_node_dft[last_q] = next_last_p + # Update the subtree sizes and last descendants of the (new) ancestors + # of q. + while p is not None: + self.subtree_size[p] += size_q + if self.last_descendent_dft[p] == last_p: + self.last_descendent_dft[p] = last_q + p = self.parent[p] + + def update_potentials(self, i, p, q): + """ + Update the potentials of the nodes in the subtree rooted at a node + q connected to its parent p by an edge i. + """ + if q == self.edge_targets[i]: + d = self.node_potentials[p] - self.edge_weights[i] - self.node_potentials[q] + else: + d = self.node_potentials[p] + self.edge_weights[i] - self.node_potentials[q] + for q in self.trace_subtree(q): + self.node_potentials[q] += d + + def reduced_cost(self, i): + """Returns the reduced cost of an edge i.""" + c = ( + self.edge_weights[i] + - self.node_potentials[self.edge_sources[i]] + + self.node_potentials[self.edge_targets[i]] + ) + return c if self.edge_flow[i] == 0 else -c + + def find_entering_edges(self): + """Yield entering edges until none can be found.""" + if self.edge_count == 0: + return + + # Entering edges are found by combining Dantzig's rule and Bland's + # rule. The edges are cyclically grouped into blocks of size B. Within + # each block, Dantzig's rule is applied to find an entering edge. The + # blocks to search is determined following Bland's rule. + B = int(ceil(sqrt(self.edge_count))) # pivot block size + M = (self.edge_count + B - 1) // B # number of blocks needed to cover all edges + m = 0 # number of consecutive blocks without eligible + # entering edges + f = 0 # first edge in block + while m < M: + # Determine the next block of edges. + l = f + B + if l <= self.edge_count: + edges = range(f, l) + else: + l -= self.edge_count + edges = chain(range(f, self.edge_count), range(l)) + f = l + # Find the first edge with the lowest reduced cost. + i = min(edges, key=self.reduced_cost) + c = self.reduced_cost(i) + if c >= 0: + # No entering edge found in the current block. + m += 1 + else: + # Entering edge found. + if self.edge_flow[i] == 0: + p = self.edge_sources[i] + q = self.edge_targets[i] + else: + p = self.edge_targets[i] + q = self.edge_sources[i] + yield i, p, q + m = 0 + # All edges have nonnegative reduced costs. The current flow is + # optimal. + + def residual_capacity(self, i, p): + """Returns the residual capacity of an edge i in the direction away + from its endpoint p. + """ + return ( + self.edge_capacities[i] - self.edge_flow[i] + if self.edge_sources[i] == p + else self.edge_flow[i] + ) + + def find_leaving_edge(self, Wn, We): + """Returns the leaving edge in a cycle represented by Wn and We.""" + j, s = min( + zip(reversed(We), reversed(Wn)), + key=lambda i_p: self.residual_capacity(*i_p), + ) + t = self.edge_targets[j] if self.edge_sources[j] == s else self.edge_sources[j] + return j, s, t + + +@not_implemented_for("undirected") +@nx._dispatchable( + node_attrs="demand", edge_attrs={"capacity": float("inf"), "weight": 0} +) +def network_simplex(G, demand="demand", capacity="capacity", weight="weight"): + r"""Find a minimum cost flow satisfying all demands in digraph G. + + This is a primal network simplex algorithm that uses the leaving + arc rule to prevent cycling. + + G is a digraph with edge costs and capacities and in which nodes + have demand, i.e., they want to send or receive some amount of + flow. A negative demand means that the node wants to send flow, a + positive demand means that the node want to receive flow. A flow on + the digraph G satisfies all demand if the net flow into each node + is equal to the demand of that node. + + Parameters + ---------- + G : NetworkX graph + DiGraph on which a minimum cost flow satisfying all demands is + to be found. + + demand : string + Nodes of the graph G are expected to have an attribute demand + that indicates how much flow a node wants to send (negative + demand) or receive (positive demand). Note that the sum of the + demands should be 0 otherwise the problem in not feasible. If + this attribute is not present, a node is considered to have 0 + demand. Default value: 'demand'. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + weight : string + Edges of the graph G are expected to have an attribute weight + that indicates the cost incurred by sending one unit of flow on + that edge. If not present, the weight is considered to be 0. + Default value: 'weight'. + + Returns + ------- + flowCost : integer, float + Cost of a minimum cost flow satisfying all demands. + + flowDict : dictionary + Dictionary of dictionaries keyed by nodes such that + flowDict[u][v] is the flow edge (u, v). + + Raises + ------ + NetworkXError + This exception is raised if the input graph is not directed or + not connected. + + NetworkXUnfeasible + This exception is raised in the following situations: + + * The sum of the demands is not zero. Then, there is no + flow satisfying all demands. + * There is no flow satisfying all demand. + + NetworkXUnbounded + This exception is raised if the digraph G has a cycle of + negative cost and infinite capacity. Then, the cost of a flow + satisfying all demands is unbounded below. + + Notes + ----- + This algorithm is not guaranteed to work if edge weights or demands + are floating point numbers (overflows and roundoff errors can + cause problems). As a workaround you can use integer numbers by + multiplying the relevant edge attributes by a convenient + constant factor (eg 100). + + See also + -------- + cost_of_flow, max_flow_min_cost, min_cost_flow, min_cost_flow_cost + + Examples + -------- + A simple example of a min cost flow problem. + + >>> G = nx.DiGraph() + >>> G.add_node("a", demand=-5) + >>> G.add_node("d", demand=5) + >>> G.add_edge("a", "b", weight=3, capacity=4) + >>> G.add_edge("a", "c", weight=6, capacity=10) + >>> G.add_edge("b", "d", weight=1, capacity=9) + >>> G.add_edge("c", "d", weight=2, capacity=5) + >>> flowCost, flowDict = nx.network_simplex(G) + >>> flowCost + 24 + >>> flowDict + {'a': {'b': 4, 'c': 1}, 'd': {}, 'b': {'d': 4}, 'c': {'d': 1}} + + The mincost flow algorithm can also be used to solve shortest path + problems. To find the shortest path between two nodes u and v, + give all edges an infinite capacity, give node u a demand of -1 and + node v a demand a 1. Then run the network simplex. The value of a + min cost flow will be the distance between u and v and edges + carrying positive flow will indicate the path. + + >>> G = nx.DiGraph() + >>> G.add_weighted_edges_from( + ... [ + ... ("s", "u", 10), + ... ("s", "x", 5), + ... ("u", "v", 1), + ... ("u", "x", 2), + ... ("v", "y", 1), + ... ("x", "u", 3), + ... ("x", "v", 5), + ... ("x", "y", 2), + ... ("y", "s", 7), + ... ("y", "v", 6), + ... ] + ... ) + >>> G.add_node("s", demand=-1) + >>> G.add_node("v", demand=1) + >>> flowCost, flowDict = nx.network_simplex(G) + >>> flowCost == nx.shortest_path_length(G, "s", "v", weight="weight") + True + >>> sorted([(u, v) for u in flowDict for v in flowDict[u] if flowDict[u][v] > 0]) + [('s', 'x'), ('u', 'v'), ('x', 'u')] + >>> nx.shortest_path(G, "s", "v", weight="weight") + ['s', 'x', 'u', 'v'] + + It is possible to change the name of the attributes used for the + algorithm. + + >>> G = nx.DiGraph() + >>> G.add_node("p", spam=-4) + >>> G.add_node("q", spam=2) + >>> G.add_node("a", spam=-2) + >>> G.add_node("d", spam=-1) + >>> G.add_node("t", spam=2) + >>> G.add_node("w", spam=3) + >>> G.add_edge("p", "q", cost=7, vacancies=5) + >>> G.add_edge("p", "a", cost=1, vacancies=4) + >>> G.add_edge("q", "d", cost=2, vacancies=3) + >>> G.add_edge("t", "q", cost=1, vacancies=2) + >>> G.add_edge("a", "t", cost=2, vacancies=4) + >>> G.add_edge("d", "w", cost=3, vacancies=4) + >>> G.add_edge("t", "w", cost=4, vacancies=1) + >>> flowCost, flowDict = nx.network_simplex( + ... G, demand="spam", capacity="vacancies", weight="cost" + ... ) + >>> flowCost + 37 + >>> flowDict + {'p': {'q': 2, 'a': 2}, 'q': {'d': 1}, 'a': {'t': 4}, 'd': {'w': 2}, 't': {'q': 1, 'w': 1}, 'w': {}} + + References + ---------- + .. [1] Z. Kiraly, P. Kovacs. + Efficient implementation of minimum-cost flow algorithms. + Acta Universitatis Sapientiae, Informatica 4(1):67--118. 2012. + .. [2] R. Barr, F. Glover, D. Klingman. + Enhancement of spanning tree labeling procedures for network + optimization. + INFOR 17(1):16--34. 1979. + """ + ########################################################################### + # Problem essentials extraction and sanity check + ########################################################################### + + if len(G) == 0: + raise nx.NetworkXError("graph has no nodes") + + multigraph = G.is_multigraph() + + # extracting data essential to problem + DEAF = _DataEssentialsAndFunctions( + G, multigraph, demand=demand, capacity=capacity, weight=weight + ) + + ########################################################################### + # Quick Error Detection + ########################################################################### + + inf = float("inf") + for u, d in zip(DEAF.node_list, DEAF.node_demands): + if abs(d) == inf: + raise nx.NetworkXError(f"node {u!r} has infinite demand") + for e, w in zip(DEAF.edge_indices, DEAF.edge_weights): + if abs(w) == inf: + raise nx.NetworkXError(f"edge {e!r} has infinite weight") + if not multigraph: + edges = nx.selfloop_edges(G, data=True) + else: + edges = nx.selfloop_edges(G, data=True, keys=True) + for e in edges: + if abs(e[-1].get(weight, 0)) == inf: + raise nx.NetworkXError(f"edge {e[:-1]!r} has infinite weight") + + ########################################################################### + # Quick Infeasibility Detection + ########################################################################### + + if sum(DEAF.node_demands) != 0: + raise nx.NetworkXUnfeasible("total node demand is not zero") + for e, c in zip(DEAF.edge_indices, DEAF.edge_capacities): + if c < 0: + raise nx.NetworkXUnfeasible(f"edge {e!r} has negative capacity") + if not multigraph: + edges = nx.selfloop_edges(G, data=True) + else: + edges = nx.selfloop_edges(G, data=True, keys=True) + for e in edges: + if e[-1].get(capacity, inf) < 0: + raise nx.NetworkXUnfeasible(f"edge {e[:-1]!r} has negative capacity") + + ########################################################################### + # Initialization + ########################################################################### + + # Add a dummy node -1 and connect all existing nodes to it with infinite- + # capacity dummy edges. Node -1 will serve as the root of the + # spanning tree of the network simplex method. The new edges will used to + # trivially satisfy the node demands and create an initial strongly + # feasible spanning tree. + for i, d in enumerate(DEAF.node_demands): + # Must be greater-than here. Zero-demand nodes must have + # edges pointing towards the root to ensure strong feasibility. + if d > 0: + DEAF.edge_sources.append(-1) + DEAF.edge_targets.append(i) + else: + DEAF.edge_sources.append(i) + DEAF.edge_targets.append(-1) + faux_inf = ( + 3 + * max( + sum(c for c in DEAF.edge_capacities if c < inf), + sum(abs(w) for w in DEAF.edge_weights), + sum(abs(d) for d in DEAF.node_demands), + ) + or 1 + ) + + n = len(DEAF.node_list) # number of nodes + DEAF.edge_weights.extend(repeat(faux_inf, n)) + DEAF.edge_capacities.extend(repeat(faux_inf, n)) + + # Construct the initial spanning tree. + DEAF.initialize_spanning_tree(n, faux_inf) + + ########################################################################### + # Pivot loop + ########################################################################### + + for i, p, q in DEAF.find_entering_edges(): + Wn, We = DEAF.find_cycle(i, p, q) + j, s, t = DEAF.find_leaving_edge(Wn, We) + DEAF.augment_flow(Wn, We, DEAF.residual_capacity(j, s)) + # Do nothing more if the entering edge is the same as the leaving edge. + if i != j: + if DEAF.parent[t] != s: + # Ensure that s is the parent of t. + s, t = t, s + if We.index(i) > We.index(j): + # Ensure that q is in the subtree rooted at t. + p, q = q, p + DEAF.remove_edge(s, t) + DEAF.make_root(q) + DEAF.add_edge(i, p, q) + DEAF.update_potentials(i, p, q) + + ########################################################################### + # Infeasibility and unboundedness detection + ########################################################################### + + if any(DEAF.edge_flow[i] != 0 for i in range(-n, 0)): + raise nx.NetworkXUnfeasible("no flow satisfies all node demands") + + if any(DEAF.edge_flow[i] * 2 >= faux_inf for i in range(DEAF.edge_count)) or any( + e[-1].get(capacity, inf) == inf and e[-1].get(weight, 0) < 0 + for e in nx.selfloop_edges(G, data=True) + ): + raise nx.NetworkXUnbounded("negative cycle with infinite capacity found") + + ########################################################################### + # Flow cost calculation and flow dict construction + ########################################################################### + + del DEAF.edge_flow[DEAF.edge_count :] + flow_cost = sum(w * x for w, x in zip(DEAF.edge_weights, DEAF.edge_flow)) + flow_dict = {n: {} for n in DEAF.node_list} + + def add_entry(e): + """Add a flow dict entry.""" + d = flow_dict[e[0]] + for k in e[1:-2]: + try: + d = d[k] + except KeyError: + t = {} + d[k] = t + d = t + d[e[-2]] = e[-1] + + DEAF.edge_sources = ( + DEAF.node_list[s] for s in DEAF.edge_sources + ) # Use original nodes. + DEAF.edge_targets = ( + DEAF.node_list[t] for t in DEAF.edge_targets + ) # Use original nodes. + if not multigraph: + for e in zip(DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_flow): + add_entry(e) + edges = G.edges(data=True) + else: + for e in zip( + DEAF.edge_sources, DEAF.edge_targets, DEAF.edge_keys, DEAF.edge_flow + ): + add_entry(e) + edges = G.edges(data=True, keys=True) + for e in edges: + if e[0] != e[1]: + if e[-1].get(capacity, inf) == 0: + add_entry(e[:-1] + (0,)) + else: + w = e[-1].get(weight, 0) + if w >= 0: + add_entry(e[:-1] + (0,)) + else: + c = e[-1][capacity] + flow_cost += w * c + add_entry(e[:-1] + (c,)) + + return flow_cost, flow_dict diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/preflowpush.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/preflowpush.py new file mode 100644 index 0000000000000000000000000000000000000000..42cadc2e2db6ecfb5a347499c89d5ae77f6af3d8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/preflowpush.py @@ -0,0 +1,425 @@ +""" +Highest-label preflow-push algorithm for maximum flow problems. +""" + +from collections import deque +from itertools import islice + +import networkx as nx + +from ...utils import arbitrary_element +from .utils import ( + CurrentEdge, + GlobalRelabelThreshold, + Level, + build_residual_network, + detect_unboundedness, +) + +__all__ = ["preflow_push"] + + +def preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only): + """Implementation of the highest-label preflow-push algorithm.""" + if s not in G: + raise nx.NetworkXError(f"node {str(s)} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {str(t)} not in graph") + if s == t: + raise nx.NetworkXError("source and sink are the same node") + + if global_relabel_freq is None: + global_relabel_freq = 0 + if global_relabel_freq < 0: + raise nx.NetworkXError("global_relabel_freq must be nonnegative.") + + if residual is None: + R = build_residual_network(G, capacity) + else: + R = residual + + detect_unboundedness(R, s, t) + + R_nodes = R.nodes + R_pred = R.pred + R_succ = R.succ + + # Initialize/reset the residual network. + for u in R: + R_nodes[u]["excess"] = 0 + for e in R_succ[u].values(): + e["flow"] = 0 + + def reverse_bfs(src): + """Perform a reverse breadth-first search from src in the residual + network. + """ + heights = {src: 0} + q = deque([(src, 0)]) + while q: + u, height = q.popleft() + height += 1 + for v, attr in R_pred[u].items(): + if v not in heights and attr["flow"] < attr["capacity"]: + heights[v] = height + q.append((v, height)) + return heights + + # Initialize heights of the nodes. + heights = reverse_bfs(t) + + if s not in heights: + # t is not reachable from s in the residual network. The maximum flow + # must be zero. + R.graph["flow_value"] = 0 + return R + + n = len(R) + # max_height represents the height of the highest level below level n with + # at least one active node. + max_height = max(heights[u] for u in heights if u != s) + heights[s] = n + + grt = GlobalRelabelThreshold(n, R.size(), global_relabel_freq) + + # Initialize heights and 'current edge' data structures of the nodes. + for u in R: + R_nodes[u]["height"] = heights[u] if u in heights else n + 1 + R_nodes[u]["curr_edge"] = CurrentEdge(R_succ[u]) + + def push(u, v, flow): + """Push flow units of flow from u to v.""" + R_succ[u][v]["flow"] += flow + R_succ[v][u]["flow"] -= flow + R_nodes[u]["excess"] -= flow + R_nodes[v]["excess"] += flow + + # The maximum flow must be nonzero now. Initialize the preflow by + # saturating all edges emanating from s. + for u, attr in R_succ[s].items(): + flow = attr["capacity"] + if flow > 0: + push(s, u, flow) + + # Partition nodes into levels. + levels = [Level() for i in range(2 * n)] + for u in R: + if u != s and u != t: + level = levels[R_nodes[u]["height"]] + if R_nodes[u]["excess"] > 0: + level.active.add(u) + else: + level.inactive.add(u) + + def activate(v): + """Move a node from the inactive set to the active set of its level.""" + if v != s and v != t: + level = levels[R_nodes[v]["height"]] + if v in level.inactive: + level.inactive.remove(v) + level.active.add(v) + + def relabel(u): + """Relabel a node to create an admissible edge.""" + grt.add_work(len(R_succ[u])) + return ( + min( + R_nodes[v]["height"] + for v, attr in R_succ[u].items() + if attr["flow"] < attr["capacity"] + ) + + 1 + ) + + def discharge(u, is_phase1): + """Discharge a node until it becomes inactive or, during phase 1 (see + below), its height reaches at least n. The node is known to have the + largest height among active nodes. + """ + height = R_nodes[u]["height"] + curr_edge = R_nodes[u]["curr_edge"] + # next_height represents the next height to examine after discharging + # the current node. During phase 1, it is capped to below n. + next_height = height + levels[height].active.remove(u) + while True: + v, attr = curr_edge.get() + if height == R_nodes[v]["height"] + 1 and attr["flow"] < attr["capacity"]: + flow = min(R_nodes[u]["excess"], attr["capacity"] - attr["flow"]) + push(u, v, flow) + activate(v) + if R_nodes[u]["excess"] == 0: + # The node has become inactive. + levels[height].inactive.add(u) + break + try: + curr_edge.move_to_next() + except StopIteration: + # We have run off the end of the adjacency list, and there can + # be no more admissible edges. Relabel the node to create one. + height = relabel(u) + if is_phase1 and height >= n - 1: + # Although the node is still active, with a height at least + # n - 1, it is now known to be on the s side of the minimum + # s-t cut. Stop processing it until phase 2. + levels[height].active.add(u) + break + # The first relabel operation after global relabeling may not + # increase the height of the node since the 'current edge' data + # structure is not rewound. Use height instead of (height - 1) + # in case other active nodes at the same level are missed. + next_height = height + R_nodes[u]["height"] = height + return next_height + + def gap_heuristic(height): + """Apply the gap heuristic.""" + # Move all nodes at levels (height + 1) to max_height to level n + 1. + for level in islice(levels, height + 1, max_height + 1): + for u in level.active: + R_nodes[u]["height"] = n + 1 + for u in level.inactive: + R_nodes[u]["height"] = n + 1 + levels[n + 1].active.update(level.active) + level.active.clear() + levels[n + 1].inactive.update(level.inactive) + level.inactive.clear() + + def global_relabel(from_sink): + """Apply the global relabeling heuristic.""" + src = t if from_sink else s + heights = reverse_bfs(src) + if not from_sink: + # s must be reachable from t. Remove t explicitly. + del heights[t] + max_height = max(heights.values()) + if from_sink: + # Also mark nodes from which t is unreachable for relabeling. This + # serves the same purpose as the gap heuristic. + for u in R: + if u not in heights and R_nodes[u]["height"] < n: + heights[u] = n + 1 + else: + # Shift the computed heights because the height of s is n. + for u in heights: + heights[u] += n + max_height += n + del heights[src] + for u, new_height in heights.items(): + old_height = R_nodes[u]["height"] + if new_height != old_height: + if u in levels[old_height].active: + levels[old_height].active.remove(u) + levels[new_height].active.add(u) + else: + levels[old_height].inactive.remove(u) + levels[new_height].inactive.add(u) + R_nodes[u]["height"] = new_height + return max_height + + # Phase 1: Find the maximum preflow by pushing as much flow as possible to + # t. + + height = max_height + while height > 0: + # Discharge active nodes in the current level. + while True: + level = levels[height] + if not level.active: + # All active nodes in the current level have been discharged. + # Move to the next lower level. + height -= 1 + break + # Record the old height and level for the gap heuristic. + old_height = height + old_level = level + u = arbitrary_element(level.active) + height = discharge(u, True) + if grt.is_reached(): + # Global relabeling heuristic: Recompute the exact heights of + # all nodes. + height = global_relabel(True) + max_height = height + grt.clear_work() + elif not old_level.active and not old_level.inactive: + # Gap heuristic: If the level at old_height is empty (a 'gap'), + # a minimum cut has been identified. All nodes with heights + # above old_height can have their heights set to n + 1 and not + # be further processed before a maximum preflow is found. + gap_heuristic(old_height) + height = old_height - 1 + max_height = height + else: + # Update the height of the highest level with at least one + # active node. + max_height = max(max_height, height) + + # A maximum preflow has been found. The excess at t is the maximum flow + # value. + if value_only: + R.graph["flow_value"] = R_nodes[t]["excess"] + return R + + # Phase 2: Convert the maximum preflow into a maximum flow by returning the + # excess to s. + + # Relabel all nodes so that they have accurate heights. + height = global_relabel(False) + grt.clear_work() + + # Continue to discharge the active nodes. + while height > n: + # Discharge active nodes in the current level. + while True: + level = levels[height] + if not level.active: + # All active nodes in the current level have been discharged. + # Move to the next lower level. + height -= 1 + break + u = arbitrary_element(level.active) + height = discharge(u, False) + if grt.is_reached(): + # Global relabeling heuristic. + height = global_relabel(False) + grt.clear_work() + + R.graph["flow_value"] = R_nodes[t]["excess"] + return R + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def preflow_push( + G, s, t, capacity="capacity", residual=None, global_relabel_freq=1, value_only=False +): + r"""Find a maximum single-commodity flow using the highest-label + preflow-push algorithm. + + This function returns the residual network resulting after computing + the maximum flow. See below for details about the conventions + NetworkX uses for defining residual networks. + + This algorithm has a running time of $O(n^2 \sqrt{m})$ for $n$ nodes and + $m$ edges. + + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + residual : NetworkX graph + Residual network on which the algorithm is to be executed. If None, a + new residual network is created. Default value: None. + + global_relabel_freq : integer, float + Relative frequency of applying the global relabeling heuristic to speed + up the algorithm. If it is None, the heuristic is disabled. Default + value: 1. + + value_only : bool + If False, compute a maximum flow; otherwise, compute a maximum preflow + which is enough for computing the maximum flow value. Default value: + False. + + Returns + ------- + R : NetworkX DiGraph + Residual network after computing the maximum flow. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`edmonds_karp` + :meth:`shortest_augmenting_path` + + Notes + ----- + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. For each node :samp:`u` in :samp:`R`, + :samp:`R.nodes[u]['excess']` represents the difference between flow into + :samp:`u` and flow out of :samp:`u`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. Reachability to :samp:`t` using + only edges :samp:`(u, v)` such that + :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Examples + -------- + >>> from networkx.algorithms.flow import preflow_push + + The functions that implement flow algorithms and output a residual + network, such as this one, are not imported to the base NetworkX + namespace, so you have to explicitly import them from the flow package. + + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + >>> R = preflow_push(G, "x", "y") + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value == R.graph["flow_value"] + True + >>> # preflow_push also stores the maximum flow value + >>> # in the excess attribute of the sink node t + >>> flow_value == R.nodes["y"]["excess"] + True + >>> # For some problems, you might only want to compute a + >>> # maximum preflow. + >>> R = preflow_push(G, "x", "y", value_only=True) + >>> flow_value == R.graph["flow_value"] + True + >>> flow_value == R.nodes["y"]["excess"] + True + + """ + R = preflow_push_impl(G, s, t, capacity, residual, global_relabel_freq, value_only) + R.graph["algorithm"] = "preflow_push" + nx._clear_cache(R) + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/shortestaugmentingpath.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/shortestaugmentingpath.py new file mode 100644 index 0000000000000000000000000000000000000000..9f1193f1cbfbe188ebf05105a2c6f1802baca6f1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/shortestaugmentingpath.py @@ -0,0 +1,300 @@ +""" +Shortest augmenting path algorithm for maximum flow problems. +""" + +from collections import deque + +import networkx as nx + +from .edmondskarp import edmonds_karp_core +from .utils import CurrentEdge, build_residual_network + +__all__ = ["shortest_augmenting_path"] + + +def shortest_augmenting_path_impl(G, s, t, capacity, residual, two_phase, cutoff): + """Implementation of the shortest augmenting path algorithm.""" + if s not in G: + raise nx.NetworkXError(f"node {str(s)} not in graph") + if t not in G: + raise nx.NetworkXError(f"node {str(t)} not in graph") + if s == t: + raise nx.NetworkXError("source and sink are the same node") + + if residual is None: + R = build_residual_network(G, capacity) + else: + R = residual + + R_nodes = R.nodes + R_pred = R.pred + R_succ = R.succ + + # Initialize/reset the residual network. + for u in R: + for e in R_succ[u].values(): + e["flow"] = 0 + + # Initialize heights of the nodes. + heights = {t: 0} + q = deque([(t, 0)]) + while q: + u, height = q.popleft() + height += 1 + for v, attr in R_pred[u].items(): + if v not in heights and attr["flow"] < attr["capacity"]: + heights[v] = height + q.append((v, height)) + + if s not in heights: + # t is not reachable from s in the residual network. The maximum flow + # must be zero. + R.graph["flow_value"] = 0 + return R + + n = len(G) + m = R.size() / 2 + + # Initialize heights and 'current edge' data structures of the nodes. + for u in R: + R_nodes[u]["height"] = heights[u] if u in heights else n + R_nodes[u]["curr_edge"] = CurrentEdge(R_succ[u]) + + # Initialize counts of nodes in each level. + counts = [0] * (2 * n - 1) + for u in R: + counts[R_nodes[u]["height"]] += 1 + + inf = R.graph["inf"] + + def augment(path): + """Augment flow along a path from s to t.""" + # Determine the path residual capacity. + flow = inf + it = iter(path) + u = next(it) + for v in it: + attr = R_succ[u][v] + flow = min(flow, attr["capacity"] - attr["flow"]) + u = v + if flow * 2 > inf: + raise nx.NetworkXUnbounded("Infinite capacity path, flow unbounded above.") + # Augment flow along the path. + it = iter(path) + u = next(it) + for v in it: + R_succ[u][v]["flow"] += flow + R_succ[v][u]["flow"] -= flow + u = v + return flow + + def relabel(u): + """Relabel a node to create an admissible edge.""" + height = n - 1 + for v, attr in R_succ[u].items(): + if attr["flow"] < attr["capacity"]: + height = min(height, R_nodes[v]["height"]) + return height + 1 + + if cutoff is None: + cutoff = float("inf") + + # Phase 1: Look for shortest augmenting paths using depth-first search. + + flow_value = 0 + path = [s] + u = s + d = n if not two_phase else int(min(m**0.5, 2 * n ** (2.0 / 3))) + done = R_nodes[s]["height"] >= d + while not done: + height = R_nodes[u]["height"] + curr_edge = R_nodes[u]["curr_edge"] + # Depth-first search for the next node on the path to t. + while True: + v, attr = curr_edge.get() + if height == R_nodes[v]["height"] + 1 and attr["flow"] < attr["capacity"]: + # Advance to the next node following an admissible edge. + path.append(v) + u = v + break + try: + curr_edge.move_to_next() + except StopIteration: + counts[height] -= 1 + if counts[height] == 0: + # Gap heuristic: If relabeling causes a level to become + # empty, a minimum cut has been identified. The algorithm + # can now be terminated. + R.graph["flow_value"] = flow_value + return R + height = relabel(u) + if u == s and height >= d: + if not two_phase: + # t is disconnected from s in the residual network. No + # more augmenting paths exist. + R.graph["flow_value"] = flow_value + return R + else: + # t is at least d steps away from s. End of phase 1. + done = True + break + counts[height] += 1 + R_nodes[u]["height"] = height + if u != s: + # After relabeling, the last edge on the path is no longer + # admissible. Retreat one step to look for an alternative. + path.pop() + u = path[-1] + break + if u == t: + # t is reached. Augment flow along the path and reset it for a new + # depth-first search. + flow_value += augment(path) + if flow_value >= cutoff: + R.graph["flow_value"] = flow_value + return R + path = [s] + u = s + + # Phase 2: Look for shortest augmenting paths using breadth-first search. + flow_value += edmonds_karp_core(R, s, t, cutoff - flow_value) + + R.graph["flow_value"] = flow_value + return R + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def shortest_augmenting_path( + G, + s, + t, + capacity="capacity", + residual=None, + value_only=False, + two_phase=False, + cutoff=None, +): + r"""Find a maximum single-commodity flow using the shortest augmenting path + algorithm. + + This function returns the residual network resulting after computing + the maximum flow. See below for details about the conventions + NetworkX uses for defining residual networks. + + This algorithm has a running time of $O(n^2 m)$ for $n$ nodes and $m$ + edges. + + + Parameters + ---------- + G : NetworkX graph + Edges of the graph are expected to have an attribute called + 'capacity'. If this attribute is not present, the edge is + considered to have infinite capacity. + + s : node + Source node for the flow. + + t : node + Sink node for the flow. + + capacity : string + Edges of the graph G are expected to have an attribute capacity + that indicates how much flow the edge can support. If this + attribute is not present, the edge is considered to have + infinite capacity. Default value: 'capacity'. + + residual : NetworkX graph + Residual network on which the algorithm is to be executed. If None, a + new residual network is created. Default value: None. + + value_only : bool + If True compute only the value of the maximum flow. This parameter + will be ignored by this algorithm because it is not applicable. + + two_phase : bool + If True, a two-phase variant is used. The two-phase variant improves + the running time on unit-capacity networks from $O(nm)$ to + $O(\min(n^{2/3}, m^{1/2}) m)$. Default value: False. + + cutoff : integer, float + If specified, the algorithm will terminate when the flow value reaches + or exceeds the cutoff. In this case, it may be unable to immediately + determine a minimum cut. Default value: None. + + Returns + ------- + R : NetworkX DiGraph + Residual network after computing the maximum flow. + + Raises + ------ + NetworkXError + The algorithm does not support MultiGraph and MultiDiGraph. If + the input graph is an instance of one of these two classes, a + NetworkXError is raised. + + NetworkXUnbounded + If the graph has a path of infinite capacity, the value of a + feasible flow on the graph is unbounded above and the function + raises a NetworkXUnbounded. + + See also + -------- + :meth:`maximum_flow` + :meth:`minimum_cut` + :meth:`edmonds_karp` + :meth:`preflow_push` + + Notes + ----- + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not + specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such + that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + Examples + -------- + >>> from networkx.algorithms.flow import shortest_augmenting_path + + The functions that implement flow algorithms and output a residual + network, such as this one, are not imported to the base NetworkX + namespace, so you have to explicitly import them from the flow package. + + >>> G = nx.DiGraph() + >>> G.add_edge("x", "a", capacity=3.0) + >>> G.add_edge("x", "b", capacity=1.0) + >>> G.add_edge("a", "c", capacity=3.0) + >>> G.add_edge("b", "c", capacity=5.0) + >>> G.add_edge("b", "d", capacity=4.0) + >>> G.add_edge("d", "e", capacity=2.0) + >>> G.add_edge("c", "y", capacity=2.0) + >>> G.add_edge("e", "y", capacity=3.0) + >>> R = shortest_augmenting_path(G, "x", "y") + >>> flow_value = nx.maximum_flow_value(G, "x", "y") + >>> flow_value + 3.0 + >>> flow_value == R.graph["flow_value"] + True + + """ + R = shortest_augmenting_path_impl(G, s, t, capacity, residual, two_phase, cutoff) + R.graph["algorithm"] = "shortest_augmenting_path" + nx._clear_cache(R) + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4d63b5b64ca5f501cbf1098f9d280ecffd4a8f1b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/flow/utils.py @@ -0,0 +1,194 @@ +""" +Utility classes and functions for network flow algorithms. +""" + +from collections import deque + +import networkx as nx + +__all__ = [ + "CurrentEdge", + "Level", + "GlobalRelabelThreshold", + "build_residual_network", + "detect_unboundedness", + "build_flow_dict", +] + + +class CurrentEdge: + """Mechanism for iterating over out-edges incident to a node in a circular + manner. StopIteration exception is raised when wraparound occurs. + """ + + __slots__ = ("_edges", "_it", "_curr") + + def __init__(self, edges): + self._edges = edges + if self._edges: + self._rewind() + + def get(self): + return self._curr + + def move_to_next(self): + try: + self._curr = next(self._it) + except StopIteration: + self._rewind() + raise + + def _rewind(self): + self._it = iter(self._edges.items()) + self._curr = next(self._it) + + def __eq__(self, other): + return (getattr(self, "_curr", None), self._edges) == ( + (getattr(other, "_curr", None), other._edges) + ) + + +class Level: + """Active and inactive nodes in a level.""" + + __slots__ = ("active", "inactive") + + def __init__(self): + self.active = set() + self.inactive = set() + + +class GlobalRelabelThreshold: + """Measurement of work before the global relabeling heuristic should be + applied. + """ + + def __init__(self, n, m, freq): + self._threshold = (n + m) / freq if freq else float("inf") + self._work = 0 + + def add_work(self, work): + self._work += work + + def is_reached(self): + return self._work >= self._threshold + + def clear_work(self): + self._work = 0 + + +@nx._dispatchable(edge_attrs={"capacity": float("inf")}, returns_graph=True) +def build_residual_network(G, capacity): + """Build a residual network and initialize a zero flow. + + The residual network :samp:`R` from an input graph :samp:`G` has the + same nodes as :samp:`G`. :samp:`R` is a DiGraph that contains a pair + of edges :samp:`(u, v)` and :samp:`(v, u)` iff :samp:`(u, v)` is not a + self-loop, and at least one of :samp:`(u, v)` and :samp:`(v, u)` exists + in :samp:`G`. + + For each edge :samp:`(u, v)` in :samp:`R`, :samp:`R[u][v]['capacity']` + is equal to the capacity of :samp:`(u, v)` in :samp:`G` if it exists + in :samp:`G` or zero otherwise. If the capacity is infinite, + :samp:`R[u][v]['capacity']` will have a high arbitrary finite value + that does not affect the solution of the problem. This value is stored in + :samp:`R.graph['inf']`. For each edge :samp:`(u, v)` in :samp:`R`, + :samp:`R[u][v]['flow']` represents the flow function of :samp:`(u, v)` and + satisfies :samp:`R[u][v]['flow'] == -R[v][u]['flow']`. + + The flow value, defined as the total flow into :samp:`t`, the sink, is + stored in :samp:`R.graph['flow_value']`. If :samp:`cutoff` is not + specified, reachability to :samp:`t` using only edges :samp:`(u, v)` such + that :samp:`R[u][v]['flow'] < R[u][v]['capacity']` induces a minimum + :samp:`s`-:samp:`t` cut. + + """ + if G.is_multigraph(): + raise nx.NetworkXError("MultiGraph and MultiDiGraph not supported (yet).") + + R = nx.DiGraph() + R.__networkx_cache__ = None # Disable caching + R.add_nodes_from(G) + + inf = float("inf") + # Extract edges with positive capacities. Self loops excluded. + edge_list = [ + (u, v, attr) + for u, v, attr in G.edges(data=True) + if u != v and attr.get(capacity, inf) > 0 + ] + # Simulate infinity with three times the sum of the finite edge capacities + # or any positive value if the sum is zero. This allows the + # infinite-capacity edges to be distinguished for unboundedness detection + # and directly participate in residual capacity calculation. If the maximum + # flow is finite, these edges cannot appear in the minimum cut and thus + # guarantee correctness. Since the residual capacity of an + # infinite-capacity edge is always at least 2/3 of inf, while that of an + # finite-capacity edge is at most 1/3 of inf, if an operation moves more + # than 1/3 of inf units of flow to t, there must be an infinite-capacity + # s-t path in G. + inf = ( + 3 + * sum( + attr[capacity] + for u, v, attr in edge_list + if capacity in attr and attr[capacity] != inf + ) + or 1 + ) + if G.is_directed(): + for u, v, attr in edge_list: + r = min(attr.get(capacity, inf), inf) + if not R.has_edge(u, v): + # Both (u, v) and (v, u) must be present in the residual + # network. + R.add_edge(u, v, capacity=r) + R.add_edge(v, u, capacity=0) + else: + # The edge (u, v) was added when (v, u) was visited. + R[u][v]["capacity"] = r + else: + for u, v, attr in edge_list: + # Add a pair of edges with equal residual capacities. + r = min(attr.get(capacity, inf), inf) + R.add_edge(u, v, capacity=r) + R.add_edge(v, u, capacity=r) + + # Record the value simulating infinity. + R.graph["inf"] = inf + + return R + + +@nx._dispatchable( + graphs="R", + preserve_edge_attrs={"R": {"capacity": float("inf")}}, + preserve_graph_attrs=True, +) +def detect_unboundedness(R, s, t): + """Detect an infinite-capacity s-t path in R.""" + q = deque([s]) + seen = {s} + inf = R.graph["inf"] + while q: + u = q.popleft() + for v, attr in R[u].items(): + if attr["capacity"] == inf and v not in seen: + if v == t: + raise nx.NetworkXUnbounded( + "Infinite capacity path, flow unbounded above." + ) + seen.add(v) + q.append(v) + + +@nx._dispatchable(graphs={"G": 0, "R": 1}, preserve_edge_attrs={"R": {"flow": None}}) +def build_flow_dict(G, R): + """Build a flow dictionary from a residual network.""" + flow_dict = {} + for u in G: + flow_dict[u] = {v: 0 for v in G[u]} + flow_dict[u].update( + (v, attr["flow"]) for v, attr in R[u].items() if attr["flow"] > 0 + ) + return flow_dict diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graph_hashing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graph_hashing.py new file mode 100644 index 0000000000000000000000000000000000000000..96a76ffa41ae1f6b3aaa749ab7d9eef550bf36d8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graph_hashing.py @@ -0,0 +1,435 @@ +""" +Functions for hashing graphs to strings. +Isomorphic graphs should be assigned identical hashes. +For now, only Weisfeiler-Lehman hashing is implemented. +""" + +import warnings +from collections import Counter, defaultdict +from hashlib import blake2b + +import networkx as nx + +__all__ = ["weisfeiler_lehman_graph_hash", "weisfeiler_lehman_subgraph_hashes"] + + +def _hash_label(label, digest_size): + return blake2b(label.encode("ascii"), digest_size=digest_size).hexdigest() + + +def _init_node_labels(G, edge_attr, node_attr): + if node_attr: + return {u: str(dd[node_attr]) for u, dd in G.nodes(data=True)} + elif edge_attr: + return {u: "" for u in G} + else: + warnings.warn( + "The hashes produced for graphs without node or edge attributes " + "changed in v3.5 due to a bugfix (see documentation).", + UserWarning, + stacklevel=2, + ) + if nx.is_directed(G): + return {u: str(G.in_degree(u)) + "_" + str(G.out_degree(u)) for u in G} + else: + return {u: str(deg) for u, deg in G.degree()} + + +def _neighborhood_aggregate_undirected(G, node, node_labels, edge_attr=None): + """ + Compute new labels for given node in an undirected graph by aggregating + the labels of each node's neighbors. + """ + label_list = [] + for nbr in G.neighbors(node): + prefix = "" if edge_attr is None else str(G[node][nbr][edge_attr]) + label_list.append(prefix + node_labels[nbr]) + return node_labels[node] + "".join(sorted(label_list)) + + +def _neighborhood_aggregate_directed(G, node, node_labels, edge_attr=None): + """ + Compute new labels for given node in a directed graph by aggregating + the labels of each node's neighbors. + """ + successor_labels = [] + for nbr in G.successors(node): + prefix = "s_" + "" if edge_attr is None else str(G[node][nbr][edge_attr]) + successor_labels.append(prefix + node_labels[nbr]) + + predecessor_labels = [] + for nbr in G.predecessors(node): + prefix = "p_" + "" if edge_attr is None else str(G[nbr][node][edge_attr]) + predecessor_labels.append(prefix + node_labels[nbr]) + return ( + node_labels[node] + + "".join(sorted(successor_labels)) + + "".join(sorted(predecessor_labels)) + ) + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr") +def weisfeiler_lehman_graph_hash( + G, edge_attr=None, node_attr=None, iterations=3, digest_size=16 +): + """Return Weisfeiler Lehman (WL) graph hash. + + .. Warning:: Hash values for directed graphs and graphs without edge or + node attributes have changed in v3.5. In previous versions, + directed graphs did not distinguish in- and outgoing edges. Also, + graphs without attributes set initial states such that effectively + one extra iteration of WL occurred than indicated by `iterations`. + For undirected graphs without node or edge labels, the old + hashes can be obtained by increasing the iteration count by one. + For more details, see `issue #7806 + `_. + + The function iteratively aggregates and hashes neighborhoods of each node. + After each node's neighbors are hashed to obtain updated node labels, + a hashed histogram of resulting labels is returned as the final hash. + + Hashes are identical for isomorphic graphs and strong guarantees that + non-isomorphic graphs will get different hashes. See [1]_ for details. + + If no node or edge attributes are provided, the degree of each node + is used as its initial label. + Otherwise, node and/or edge labels are used to compute the hash. + + Parameters + ---------- + G : graph + The graph to be hashed. + Can have node and/or edge attributes. Can also have no attributes. + edge_attr : string, optional (default=None) + The key in edge attribute dictionary to be used for hashing. + If None, edge labels are ignored. + node_attr: string, optional (default=None) + The key in node attribute dictionary to be used for hashing. + If None, and no edge_attr given, use the degrees of the nodes as labels. + iterations: int, optional (default=3) + Number of neighbor aggregations to perform. + Should be larger for larger graphs. + digest_size: int, optional (default=16) + Size (in bytes) of blake2b hash digest to use for hashing node labels. + + Returns + ------- + h : string + Hexadecimal string corresponding to hash of `G` (length ``2 * digest_size``). + + Raises + ------ + ValueError + If `iterations` is not a positve number. + + Examples + -------- + Two graphs with edge attributes that are isomorphic, except for + differences in the edge labels. + + >>> G1 = nx.Graph() + >>> G1.add_edges_from( + ... [ + ... (1, 2, {"label": "A"}), + ... (2, 3, {"label": "A"}), + ... (3, 1, {"label": "A"}), + ... (1, 4, {"label": "B"}), + ... ] + ... ) + >>> G2 = nx.Graph() + >>> G2.add_edges_from( + ... [ + ... (5, 6, {"label": "B"}), + ... (6, 7, {"label": "A"}), + ... (7, 5, {"label": "A"}), + ... (7, 8, {"label": "A"}), + ... ] + ... ) + + Omitting the `edge_attr` option, results in identical hashes. + + >>> nx.weisfeiler_lehman_graph_hash(G1) + 'c045439172215f49e0bef8c3d26c6b61' + >>> nx.weisfeiler_lehman_graph_hash(G2) + 'c045439172215f49e0bef8c3d26c6b61' + + With edge labels, the graphs are no longer assigned + the same hash digest. + + >>> nx.weisfeiler_lehman_graph_hash(G1, edge_attr="label") + 'c653d85538bcf041d88c011f4f905f10' + >>> nx.weisfeiler_lehman_graph_hash(G2, edge_attr="label") + '3dcd84af1ca855d0eff3c978d88e7ec7' + + Notes + ----- + To return the WL hashes of each subgraph of a graph, use + `weisfeiler_lehman_subgraph_hashes` + + Similarity between hashes does not imply similarity between graphs. + + References + ---------- + .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen, + Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman + Graph Kernels. Journal of Machine Learning Research. 2011. + http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf + + See also + -------- + weisfeiler_lehman_subgraph_hashes + """ + + if G.is_directed(): + _neighborhood_aggregate = _neighborhood_aggregate_directed + warnings.warn( + "The hashes produced for directed graphs changed in version v3.5" + " due to a bugfix to track in and out edges separately (see documentation).", + UserWarning, + stacklevel=2, + ) + else: + _neighborhood_aggregate = _neighborhood_aggregate_undirected + + def weisfeiler_lehman_step(G, labels, edge_attr=None): + """ + Apply neighborhood aggregation to each node + in the graph. + Computes a dictionary with labels for each node. + """ + new_labels = {} + for node in G.nodes(): + label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr) + new_labels[node] = _hash_label(label, digest_size) + return new_labels + + if iterations <= 0: + raise ValueError("The WL algorithm requires that `iterations` be positive") + + # set initial node labels + node_labels = _init_node_labels(G, edge_attr, node_attr) + + # If the graph has no attributes, initial labels are the nodes' degrees. + # This is equivalent to doing the first iterations of WL. + if not edge_attr and not node_attr: + iterations -= 1 + + subgraph_hash_counts = [] + for _ in range(iterations): + node_labels = weisfeiler_lehman_step(G, node_labels, edge_attr=edge_attr) + counter = Counter(node_labels.values()) + # sort the counter, extend total counts + subgraph_hash_counts.extend(sorted(counter.items(), key=lambda x: x[0])) + + # hash the final counter + return _hash_label(str(tuple(subgraph_hash_counts)), digest_size) + + +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr") +def weisfeiler_lehman_subgraph_hashes( + G, + edge_attr=None, + node_attr=None, + iterations=3, + digest_size=16, + include_initial_labels=False, +): + """ + Return a dictionary of subgraph hashes by node. + + .. Warning:: Hash values for directed graphs have changed in version + v3.5. In previous versions, directed graphs did not distinguish in- + and outgoing edges. + Graphs without attributes previously performed an extra iteration of + WL at initialisation, which was not visible in the output of this + function. This hash value is now included in the returned dictionary, + shifting the other calculated hashes one position to the right. To + obtain the same last subgraph hash, increase the number of iterations + by one. + For more details, see `issue #7806 + `_. + + Dictionary keys are nodes in `G`, and values are a list of hashes. + Each hash corresponds to a subgraph rooted at a given node u in `G`. + Lists of subgraph hashes are sorted in increasing order of depth from + their root node, with the hash at index i corresponding to a subgraph + of nodes at most i-hops (i edges) distance from u. Thus, each list will contain + `iterations` elements - a hash for a subgraph at each depth. If + `include_initial_labels` is set to `True`, each list will additionally + have contain a hash of the initial node label (or equivalently a + subgraph of depth 0) prepended, totalling ``iterations + 1`` elements. + + The function iteratively aggregates and hashes neighborhoods of each node. + This is achieved for each step by replacing for each node its label from + the previous iteration with its hashed 1-hop neighborhood aggregate. + The new node label is then appended to a list of node labels for each + node. + + To aggregate neighborhoods for a node $u$ at each step, all labels of + nodes adjacent to $u$ are concatenated. If the `edge_attr` parameter is set, + labels for each neighboring node are prefixed with the value of this attribute + along the connecting edge from this neighbor to node $u$. The resulting string + is then hashed to compress this information into a fixed digest size. + + Thus, at the i-th iteration, nodes within i hops influence any given + hashed node label. We can therefore say that at depth $i$ for node $u$ + we have a hash for a subgraph induced by the i-hop neighborhood of $u$. + + The output can be used to create general Weisfeiler-Lehman graph kernels, + or generate features for graphs or nodes - for example to generate 'words' in + a graph as seen in the 'graph2vec' algorithm. + See [1]_ & [2]_ respectively for details. + + Hashes are identical for isomorphic subgraphs and there exist strong + guarantees that non-isomorphic graphs will get different hashes. + See [1]_ for details. + + If no node or edge attributes are provided, the degree of each node + is used as its initial label. + Otherwise, node and/or edge labels are used to compute the hash. + + Parameters + ---------- + G : graph + The graph to be hashed. + Can have node and/or edge attributes. Can also have no attributes. + edge_attr : string, optional (default=None) + The key in edge attribute dictionary to be used for hashing. + If None, edge labels are ignored. + node_attr : string, optional (default=None) + The key in node attribute dictionary to be used for hashing. + If None, and no edge_attr given, use the degrees of the nodes as labels. + If None, and edge_attr is given, each node starts with an identical label. + iterations : int, optional (default=3) + Number of neighbor aggregations to perform. + Should be larger for larger graphs. + digest_size : int, optional (default=16) + Size (in bytes) of blake2b hash digest to use for hashing node labels. + The default size is 16 bytes. + include_initial_labels : bool, optional (default=False) + If True, include the hashed initial node label as the first subgraph + hash for each node. + + Returns + ------- + node_subgraph_hashes : dict + A dictionary with each key given by a node in G, and each value given + by the subgraph hashes in order of depth from the key node. + Hashes are hexadecimal strings (hence ``2 * digest_size`` long). + + + Raises + ------ + ValueError + If `iterations` is not a positve number. + + Examples + -------- + Finding similar nodes in different graphs: + + >>> G1 = nx.Graph() + >>> G1.add_edges_from([(1, 2), (2, 3), (2, 4), (3, 5), (4, 6), (5, 7), (6, 7)]) + >>> G2 = nx.Graph() + >>> G2.add_edges_from([(1, 3), (2, 3), (1, 6), (1, 5), (4, 6)]) + >>> g1_hashes = nx.weisfeiler_lehman_subgraph_hashes( + ... G1, iterations=4, digest_size=8 + ... ) + >>> g2_hashes = nx.weisfeiler_lehman_subgraph_hashes( + ... G2, iterations=4, digest_size=8 + ... ) + + Even though G1 and G2 are not isomorphic (they have different numbers of edges), + the hash sequence of depth 3 for node 1 in G1 and node 5 in G2 are similar: + + >>> g1_hashes[1] + ['f6fc42039fba3776', 'a93b64973cfc8897', 'db1b43ae35a1878f', '57872a7d2059c1c0'] + >>> g2_hashes[5] + ['f6fc42039fba3776', 'a93b64973cfc8897', 'db1b43ae35a1878f', '1716d2a4012fa4bc'] + + The first 3 WL subgraph hashes match. From this we can conclude that it's very + likely the neighborhood of 3 hops around these nodes are isomorphic. + + However the 4-hop neighborhoods of ``G1`` and ``G2`` are not isomorphic since the + 4th hashes in the lists above are not equal. + + These nodes may be candidates to be classified together since their local topology + is similar. + + Notes + ----- + To hash the full graph when subgraph hashes are not needed, use + `weisfeiler_lehman_graph_hash` for efficiency. + + Similarity between hashes does not imply similarity between graphs. + + References + ---------- + .. [1] Shervashidze, Nino, Pascal Schweitzer, Erik Jan Van Leeuwen, + Kurt Mehlhorn, and Karsten M. Borgwardt. Weisfeiler Lehman + Graph Kernels. Journal of Machine Learning Research. 2011. + http://www.jmlr.org/papers/volume12/shervashidze11a/shervashidze11a.pdf + .. [2] Annamalai Narayanan, Mahinthan Chandramohan, Rajasekar Venkatesan, + Lihui Chen, Yang Liu and Shantanu Jaiswa. graph2vec: Learning + Distributed Representations of Graphs. arXiv. 2017 + https://arxiv.org/pdf/1707.05005.pdf + + See also + -------- + weisfeiler_lehman_graph_hash + """ + + if G.is_directed(): + _neighborhood_aggregate = _neighborhood_aggregate_directed + warnings.warn( + "The hashes produced for directed graphs changed in v3.5" + " due to a bugfix (see documentation).", + UserWarning, + stacklevel=2, + ) + else: + _neighborhood_aggregate = _neighborhood_aggregate_undirected + + def weisfeiler_lehman_step(G, labels, node_subgraph_hashes, edge_attr=None): + """ + Apply neighborhood aggregation to each node + in the graph. + Computes a dictionary with labels for each node. + Appends the new hashed label to the dictionary of subgraph hashes + originating from and indexed by each node in G + """ + new_labels = {} + for node in G.nodes(): + label = _neighborhood_aggregate(G, node, labels, edge_attr=edge_attr) + hashed_label = _hash_label(label, digest_size) + new_labels[node] = hashed_label + node_subgraph_hashes[node].append(hashed_label) + return new_labels + + if iterations <= 0: + raise ValueError("The WL algorithm requires that `iterations` be positive") + + node_labels = _init_node_labels(G, edge_attr, node_attr) + + if include_initial_labels: + node_subgraph_hashes = { + k: [_hash_label(v, digest_size)] for k, v in node_labels.items() + } + else: + node_subgraph_hashes = defaultdict(list) + + # If the graph has no attributes, initial labels are the nodes' degrees. + # This is equivalent to doing the first iterations of WL. + if not edge_attr and not node_attr: + iterations -= 1 + for node in G.nodes(): + hashed_label = _hash_label(node_labels[node], digest_size) + node_subgraph_hashes[node].append(hashed_label) + + for _ in range(iterations): + node_labels = weisfeiler_lehman_step( + G, node_labels, node_subgraph_hashes, edge_attr + ) + + return dict(node_subgraph_hashes) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graphical.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graphical.py new file mode 100644 index 0000000000000000000000000000000000000000..d5d82dedda6f9810e3f51bc4c82a9a2b252fa998 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/graphical.py @@ -0,0 +1,483 @@ +"""Test sequences for graphiness.""" + +import heapq + +import networkx as nx + +__all__ = [ + "is_graphical", + "is_multigraphical", + "is_pseudographical", + "is_digraphical", + "is_valid_degree_sequence_erdos_gallai", + "is_valid_degree_sequence_havel_hakimi", +] + + +@nx._dispatchable(graphs=None) +def is_graphical(sequence, method="eg"): + """Returns True if sequence is a valid degree sequence. + + A degree sequence is valid if some graph can realize it. + + Parameters + ---------- + sequence : list or iterable container + A sequence of integer node degrees + + method : "eg" | "hh" (default: 'eg') + The method used to validate the degree sequence. + "eg" corresponds to the Erdős-Gallai algorithm + [EG1960]_, [choudum1986]_, and + "hh" to the Havel-Hakimi algorithm + [havel1955]_, [hakimi1962]_, [CL1996]_. + + Returns + ------- + valid : bool + True if the sequence is a valid degree sequence and False if not. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> sequence = (d for n, d in G.degree()) + >>> nx.is_graphical(sequence) + True + + To test a non-graphical sequence: + >>> sequence_list = [d for n, d in G.degree()] + >>> sequence_list[-1] += 1 + >>> nx.is_graphical(sequence_list) + False + + References + ---------- + .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960. + .. [choudum1986] S.A. Choudum. "A simple proof of the Erdős-Gallai theorem on + graph sequences." Bulletin of the Australian Mathematical Society, 33, + pp 67-70, 1986. https://doi.org/10.1017/S0004972700002872 + .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs" + Casopis Pest. Mat. 80, 477-480, 1955. + .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as + Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962. + .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs", + Chapman and Hall/CRC, 1996. + """ + if method == "eg": + valid = is_valid_degree_sequence_erdos_gallai(list(sequence)) + elif method == "hh": + valid = is_valid_degree_sequence_havel_hakimi(list(sequence)) + else: + msg = "`method` must be 'eg' or 'hh'" + raise nx.NetworkXException(msg) + return valid + + +def _basic_graphical_tests(deg_sequence): + # Sort and perform some simple tests on the sequence + deg_sequence = nx.utils.make_list_of_ints(deg_sequence) + p = len(deg_sequence) + num_degs = [0] * p + dmax, dmin, dsum, n = 0, p, 0, 0 + for d in deg_sequence: + # Reject if degree is negative or larger than the sequence length + if d < 0 or d >= p: + raise nx.NetworkXUnfeasible + # Process only the non-zero integers + elif d > 0: + dmax, dmin, dsum, n = max(dmax, d), min(dmin, d), dsum + d, n + 1 + num_degs[d] += 1 + # Reject sequence if it has odd sum or is oversaturated + if dsum % 2 or dsum > n * (n - 1): + raise nx.NetworkXUnfeasible + return dmax, dmin, dsum, n, num_degs + + +@nx._dispatchable(graphs=None) +def is_valid_degree_sequence_havel_hakimi(deg_sequence): + r"""Returns True if deg_sequence can be realized by a simple graph. + + The validation proceeds using the Havel-Hakimi theorem + [havel1955]_, [hakimi1962]_, [CL1996]_. + Worst-case run time is $O(s)$ where $s$ is the sum of the sequence. + + Parameters + ---------- + deg_sequence : list + A list of integers where each element specifies the degree of a node + in a graph. + + Returns + ------- + valid : bool + True if deg_sequence is graphical and False if not. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)]) + >>> sequence = (d for _, d in G.degree()) + >>> nx.is_valid_degree_sequence_havel_hakimi(sequence) + True + + To test a non-valid sequence: + >>> sequence_list = [d for _, d in G.degree()] + >>> sequence_list[-1] += 1 + >>> nx.is_valid_degree_sequence_havel_hakimi(sequence_list) + False + + Notes + ----- + The ZZ condition says that for the sequence d if + + .. math:: + |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)} + + then d is graphical. This was shown in Theorem 6 in [1]_. + + References + ---------- + .. [1] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory + of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992). + .. [havel1955] Havel, V. "A Remark on the Existence of Finite Graphs" + Casopis Pest. Mat. 80, 477-480, 1955. + .. [hakimi1962] Hakimi, S. "On the Realizability of a Set of Integers as + Degrees of the Vertices of a Graph." SIAM J. Appl. Math. 10, 496-506, 1962. + .. [CL1996] G. Chartrand and L. Lesniak, "Graphs and Digraphs", + Chapman and Hall/CRC, 1996. + """ + try: + dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence) + except nx.NetworkXUnfeasible: + return False + # Accept if sequence has no non-zero degrees or passes the ZZ condition + if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1): + return True + + modstubs = [0] * (dmax + 1) + # Successively reduce degree sequence by removing the maximum degree + while n > 0: + # Retrieve the maximum degree in the sequence + while num_degs[dmax] == 0: + dmax -= 1 + # If there are not enough stubs to connect to, then the sequence is + # not graphical + if dmax > n - 1: + return False + + # Remove largest stub in list + num_degs[dmax], n = num_degs[dmax] - 1, n - 1 + # Reduce the next dmax largest stubs + mslen = 0 + k = dmax + for i in range(dmax): + while num_degs[k] == 0: + k -= 1 + num_degs[k], n = num_degs[k] - 1, n - 1 + if k > 1: + modstubs[mslen] = k - 1 + mslen += 1 + # Add back to the list any non-zero stubs that were removed + for i in range(mslen): + stub = modstubs[i] + num_degs[stub], n = num_degs[stub] + 1, n + 1 + return True + + +@nx._dispatchable(graphs=None) +def is_valid_degree_sequence_erdos_gallai(deg_sequence): + r"""Returns True if deg_sequence can be realized by a simple graph. + + The validation is done using the Erdős-Gallai theorem [EG1960]_. + + Parameters + ---------- + deg_sequence : list + A list of integers + + Returns + ------- + valid : bool + True if deg_sequence is graphical and False if not. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)]) + >>> sequence = (d for _, d in G.degree()) + >>> nx.is_valid_degree_sequence_erdos_gallai(sequence) + True + + To test a non-valid sequence: + >>> sequence_list = [d for _, d in G.degree()] + >>> sequence_list[-1] += 1 + >>> nx.is_valid_degree_sequence_erdos_gallai(sequence_list) + False + + Notes + ----- + + This implementation uses an equivalent form of the Erdős-Gallai criterion. + Worst-case run time is $O(n)$ where $n$ is the length of the sequence. + + Specifically, a sequence d is graphical if and only if the + sum of the sequence is even and for all strong indices k in the sequence, + + .. math:: + + \sum_{i=1}^{k} d_i \leq k(k-1) + \sum_{j=k+1}^{n} \min(d_i,k) + = k(n-1) - ( k \sum_{j=0}^{k-1} n_j - \sum_{j=0}^{k-1} j n_j ) + + A strong index k is any index where d_k >= k and the value n_j is the + number of occurrences of j in d. The maximal strong index is called the + Durfee index. + + This particular rearrangement comes from the proof of Theorem 3 in [2]_. + + The ZZ condition says that for the sequence d if + + .. math:: + |d| >= \frac{(\max(d) + \min(d) + 1)^2}{4*\min(d)} + + then d is graphical. This was shown in Theorem 6 in [2]_. + + References + ---------- + .. [1] A. Tripathi and S. Vijay. "A note on a theorem of Erdős & Gallai", + Discrete Mathematics, 265, pp. 417-420 (2003). + .. [2] I.E. Zverovich and V.E. Zverovich. "Contributions to the theory + of graphic sequences", Discrete Mathematics, 105, pp. 292-303 (1992). + .. [EG1960] Erdős and Gallai, Mat. Lapok 11 264, 1960. + """ + try: + dmax, dmin, dsum, n, num_degs = _basic_graphical_tests(deg_sequence) + except nx.NetworkXUnfeasible: + return False + # Accept if sequence has no non-zero degrees or passes the ZZ condition + if n == 0 or 4 * dmin * n >= (dmax + dmin + 1) * (dmax + dmin + 1): + return True + + # Perform the EG checks using the reformulation of Zverovich and Zverovich + k, sum_deg, sum_nj, sum_jnj = 0, 0, 0, 0 + for dk in range(dmax, dmin - 1, -1): + if dk < k + 1: # Check if already past Durfee index + return True + if num_degs[dk] > 0: + run_size = num_degs[dk] # Process a run of identical-valued degrees + if dk < k + run_size: # Check if end of run is past Durfee index + run_size = dk - k # Adjust back to Durfee index + sum_deg += run_size * dk + for v in range(run_size): + sum_nj += num_degs[k + v] + sum_jnj += (k + v) * num_degs[k + v] + k += run_size + if sum_deg > k * (n - 1) - k * sum_nj + sum_jnj: + return False + return True + + +@nx._dispatchable(graphs=None) +def is_multigraphical(sequence): + """Returns True if some multigraph can realize the sequence. + + Parameters + ---------- + sequence : list + A list of integers + + Returns + ------- + valid : bool + True if deg_sequence is a multigraphic degree sequence and False if not. + + Examples + -------- + >>> G = nx.MultiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)]) + >>> sequence = (d for _, d in G.degree()) + >>> nx.is_multigraphical(sequence) + True + + To test a non-multigraphical sequence: + >>> sequence_list = [d for _, d in G.degree()] + >>> sequence_list[-1] += 1 + >>> nx.is_multigraphical(sequence_list) + False + + Notes + ----- + The worst-case run time is $O(n)$ where $n$ is the length of the sequence. + + References + ---------- + .. [1] S. L. Hakimi. "On the realizability of a set of integers as + degrees of the vertices of a linear graph", J. SIAM, 10, pp. 496-506 + (1962). + """ + try: + deg_sequence = nx.utils.make_list_of_ints(sequence) + except nx.NetworkXError: + return False + dsum, dmax = 0, 0 + for d in deg_sequence: + if d < 0: + return False + dsum, dmax = dsum + d, max(dmax, d) + if dsum % 2 or dsum < 2 * dmax: + return False + return True + + +@nx._dispatchable(graphs=None) +def is_pseudographical(sequence): + """Returns True if some pseudograph can realize the sequence. + + Every nonnegative integer sequence with an even sum is pseudographical + (see [1]_). + + Parameters + ---------- + sequence : list or iterable container + A sequence of integer node degrees + + Returns + ------- + valid : bool + True if the sequence is a pseudographic degree sequence and False if not. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)]) + >>> sequence = (d for _, d in G.degree()) + >>> nx.is_pseudographical(sequence) + True + + To test a non-pseudographical sequence: + >>> sequence_list = [d for _, d in G.degree()] + >>> sequence_list[-1] += 1 + >>> nx.is_pseudographical(sequence_list) + False + + Notes + ----- + The worst-case run time is $O(n)$ where n is the length of the sequence. + + References + ---------- + .. [1] F. Boesch and F. Harary. "Line removal algorithms for graphs + and their degree lists", IEEE Trans. Circuits and Systems, CAS-23(12), + pp. 778-782 (1976). + """ + try: + deg_sequence = nx.utils.make_list_of_ints(sequence) + except nx.NetworkXError: + return False + return sum(deg_sequence) % 2 == 0 and min(deg_sequence) >= 0 + + +@nx._dispatchable(graphs=None) +def is_digraphical(in_sequence, out_sequence): + r"""Returns True if some directed graph can realize the in- and out-degree + sequences. + + Parameters + ---------- + in_sequence : list or iterable container + A sequence of integer node in-degrees + + out_sequence : list or iterable container + A sequence of integer node out-degrees + + Returns + ------- + valid : bool + True if in and out-sequences are digraphic False if not. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 4), (4, 2), (5, 1), (5, 4)]) + >>> in_seq = (d for n, d in G.in_degree()) + >>> out_seq = (d for n, d in G.out_degree()) + >>> nx.is_digraphical(in_seq, out_seq) + True + + To test a non-digraphical scenario: + >>> in_seq_list = [d for n, d in G.in_degree()] + >>> in_seq_list[-1] += 1 + >>> nx.is_digraphical(in_seq_list, out_seq) + False + + Notes + ----- + This algorithm is from Kleitman and Wang [1]_. + The worst case runtime is $O(s \times \log n)$ where $s$ and $n$ are the + sum and length of the sequences respectively. + + References + ---------- + .. [1] D.J. Kleitman and D.L. Wang + Algorithms for Constructing Graphs and Digraphs with Given Valences + and Factors, Discrete Mathematics, 6(1), pp. 79-88 (1973) + """ + try: + in_deg_sequence = nx.utils.make_list_of_ints(in_sequence) + out_deg_sequence = nx.utils.make_list_of_ints(out_sequence) + except nx.NetworkXError: + return False + # Process the sequences and form two heaps to store degree pairs with + # either zero or non-zero out degrees + sumin, sumout, nin, nout = 0, 0, len(in_deg_sequence), len(out_deg_sequence) + maxn = max(nin, nout) + maxin = 0 + if maxn == 0: + return True + stubheap, zeroheap = [], [] + for n in range(maxn): + in_deg, out_deg = 0, 0 + if n < nout: + out_deg = out_deg_sequence[n] + if n < nin: + in_deg = in_deg_sequence[n] + if in_deg < 0 or out_deg < 0: + return False + sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg) + if in_deg > 0: + stubheap.append((-1 * out_deg, -1 * in_deg)) + elif out_deg > 0: + zeroheap.append(-1 * out_deg) + if sumin != sumout: + return False + heapq.heapify(stubheap) + heapq.heapify(zeroheap) + + modstubs = [(0, 0)] * (maxin + 1) + # Successively reduce degree sequence by removing the maximum out degree + while stubheap: + # Take the first value in the sequence with non-zero in degree + (freeout, freein) = heapq.heappop(stubheap) + freein *= -1 + if freein > len(stubheap) + len(zeroheap): + return False + + # Attach out stubs to the nodes with the most in stubs + mslen = 0 + for i in range(freein): + if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0]): + stubout = heapq.heappop(zeroheap) + stubin = 0 + else: + (stubout, stubin) = heapq.heappop(stubheap) + if stubout == 0: + return False + # Check if target is now totally connected + if stubout + 1 < 0 or stubin < 0: + modstubs[mslen] = (stubout + 1, stubin) + mslen += 1 + + # Add back the nodes to the heap that still have available stubs + for i in range(mslen): + stub = modstubs[i] + if stub[1] < 0: + heapq.heappush(stubheap, stub) + else: + heapq.heappush(zeroheap, stub[0]) + if freeout < 0: + heapq.heappush(zeroheap, freeout) + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hierarchy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..d5a05525e7ddf1e98b1e07f120df0b0b5b52414b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hierarchy.py @@ -0,0 +1,57 @@ +""" +Flow Hierarchy. +""" + +import networkx as nx + +__all__ = ["flow_hierarchy"] + + +@nx._dispatchable(edge_attrs="weight") +def flow_hierarchy(G, weight=None): + """Returns the flow hierarchy of a directed network. + + Flow hierarchy is defined as the fraction of edges not participating + in cycles in a directed graph [1]_. + + Parameters + ---------- + G : DiGraph or MultiDiGraph + A directed graph + + weight : string, optional (default=None) + Attribute to use for edge weights. If None the weight defaults to 1. + + Returns + ------- + h : float + Flow hierarchy value + + Raises + ------ + NetworkXError + If `G` is not a directed graph or if `G` has no edges. + + Notes + ----- + The algorithm described in [1]_ computes the flow hierarchy through + exponentiation of the adjacency matrix. This function implements an + alternative approach that finds strongly connected components. + An edge is in a cycle if and only if it is in a strongly connected + component, which can be found in $O(m)$ time using Tarjan's algorithm. + + References + ---------- + .. [1] Luo, J.; Magee, C.L. (2011), + Detecting evolving patterns of self-organizing networks by flow + hierarchy measurement, Complexity, Volume 16 Issue 6 53-61. + DOI: 10.1002/cplx.20368 + http://web.mit.edu/~cmagee/www/documents/28-DetectingEvolvingPatterns_FlowHierarchy.pdf + """ + # corner case: G has no edges + if nx.is_empty(G): + raise nx.NetworkXError("flow_hierarchy not applicable to empty graphs") + if not G.is_directed(): + raise nx.NetworkXError("G must be a digraph in flow_hierarchy") + scc = nx.strongly_connected_components(G) + return 1 - sum(G.subgraph(c).size(weight) for c in scc) / G.size(weight) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hybrid.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hybrid.py new file mode 100644 index 0000000000000000000000000000000000000000..9d3dd3078cd25fb520a20f5866043ad977ef02f5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/hybrid.py @@ -0,0 +1,196 @@ +""" +Provides functions for finding and testing for locally `(k, l)`-connected +graphs. + +""" + +import copy + +import networkx as nx + +__all__ = ["kl_connected_subgraph", "is_kl_connected"] + + +@nx._dispatchable(returns_graph=True) +def kl_connected_subgraph(G, k, l, low_memory=False, same_as_graph=False): + """Returns the maximum locally `(k, l)`-connected subgraph of `G`. + + A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the + graph there are at least `l` edge-disjoint paths of length at most `k` + joining `u` to `v`. + + Parameters + ---------- + G : NetworkX graph + The graph in which to find a maximum locally `(k, l)`-connected + subgraph. + + k : integer + The maximum length of paths to consider. A higher number means a looser + connectivity requirement. + + l : integer + The number of edge-disjoint paths. A higher number means a stricter + connectivity requirement. + + low_memory : bool + If this is True, this function uses an algorithm that uses slightly + more time but less memory. + + same_as_graph : bool + If True then return a tuple of the form `(H, is_same)`, + where `H` is the maximum locally `(k, l)`-connected subgraph and + `is_same` is a Boolean representing whether `G` is locally `(k, + l)`-connected (and hence, whether `H` is simply a copy of the input + graph `G`). + + Returns + ------- + NetworkX graph or two-tuple + If `same_as_graph` is True, then this function returns a + two-tuple as described above. Otherwise, it returns only the maximum + locally `(k, l)`-connected subgraph. + + See also + -------- + is_kl_connected + + References + ---------- + .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid + Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg, + 2004. 89--104. + + """ + H = copy.deepcopy(G) # subgraph we construct by removing from G + + graphOK = True + deleted_some = True # hack to start off the while loop + while deleted_some: + deleted_some = False + # We use `for edge in list(H.edges()):` instead of + # `for edge in H.edges():` because we edit the graph `H` in + # the loop. Hence using an iterator will result in + # `RuntimeError: dictionary changed size during iteration` + for edge in list(H.edges()): + (u, v) = edge + # Get copy of graph needed for this search + if low_memory: + verts = {u, v} + for i in range(k): + for w in verts.copy(): + verts.update(G[w]) + G2 = G.subgraph(verts).copy() + else: + G2 = copy.deepcopy(G) + ### + path = [u, v] + cnt = 0 + accept = 0 + while path: + cnt += 1 # Found a path + if cnt >= l: + accept = 1 + break + # record edges along this graph + prev = u + for w in path: + if prev != w: + G2.remove_edge(prev, w) + prev = w + # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1? + try: + path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1? + except nx.NetworkXNoPath: + path = False + # No Other Paths + if accept == 0: + H.remove_edge(u, v) + deleted_some = True + if graphOK: + graphOK = False + # We looked through all edges and removed none of them. + # So, H is the maximal (k,l)-connected subgraph of G + if same_as_graph: + return (H, graphOK) + return H + + +@nx._dispatchable +def is_kl_connected(G, k, l, low_memory=False): + """Returns True if and only if `G` is locally `(k, l)`-connected. + + A graph is locally `(k, l)`-connected if for each edge `(u, v)` in the + graph there are at least `l` edge-disjoint paths of length at most `k` + joining `u` to `v`. + + Parameters + ---------- + G : NetworkX graph + The graph to test for local `(k, l)`-connectedness. + + k : integer + The maximum length of paths to consider. A higher number means a looser + connectivity requirement. + + l : integer + The number of edge-disjoint paths. A higher number means a stricter + connectivity requirement. + + low_memory : bool + If this is True, this function uses an algorithm that uses slightly + more time but less memory. + + Returns + ------- + bool + Whether the graph is locally `(k, l)`-connected subgraph. + + See also + -------- + kl_connected_subgraph + + References + ---------- + .. [1] Chung, Fan and Linyuan Lu. "The Small World Phenomenon in Hybrid + Power Law Graphs." *Complex Networks*. Springer Berlin Heidelberg, + 2004. 89--104. + + """ + graphOK = True + for edge in G.edges(): + (u, v) = edge + # Get copy of graph needed for this search + if low_memory: + verts = {u, v} + for i in range(k): + [verts.update(G.neighbors(w)) for w in verts.copy()] + G2 = G.subgraph(verts) + else: + G2 = copy.deepcopy(G) + ### + path = [u, v] + cnt = 0 + accept = 0 + while path: + cnt += 1 # Found a path + if cnt >= l: + accept = 1 + break + # record edges along this graph + prev = u + for w in path: + if w != prev: + G2.remove_edge(prev, w) + prev = w + # path = shortest_path(G2, u, v, k) # ??? should "Cutoff" be k+1? + try: + path = nx.shortest_path(G2, u, v) # ??? should "Cutoff" be k+1? + except nx.NetworkXNoPath: + path = False + # No Other Paths + if accept == 0: + graphOK = False + break + # return status + return graphOK diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isolate.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isolate.py new file mode 100644 index 0000000000000000000000000000000000000000..134cdff49f3a2b6079b13602309a66015af00f2c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isolate.py @@ -0,0 +1,107 @@ +""" +Functions for identifying isolate (degree zero) nodes. +""" + +import networkx as nx + +__all__ = ["is_isolate", "isolates", "number_of_isolates"] + + +@nx._dispatchable +def is_isolate(G, n): + """Determines whether a node is an isolate. + + An *isolate* is a node with no neighbors (that is, with degree + zero). For directed graphs, this means no in-neighbors and no + out-neighbors. + + Parameters + ---------- + G : NetworkX graph + + n : node + A node in `G`. + + Returns + ------- + is_isolate : bool + True if and only if `n` has no neighbors. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edge(1, 2) + >>> G.add_node(3) + >>> nx.is_isolate(G, 2) + False + >>> nx.is_isolate(G, 3) + True + """ + return G.degree(n) == 0 + + +@nx._dispatchable +def isolates(G): + """Iterator over isolates in the graph. + + An *isolate* is a node with no neighbors (that is, with degree + zero). For directed graphs, this means no in-neighbors and no + out-neighbors. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + iterator + An iterator over the isolates of `G`. + + Examples + -------- + To get a list of all isolates of a graph, use the :class:`list` + constructor: + + >>> G = nx.Graph() + >>> G.add_edge(1, 2) + >>> G.add_node(3) + >>> list(nx.isolates(G)) + [3] + + To remove all isolates in the graph, first create a list of the + isolates, then use :meth:`Graph.remove_nodes_from`: + + >>> G.remove_nodes_from(list(nx.isolates(G))) + >>> list(G) + [1, 2] + + For digraphs, isolates have zero in-degree and zero out_degree: + + >>> G = nx.DiGraph([(0, 1), (1, 2)]) + >>> G.add_node(3) + >>> list(nx.isolates(G)) + [3] + + """ + return (n for n, d in G.degree() if d == 0) + + +@nx._dispatchable +def number_of_isolates(G): + """Returns the number of isolates in the graph. + + An *isolate* is a node with no neighbors (that is, with degree + zero). For directed graphs, this means no in-neighbors and no + out-neighbors. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + int + The number of degree zero nodes in the graph `G`. + + """ + return sum(1 for v in isolates(G)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..58c22688660073a6abb59f7639871f711d1bd6ac --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/__init__.py @@ -0,0 +1,7 @@ +from networkx.algorithms.isomorphism.isomorph import * +from networkx.algorithms.isomorphism.vf2userfunc import * +from networkx.algorithms.isomorphism.matchhelpers import * +from networkx.algorithms.isomorphism.temporalisomorphvf2 import * +from networkx.algorithms.isomorphism.ismags import * +from networkx.algorithms.isomorphism.tree_isomorphism import * +from networkx.algorithms.isomorphism.vf2pp import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/ismags.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/ismags.py new file mode 100644 index 0000000000000000000000000000000000000000..34795941c7d6a36599e07421d41435a341a30c59 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/ismags.py @@ -0,0 +1,1306 @@ +""" +ISMAGS Algorithm +================ + +Provides a Python implementation of the ISMAGS algorithm. [1]_ + +ISMAGS does a symmetry analysis to find the constraints on isomorphisms if +we preclude yielding isomorphisms that differ by a symmetry of the subgraph. +For example, if the subgraph contains a 4-cycle, every isomorphism would have a +symmetric version with the nodes rotated relative to the original isomorphism. +By encoding these symmetries as constraints we reduce the search space for +isomorphisms and we also simplify processing the resulting isomorphisms. + +ISMAGS finds (subgraph) isomorphisms between two graphs, taking the +symmetry of the subgraph into account. In most cases the VF2 algorithm is +faster (at least on small graphs) than this implementation, but in some cases +there are an exponential number of isomorphisms that are symmetrically +equivalent. In that case, the ISMAGS algorithm will provide only one isomorphism +per symmetry group, speeding up finding isomorphisms and avoiding the task of +post-processing many effectively identical isomorphisms. + +>>> petersen = nx.petersen_graph() +>>> ismags = nx.isomorphism.ISMAGS(petersen, petersen) +>>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=False)) +>>> len(isomorphisms) +120 +>>> isomorphisms = list(ismags.isomorphisms_iter(symmetry=True)) +>>> answer = [{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}] +>>> answer == isomorphisms +True + +In addition, this implementation also provides an interface to find the +largest common induced subgraph [2]_ between any two graphs, again taking +symmetry into account. Given ``graph`` and ``subgraph`` the algorithm will remove +nodes from the ``subgraph`` until ``subgraph`` is isomorphic to a subgraph of +``graph``. Since only the symmetry of ``subgraph`` is taken into account it is +worth thinking about how you provide your graphs: + +>>> graph1 = nx.path_graph(4) +>>> graph2 = nx.star_graph(3) +>>> ismags = nx.isomorphism.ISMAGS(graph1, graph2) +>>> ismags.is_isomorphic() +False +>>> largest_common_subgraph = list(ismags.largest_common_subgraph()) +>>> answer = [{1: 0, 0: 1, 2: 2}, {2: 0, 1: 1, 3: 2}] +>>> answer == largest_common_subgraph +True +>>> ismags2 = nx.isomorphism.ISMAGS(graph2, graph1) +>>> largest_common_subgraph = list(ismags2.largest_common_subgraph()) +>>> answer = [ +... {1: 0, 0: 1, 2: 2}, +... {1: 0, 0: 1, 3: 2}, +... {2: 0, 0: 1, 1: 2}, +... {2: 0, 0: 1, 3: 2}, +... {3: 0, 0: 1, 1: 2}, +... {3: 0, 0: 1, 2: 2}, +... ] +>>> answer == largest_common_subgraph +True + +However, when not taking symmetry into account, it doesn't matter: + +>>> largest_common_subgraph = list(ismags.largest_common_subgraph(symmetry=False)) +>>> answer = [ +... {1: 0, 0: 1, 2: 2}, +... {1: 0, 2: 1, 0: 2}, +... {2: 0, 1: 1, 3: 2}, +... {2: 0, 3: 1, 1: 2}, +... {1: 0, 0: 1, 2: 3}, +... {1: 0, 2: 1, 0: 3}, +... {2: 0, 1: 1, 3: 3}, +... {2: 0, 3: 1, 1: 3}, +... {1: 0, 0: 2, 2: 3}, +... {1: 0, 2: 2, 0: 3}, +... {2: 0, 1: 2, 3: 3}, +... {2: 0, 3: 2, 1: 3}, +... ] +>>> answer == largest_common_subgraph +True +>>> largest_common_subgraph = list(ismags2.largest_common_subgraph(symmetry=False)) +>>> answer = [ +... {1: 0, 0: 1, 2: 2}, +... {1: 0, 0: 1, 3: 2}, +... {2: 0, 0: 1, 1: 2}, +... {2: 0, 0: 1, 3: 2}, +... {3: 0, 0: 1, 1: 2}, +... {3: 0, 0: 1, 2: 2}, +... {1: 1, 0: 2, 2: 3}, +... {1: 1, 0: 2, 3: 3}, +... {2: 1, 0: 2, 1: 3}, +... {2: 1, 0: 2, 3: 3}, +... {3: 1, 0: 2, 1: 3}, +... {3: 1, 0: 2, 2: 3}, +... ] +>>> answer == largest_common_subgraph +True + +Notes +----- +- Node and edge equality is assumed to be transitive: if A is equal to B, and + B is equal to C, then A is equal to C. + +- With a method that yields subgraph isomorphisms, we can construct functions like + ``is_subgraph_isomorphic`` by checking for any yielded mapping. And functions like + ``is_isomorphic`` by checking whether the subgraph has the same number of nodes as + the graph and is also subgraph isomorphic. This subpackage also allows a + ``symmetry`` bool keyword argument so you can find isomorphisms with or + without the symmetry constraints. + +- For more information, see [2]_ and the documentation for :class:`ISMAGS` + which includes a description of the algorithm. + +References +---------- +.. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle, + M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General + Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph + Enumeration", PLoS One 9(5): e97896, 2014. + https://doi.org/10.1371/journal.pone.0097896 +.. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph +""" + +__all__ = ["ISMAGS"] + +import itertools +from collections import Counter, defaultdict +from functools import reduce, wraps + +import networkx as nx + + +def are_all_equal(iterable): + """ + Returns ``True`` if and only if all elements in `iterable` are equal; and + ``False`` otherwise. + + Parameters + ---------- + iterable: collections.abc.Iterable + The container whose elements will be checked. + + Returns + ------- + bool + ``True`` iff all elements in `iterable` compare equal, ``False`` + otherwise. + """ + try: + shape = iterable.shape + except AttributeError: + pass + else: + if len(shape) > 1: + message = "The function does not works on multidimensional arrays." + raise NotImplementedError(message) from None + + iterator = iter(iterable) + first = next(iterator, None) + return all(item == first for item in iterator) + + +def make_partition(items, test, check=True): + """ + Partitions items into sets based on the outcome of ``test(item1, item2)``. + Pairs of items for which `test` returns `True` end up in the same set. + + Parameters + ---------- + items : collections.abc.Iterable[collections.abc.Hashable] + Items to partition + test : collections.abc.Callable[collections.abc.Hashable, collections.abc.Hashable] + A function that will be called with 2 arguments, taken from items. + Should return `True` if those 2 items match/tests so need to end up in the same + part of the partition, and `False` otherwise. + check : bool optional (default: True) + If ``True``, check that the resulting partition satisfies the match criteria. + Every item should match every item in its part and none outside the part. + + Returns + ------- + list[set] + A partition as a list of sets (the parts). Each set contains some of + the items in `items`, such that all items are in exactly one part and every + pair of items in each part matches. The following will be true: + ``all(thing_matcher(*pair) for pair in itertools.combinations(items, 2))`` + + Notes + ----- + The function `test` is assumed to be transitive: if ``test(a, b)`` and + ``test(b, c)`` return ``True``, then ``test(a, c)`` must also be ``True``. + The function `test` is assumed to be commutative: if ``test(a, b)`` + returns ``True`` then ``test(b, a)`` returns ``True``. + """ + partition = [] + for item in items: + for part in partition: + p_item = next(iter(part)) + if test(item, p_item): + part.add(item) + break + else: # No break + partition.append({item}) + + if check: + if not all( + test(t1, t2) and test(t2, t1) + for part in partition + for t1, t2 in itertools.combinations(part, 2) + ): + raise nx.NetworkXError( + f"\nInvalid partition created with {test}.\n" + "Some items in a part do not match. This leads to\n" + f"{partition=}" + ) + if not all( + not test(t1, t2) and not test(t2, t1) + for p1 in partition + for p2 in partition + if p1 != p2 + for t1, t2 in itertools.product(p1, p2) + ): + raise nx.NetworkXError( + f"\nInvalid partition created with {test}.\n" + "Some items match multiple parts. This leads to\n" + f"{partition=}" + ) + return [set(part) for part in partition] + + +def node_to_part_ID_dict(partition): + """ + Creates a dictionary that maps each item in each part to the index of + the part to which it belongs. + + Parameters + ---------- + partition: collections.abc.Sequence[collections.abc.Iterable] + As returned by :func:`make_partition`. + + Returns + ------- + dict + """ + return {node: ID for ID, part in enumerate(partition) for node in part} + + +def color_degree_by_node(G, n_colors, e_colors): + """Returns a dict by node to counts of edge and node color for that node. + + This returns a dict by node to a 2-tuple of node color and degree by + (edge color and nbr color). E.g. ``{0: (1, {(0, 2): 5})}`` means that + node ``0`` has node type 1 and has 5 edges of type 0 that go to nodes of type 2. + Thus, this is a measure of degree (edge count) by color of edge and color + of the node on the other side of that edge. + + For directed graphs the degree counts is a 2-tuple of (in, out) degree counts. + + Ideally, if edge_match is None, this could get simplified to just the node + color on the other end of the edge. Similarly if node_match is None then only + edge color is tracked. And if both are None, we simply count the number of edges. + """ + # n_colors might be incomplete when using `largest_common_subgraph()` + if len(n_colors) < len(G): + for n, nbrs in G.adjacency(): + if n not in n_colors: + n_colors[n] = None + for v in nbrs: + e_colors[n, v] = None + # undirected colored degree + if not G.is_directed(): + return { + u: (n_colors[u], Counter((e_colors[u, v], n_colors[v]) for v in nbrs)) + for u, nbrs in G.adjacency() + } + # directed colored out and in degree + return { + u: ( + n_colors[u], + Counter((e_colors[u, v], n_colors[v]) for v in nbrs), + Counter((e_colors[v, u], n_colors[v]) for v in G._pred[u]), + ) + for u, nbrs in G.adjacency() + } + + +class EdgeLookup: + """Class to handle getitem for undirected edges. + + Note that ``items()`` iterates over one of the two representations of the edge + (u, v) and (v, u). So this technically doesn't violate the Mapping + invariant that (k,v) pairs reported by ``items()`` satisfy ``.__getitem__(k) == v``. + But we are violating the spirit of the protocol by having keys available + for lookup by ``__getitem__`` that are not reported by ``items()``. + + Note that if we used frozensets for undirected edges we would have the same + behavior we see here. You could ``__getitem__`` either ``{u, v}`` or ``{v, u}`` + and get the same value -- yet ``items()`` would only report one of the two. + So from that perspective we *are* following the Mapping protocol. Our keys + are undirected edges. We are using 2-tuples as an imperfect representation + of these edges. We are not using 2-tuples as keys. Only as imperfect edges + and we use the edges as keys. + """ + + def __init__(self, edge_dict): + self.edge_dict = edge_dict + + def __getitem__(self, edge): + if edge in self.edge_dict: + return self.edge_dict[edge] + return self.edge_dict[edge[::-1]] + + def items(self): + return self.edge_dict.items() + + +class ISMAGS: + """ + Implements the ISMAGS subgraph matching algorithm. [1]_ ISMAGS stands for + "Index-based Subgraph Matching Algorithm with General Symmetries". As the + name implies, it is symmetry aware and will only generate non-symmetric + isomorphisms. + + Attributes + ---------- + graph: networkx.Graph + subgraph: networkx.Graph + + Notes + ----- + ISMAGS does a symmetry analysis to find the constraints on isomorphisms if + we preclude yielding isomorphisms that differ by a symmetry of the subgraph. + For example, if the subgraph is a 4-cycle, every isomorphism would have a + symmetric version with the nodes rotated relative to the original isomorphism. + By encoding these symmetries as constraints we reduce the search space for + isomorphisms and we also simplify processing the resulting isomorphisms. + + **Symmetry Analysis** + + The constraints in ISMAGS are based off the handling in ``nauty`` and its many + variants, in particular ``saucy``, as discussed in the ISMAGS paper [1]_. + That paper cites [3]_ for details on symmetry handling. Figure 2 of [3]_ + describes the DFS approach to symmetries used here and relying on a data structure + called an Ordered Pair Partitions(OPP). This consists of a pair of partitions + where each part represents nodes with the same degree-by-color over all colors. + We refine these partitions simultaneously in a way to result in permutations + of the nodes that preserve the graph structure. We thus find automorphisms + for the subgraph of interest. From those we identify pairs of nodes which + are structurally equivalent. We then constrain our problem by requiring the + first of the pair to always be assigned first in the isomorphism -- thus + constraining the isomorphisms reported to only one example from the set of all + symmetrically equivalent isomorphisms. These constraints are computed once + based on the subgraph symmetries and then used throughout the DFS search for + isomorphisms. + + Finding the symmetries involves a DFS of the OPP wherein we "couple" a node + to a node in its degree-by-color part of the partition. This "coupling" is done + via assigning a new color in the top partition to the node being coupled, + and the same new color in the bottom partition to the node being coupled to. + This new color has only one node in each partition. The new color also requires + that we "refine" both top and bottom partitions by splitting parts until each + part represents a common degree-by-color value. Those refinements introduce + new colors as the parts are split during refinement. Parts do not get combined + during refinement. So the coupling/refining process always results in at least + one new part with only one node in both the top and bottom partition. In practice + we usually refine into many new one-node parts in both partitions. + We continue in this way until each node has its own part/color in the top partition + -- and the node in the bottom partition with that color is the symmetric node. + That is, an OPP represents an automorphism, and thus a symmetry + of the subgraph when each color has a single node in the top partition and a single + node in the bottom partition. From those automorphisms we build up a set of nodes + that can be obtained from each other by symmetry (they are mutually symmetric). + That set of nodes is called an "orbit" of the subgraph under symmetry. + + After finding the orbits for one symmetry, we backtrack in the DFS by removing the + latest coupling and replacing it with a coupling from the same top node to a new + bottom node in its degree-by-color grouping. When all possible couplings for that + node are considered we backtrack to the previously coupled node and recouple in + a DFS manner. + + We can prune the DFS search tree in helpful ways. The paper [2]_ demonstrates 6 + situations of interest in the DFS where pruning is possible: + + - An **Automorphism OPP** is an OPP where every part in both partitions + contains a single node. The mapping/automorphism is found by mapping + each top node to the bottom node in the same color part. For example, + ``[({1}, {2}, {3}); ({2}, {3}, {1})]`` represents the mapping of each + node to the next in a triangle. It rotates the nodes around the triangle. + - The **Identity OPP** is the first automorphism found during the DFS. It + appears on the left side of the DFS tree and is first due to our ordering of + coupling nodes to be in an arbitrary but fixed ordering of the nodes. This + automorphism does not show any symmetries, but it ensures the orbit for each + node includes itself and it sets us up for handling the symmetries. Note that + a subgraph with no symmetries will only have the identity automorphism. + - A **Non-isomorphic OPP** occurs when refinement creates a different number of + parts in the top partition than in the bottom partition. This means no symmetries + will be found during further processing of that subtree of the DFS. We prune + the subtree and continue. + - A **Matching OPP** is such that each top part that has more than one node is + in fact equal to the bottom part with the same color. The many-node-parts match + exactly. The single-node parts then represent symmetries that do not permute + any matching nodes. Matching OPPs arise while finding the Identity Mapping. But + the single-node parts are identical in the two partitions, so no useful symmetries + are found. But after the Identity Mapping is found, every Matching OPP encountered + will have different nodes in at least two single-node parts of the same color. + So these positions in the DFS provide us with symmetries without any + need to find the whole automorphism. We can prune the subtree, update the orbits + and backtrack. Any larger symmetries that combine these symmetries with symmetries + of the many-node-parts do not need to be explored because the symmetry "generators" + found in this way provide a basis for all symmetries. We will find the symmetry + generators of the many-node-parts at another subtree of the DFS. + - An **Orbit Pruning OPP** is an OPP where the node coupling to be considered next + has both nodes already known to be in the same orbit. We have already identified + those permutations when we discovered the orbit. So we can prune the resulting + subtree. This is the primary pruning discussed in [1]_. + - A **Coset Point** in the DFS is a point of the tree when a node is first + back-tracked. That is, its couplings have all been analyzed once and we backtrack + to its parent. So, said another way, when a node is backtracked to and is about to + be mapped to a different node for the first time, its child in the DFS has been + completely analysed. Thus the orbit for that child at this point in the DFS is + the full orbit for symmetries involving only that child or larger nodes in the + node order. All smaller nodes are mapped to themselves. + This orbit is due to symmetries not involving smaller nodes. Such an orbit is + called the "coset" of that node. The Coset Point does not lead to pruning or to + more symmetries. It is the point in the process where we store the **coset** of + the node being backtracked. We use the cosets to construct the symmetry + constraints. + + Once the pruned DFS tree has been traversed, we have collected the cosets of some + special nodes. Often most nodes are not coupled during the progression down the left + side of the DFS. They are separated from other nodes during the partition refinement + process after coupling. So they never get coupled directly. Thus the number of cosets + we find is typically many fewer than the number of nodes. + + We turn those cosets into constraints on the nodes when building non-symmetric + isomorphisms. The node whose coset is used is paired with each other node in the + coset. These node-pairs form the constraints. During isomorphism construction we + always select the first of the constraint before the other. This removes subtrees + from the DFS traversal space used to build isomorphisms. + + The constraints we obtain via symmetry analysis of the subgraph are used for + finding non-symmetric isomorphisms. We prune the isomorphism tree based on + the constraints we obtain from the symmetry analysis. + + **Isomorphism Construction** + + Once we have symmetry constraints on the isomorphisms, ISMAGS constructs the allowed + isomorphisms by mapping each node of the subgraph to all possible nodes (with the + same degree-by-color) from the graph. We partition all nodes into degree-by-color + parts and order the subgraph nodes we consider using smallest part size first. + The idea is to try to map the most difficult subgraph nodes first (most difficult + here means least number of graph candidates). + + By considering each potential subgraph node to graph candidate mapping image in turn, + we perform a DFS traversal of partial mappings. If the mapping is rejected due to + the graph neighbors not matching the degree-by-color of the subgraph neighbors, or + rejected due to the constraints imposed from symmetry, we prune that subtree and + consider a new graph candidate node for that subgraph node. When no more graph + candidates remain we backtrack to the previous node in the mapping and consider a + new graph candidate for that node. If we ever get to a depth where all subgraph nodes + are mapped and no structural requirements or symmetry constraints are violated, + we have found an isomorphism. We yield that mapping and backtrack to find other + isomorphisms. + + As we visit more neighbors, the graph candidate nodes have to satisfy more structural + restrictions. As described in the ISMAGS paper, [1]_, we store each set of structural + restrictions separately as a set of possible candidate nodes rather than computing + the intersection of that set with the known graph candidates for the subgraph node. + We delay taking the intersection until that node is selected to be in the mapping. + While choosing the node with fewest candidates, we avoid computing the intersection + by using the size of the minimal set to be intersected rather than the size of the + intersection. This may make the node ordering slightly worse via a savings of + many intersections most of which are not ever needed. + + References + ---------- + .. [1] M. Houbraken, S. Demeyer, T. Michoel, P. Audenaert, D. Colle, + M. Pickavet, "The Index-Based Subgraph Matching Algorithm with General + Symmetries (ISMAGS): Exploiting Symmetry for Faster Subgraph + Enumeration", PLoS One 9(5): e97896, 2014. + https://doi.org/10.1371/journal.pone.0097896 + .. [2] https://en.wikipedia.org/wiki/Maximum_common_induced_subgraph + .. [3] Hadi Katebi, Karem A. Sakallah and Igor L. Markov + "Graph Symmetry Detection and Canonical Labeling: Differences and Synergies" + in "Turing-100. The Alan Turing Centenary" Ed: A. Voronkov p. 181 -- 195, (2012). + https://doi.org/10.29007/gzc1 https://arxiv.org/abs/1208.6271 + """ + + def __init__(self, graph, subgraph, node_match=None, edge_match=None, cache=None): + """ + Parameters + ---------- + graph: networkx.Graph + subgraph: networkx.Graph + node_match: collections.abc.Callable or None + Function used to determine whether two nodes are equivalent. Its + signature should look like ``f(n1: dict, n2: dict) -> bool``, with + `n1` and `n2` node property dicts. See also + :func:`~networkx.algorithms.isomorphism.categorical_node_match` and + friends. + If `None`, all nodes are considered equal. + edge_match: collections.abc.Callable or None + Function used to determine whether two edges are equivalent. Its + signature should look like ``f(e1: dict, e2: dict) -> bool``, with + `e1` and `e2` edge property dicts. See also + :func:`~networkx.algorithms.isomorphism.categorical_edge_match` and + friends. + If `None`, all edges are considered equal. + cache: collections.abc.Mapping + A cache used for caching graph symmetries. + """ + if graph.is_directed() != subgraph.is_directed(): + raise ValueError("Directed and undirected graphs cannot be compared.") + + # TODO: allow for precomputed partitions and colors + self.graph = graph + self.subgraph = subgraph + self._symmetry_cache = cache + # Naming conventions are taken from the original paper. + # For your sanity: + # sg: subgraph + # g: graph + # e: edge(s) + # n: node(s) + # So: sgn means "subgraph nodes". + node_parts = self.create_aligned_partitions( + node_match, self.subgraph.nodes, self.graph.nodes + ) + self._sgn_partition, self._gn_partition, self.N_node_colors = node_parts + self._sgn_colors = node_to_part_ID_dict(self._sgn_partition) + self._gn_colors = node_to_part_ID_dict(self._gn_partition) + + edge_partitions = self.create_aligned_partitions( + edge_match, self.subgraph.edges(), self.graph.edges() + ) + self._sge_partition, self._ge_partition, self.N_edge_colors = edge_partitions + if self.graph.is_directed(): + self._sge_colors = node_to_part_ID_dict(self._sge_partition) + self._ge_colors = node_to_part_ID_dict(self._ge_partition) + else: # allow lookups (u, v) or (v, u) + self._sge_colors = EdgeLookup(node_to_part_ID_dict(self._sge_partition)) + self._ge_colors = EdgeLookup(node_to_part_ID_dict(self._ge_partition)) + + def create_aligned_partitions(self, thing_matcher, sg_things, g_things): + """Partitions of "things" (nodes or edges) from subgraph and graph + based on function `thing_matcher`. + + Returns: sg_partition, g_partition, number_of_matched_parts + + The first `number_of_matched_parts` parts in each partition + match in order, e.g. 2nd part matches other's 2nd part. + Warning: nodes in parts after that have no matching nodes in the other graph. + For morphisms those nodes can't appear in the mapping. + """ + if thing_matcher is None: + sg_partition = [set(sg_things)] + g_partition = [set(g_things)] + return sg_partition, g_partition, 1 + + # Use thing_matcher to create a partition + # Note: isinstance(G.edges(), OutEdgeDataView) is only true for multi(di)graph + sg_multiedge = isinstance(sg_things, nx.classes.reportviews.OutEdgeDataView) + g_multiedge = isinstance(g_things, nx.classes.reportviews.OutEdgeDataView) + if not sg_multiedge: + + def sg_match(thing1, thing2): + return thing_matcher(sg_things[thing1], sg_things[thing2]) + + else: # multiedges (note nodes of multigraphs use simple case above) + + def sg_match(thing1, thing2): + (u1, v1), (u2, v2) = thing1, thing2 + return thing_matcher(self.subgraph[u1][v1], self.subgraph[u2][v2]) + + if not g_multiedge: + + def g_match(thing1, thing2): + return thing_matcher(g_things[thing1], g_things[thing2]) + + else: # multiedges (note nodes of multigraphs use simple case above) + + def g_match(thing1, thing2): + (u1, v1), (u2, v2) = thing1, thing2 + return thing_matcher(self.graph[u1][v1], self.graph[u2][v2]) + + sg_partition = make_partition(sg_things, sg_match) + g_partition = make_partition(g_things, g_match) + + # Align order of g_partition to that of sg_partition + sgc_to_gc = {} + gc_to_sgc = {} + sN, N = len(sg_partition), len(g_partition) + for sgc, gc in itertools.product(range(sN), range(N)): + sgt = next(iter(sg_partition[sgc])) + gt = next(iter(g_partition[gc])) + sgt_ = sg_things[sgt] if not sg_multiedge else self.subgraph[sgt[0]][sgt[1]] + gt_ = g_things[gt] if not g_multiedge else self.graph[gt[0]][gt[1]] + if thing_matcher(sgt_, gt_): + # TODO: remove these two if-checks when confident they never arise + # The `check` feature in match_partitions should ensure they do not + if sgc in sgc_to_gc: + raise nx.NetworkXError( + f"\nMatching function {thing_matcher} seems faulty.\n" + f"Partition found: {sg_partition=}\n" + f"So {sgt} in subgraph part {sg_partition[sgc]} matches two " + f"graph parts {g_partition[gc]} and " + f"{g_partition[sgc_to_gc[sgc]]}\n" + ) + if gc in gc_to_sgc: + raise nx.NetworkXError( + f"\nMatching function seems broken: {thing_matcher}\n" + f"Partitions found: {g_partition=} {sg_partition=}\n" + f"So {gt} in graph part {g_partition[gc]} matches two " + f"subgraph parts {sg_partition[sgc]} and " + f"{sg_partition[gc_to_sgc[gc]]}\n" + ) + sgc_to_gc[sgc] = gc + gc_to_sgc[gc] = sgc + ## return two lists and the number of partitions that match. + new_order = [ + (sg_partition[sgc], g_partition[gc]) for sgc, gc in sgc_to_gc.items() + ] + Ncolors = len(new_order) + if Ncolors: + new_sg_p, new_g_p = [list(x) for x in zip(*new_order)] + else: + new_sg_p, new_g_p = [], [] + if Ncolors < sN: + extra = [sg_partition[c] for c in range(sN) if c not in sgc_to_gc] + new_sg_p = list(new_sg_p) + extra + new_g_p = list(new_g_p) + [set()] * len(extra) + if Ncolors < N: + extra = [g_partition[c] for c in range(N) if c not in gc_to_sgc] + new_g_p = list(new_g_p) + extra + new_sg_p = list(new_sg_p) + [set()] * len(extra) + + return new_sg_p, new_g_p, Ncolors + + def find_isomorphisms(self, symmetry=True): + """Find all subgraph isomorphisms between subgraph and graph + + Finds isomorphisms where :attr:`subgraph` <= :attr:`graph`. + + Parameters + ---------- + symmetry: bool + Whether symmetry should be taken into account. If False, found + isomorphisms may be symmetrically equivalent. + + Yields + ------ + dict + The found isomorphism mappings of {graph_node: subgraph_node}. + """ + # The networkx VF2 algorithm is slightly funny in when it yields an + # empty dict and when not. + if not self.subgraph: + yield {} + return + elif not self.graph: + return + elif len(self.graph) < len(self.subgraph): + return + elif len(self._sgn_partition) > self.N_node_colors: + # some subgraph nodes have a color that doesn't occur in graph + return + elif len(self._sge_partition) > self.N_edge_colors: + # some subgraph edges have a color that doesn't occur in graph + return + + if symmetry: + cosets = self.analyze_subgraph_symmetry() + # Turn cosets into constraints. + constraints = [(n, co) for n, cs in cosets.items() for co in cs if n != co] + else: + constraints = [] + + cand_sets = self._get_node_color_candidate_sets() + + lookahead_candidates = self._get_color_degree_candidates() + for sgn, lookahead_cands in lookahead_candidates.items(): + cand_sets[sgn].add(frozenset(lookahead_cands)) + + if any(cand_sets.values()): + # Choose start node based on a heuristic for the min # of candidates + # Heuristic here is length of smallest frozenset in candidates' set + # of frozensets for that node. Using the smallest length avoids + # computing the intersection of the frozensets for each node. + start_sgn = min(cand_sets, key=lambda n: min(len(x) for x in cand_sets[n])) + cand_sets[start_sgn] = (frozenset.intersection(*cand_sets[start_sgn]),) + yield from self._map_nodes(start_sgn, cand_sets, constraints) + return + + def _get_color_degree_candidates(self): + """ + Returns a mapping of {subgraph node: set of graph nodes} for + which the graph nodes are feasible mapping candidate_sets for the + subgraph node, as determined by looking ahead one edge. + """ + g_deg = color_degree_by_node(self.graph, self._gn_colors, self._ge_colors) + sg_deg = color_degree_by_node(self.subgraph, self._sgn_colors, self._sge_colors) + + return { + sgn: { + gn + for gn, (_, *g_counts) in g_deg.items() + if all( + sg_cnt <= g_counts[idx][color] + for idx, counts in enumerate(needed_counts) + for color, sg_cnt in counts.items() + ) + } + for sgn, (_, *needed_counts) in sg_deg.items() + } + + def largest_common_subgraph(self, symmetry=True): + """ + Find the largest common induced subgraphs between :attr:`subgraph` and + :attr:`graph`. + + Parameters + ---------- + symmetry: bool + Whether symmetry should be taken into account. If False, found + largest common subgraphs may be symmetrically equivalent. + + Yields + ------ + dict + The found isomorphism mappings of {graph_node: subgraph_node}. + """ + # The networkx VF2 algorithm is slightly funny in when it yields an + # empty dict and when not. + if not self.subgraph: + yield {} + return + elif not self.graph: + return + + if symmetry: + cosets = self.analyze_subgraph_symmetry() + # Turn cosets into constraints. + constraints = [(n, cn) for n, cs in cosets.items() for cn in cs if n != cn] + else: + constraints = [] + + candidate_sets = self._get_node_color_candidate_sets() + + if any(candidate_sets.values()): + relevant_parts = self._sgn_partition[: self.N_node_colors] + to_be_mapped = {frozenset(n for p in relevant_parts for n in p)} + yield from self._largest_common_subgraph( + candidate_sets, constraints, to_be_mapped + ) + else: + return + + def analyze_subgraph_symmetry(self): + """ + Find a minimal set of permutations and corresponding co-sets that + describe the symmetry of ``self.subgraph``, given the node and edge + equalities given by `node_partition` and `edge_colors`, respectively. + + Returns + ------- + dict[collections.abc.Hashable, set[collections.abc.Hashable]] + The found co-sets. The co-sets is a dictionary of + ``{node key: set of node keys}``. + Every key-value pair describes which ``values`` can be interchanged + without changing nodes less than ``key``. + """ + partition, edge_colors = self._sgn_partition, self._sge_colors + + if self._symmetry_cache is not None: + key = hash( + ( + tuple(self.subgraph.nodes), + tuple(self.subgraph.edges), + tuple(map(tuple, node_partition)), + tuple(edge_colors.items()), + self.subgraph.is_directed(), + ) + ) + if key in self._symmetry_cache: + return self._symmetry_cache[key] + partition = self._refine_node_partition(self.subgraph, partition, edge_colors) + cosets = self._process_ordered_pair_partitions( + self.subgraph, partition, partition, edge_colors + ) + if self._symmetry_cache is not None: + self._symmetry_cache[key] = cosets + return cosets + + def is_isomorphic(self, symmetry=False): + """ + Returns True if :attr:`graph` is isomorphic to :attr:`subgraph` and + False otherwise. + + Returns + ------- + bool + """ + return len(self.subgraph) == len(self.graph) and self.subgraph_is_isomorphic( + symmetry + ) + + def subgraph_is_isomorphic(self, symmetry=False): + """ + Returns True if a subgraph of :attr:`graph` is isomorphic to + :attr:`subgraph` and False otherwise. + + Returns + ------- + bool + """ + # symmetry=False, since we only need to know whether there is any + # example; figuring out all symmetry elements probably costs more time + # than it gains. + isom = next(self.subgraph_isomorphisms_iter(symmetry=symmetry), None) + return isom is not None + + def isomorphisms_iter(self, symmetry=True): + """ + Does the same as :meth:`find_isomorphisms` if :attr:`graph` and + :attr:`subgraph` have the same number of nodes. + """ + if len(self.graph) == len(self.subgraph): + yield from self.subgraph_isomorphisms_iter(symmetry=symmetry) + + def subgraph_isomorphisms_iter(self, symmetry=True): + """Alternative name for :meth:`find_isomorphisms`.""" + return self.find_isomorphisms(symmetry) + + def _get_node_color_candidate_sets(self): + """ + Per node in subgraph find all nodes in graph that have the same color. + Stored as a dict-of-set-of-frozenset. The dict is keyed by node to a + collection of frozensets of graph nodes. Each of these frozensets are + a restriction. The node can be mapped only to nodes in the frozenset. + Thus it must be mapped to nodes in the intersection of all these sets. + We store the sets to delay taking the intersection of them. This helps + for two reasons: Firstly any duplicate restriction sets can be ignored; + Secondly, some nodes will not need the intersection to be constructed. + Note: a dict-of-list-of-set would store duplicate sets in the list and + we want to avoid that. But I wonder if checking hash/equality when `add`ing + removes the benefit of avoiding computing intersections. + """ + candidate_sets = defaultdict(set) + for sgn in self.subgraph.nodes: + sgn_color = self._sgn_colors[sgn] + if sgn_color >= self.N_node_colors: # color has no candidates + candidate_sets[sgn] # creates empty set entry in defaultdict + else: + candidate_sets[sgn].add(frozenset(self._gn_partition[sgn_color])) + return dict(candidate_sets) + + @classmethod + def _refine_node_partition(cls, graph, partition, edge_colors): + def equal_color(node1, node2): + return color_degree[node1] == color_degree[node2] + + node_colors = node_to_part_ID_dict(partition) + color_degree = color_degree_by_node(graph, node_colors, edge_colors) + while not all(are_all_equal(color_degree[n] for n in p) for p in partition): + partition = [ + p + for part in partition + for p in ( + [part] + if are_all_equal(color_degree[n] for n in part) + else sorted(make_partition(part, equal_color, check=False), key=len) + ) + ] + node_colors = node_to_part_ID_dict(partition) + color_degree = color_degree_by_node(graph, node_colors, edge_colors) + return partition + + def _map_nodes(self, sgn, candidate_sets, constraints, to_be_mapped=None): + """ + Find all subgraph isomorphisms honoring constraints. + The collection `candidate_sets` is stored as a dict-of-set-of-frozenset. + The dict is keyed by node to a collection of candidate frozensets. Any + viable candidate must belong to all the frozensets in the collection. + So each frozenset added to the collection is a restriction on the candidates. + + According to the paper, we store the collection of sets rather than their + intersection to delay computing many intersections with the hope of avoiding + them completely. Having the middle collection be a set also means that + duplicate restrictions on candidates are ignored, avoiding another intersection. + """ + # shortcuts for speed + subgraph = self.subgraph + subgraph_adj = subgraph._adj + graph = self.graph + graph_adj = graph._adj + self_ge_partition = self._ge_partition + self_sge_colors = self._sge_colors + is_directed = subgraph.is_directed() + + gn_ID_to_node = list(graph) + gn_node_to_ID = {n: id for id, n in enumerate(graph)} + + mapping = {} + rev_mapping = {} + if to_be_mapped is None: + to_be_mapped = subgraph_adj.keys() + + # Note that we don't copy candidates here. This means we leak + # information between the branches of the search. This is intentional! + # Specifically, we modify candidates here. That's OK because we substitute + # the set of frozensets with a set containing the frozenset intersection. + # So, it doesn't change the membership rule or the length rule for sorting. + # Membership: any candidate must be an element of each of the frozensets. + # Length: length of the intersection set. Use heuristic min(len of frozensets). + # This intersection improves future length heuristics which can only occur + # after this element of the queu is popped. But it means future additional + # restriction frozensets that duplicate previous ones are not ignored. + sgn_candidates = frozenset.intersection(*candidate_sets[sgn]) + candidate_sets[sgn] = {sgn_candidates} + queue = [(sgn, candidate_sets, iter(sgn_candidates))] + while queue: # DFS over all possible mappings + sgn, candidate_sets, sgn_cand_iter = queue[-1] + + for gn in sgn_cand_iter: + # We're going to try to map sgn to gn. + if gn in rev_mapping: + continue # pragma: no cover + + # REDUCTION and COMBINATION + if sgn in mapping: + old_gn = mapping[sgn] + del rev_mapping[old_gn] + mapping[sgn] = gn + rev_mapping[gn] = sgn + # BASECASE + if len(mapping) == len(to_be_mapped): + yield rev_mapping.copy() + del mapping[sgn] + del rev_mapping[gn] + continue + left_to_map = to_be_mapped - mapping.keys() + + # We copy the candidates dict. But it is not a deepcopy. + # This avoids inner set copies, yet still allows updates b/c setitem + # changes sgn in new dict without changing original set. + # Below be careful to not change the sets of frozensets. + cand_sets = candidate_sets.copy() + + # update the candidate_sets for unmapped sgn based on sgn mapped + if not is_directed: + sgn_nbrs = subgraph_adj[sgn] + not_gn_nbrs = graph_adj.keys() - graph_adj[gn].keys() + for sgn2 in left_to_map: + # edge color must match when sgn2 connected to sgn + if sgn2 not in sgn_nbrs: + gn2_cands = not_gn_nbrs + else: + g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]] + gn2_cands = {n for e in g_edges if gn in e for n in e} + # Node color compatibility should be taken care of by the + # initial candidate lists made by find_subgraphs + + # Add gn2_cands to the right collection. + # Do not change the original set. So do not use |= operator + cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)} + else: # directed + sgn_nbrs = subgraph_adj[sgn].keys() + sgn_preds = subgraph._pred[sgn].keys() + not_gn_nbrs = ( + graph_adj.keys() - graph_adj[gn].keys() - graph._pred[gn].keys() + ) + for sgn2 in left_to_map: + # edge color must match when sgn2 connected to sgn + if sgn2 not in sgn_nbrs: + if sgn2 not in sgn_preds: + gn2_cands = not_gn_nbrs + else: # sgn2 in sgn_preds + g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]] + gn2_cands = {e[0] for e in g_edges if gn == e[1]} + else: + if sgn2 not in sgn_preds: + g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]] + gn2_cands = {e[1] for e in g_edges if gn == e[0]} + else: + # gn2 must have correct color in both directions + g_edges = self_ge_partition[self_sge_colors[sgn, sgn2]] + gn2_cands = {e[1] for e in g_edges if gn == e[0]} + g_edges = self_ge_partition[self_sge_colors[sgn2, sgn]] + gn2_cands &= {e[0] for e in g_edges if gn == e[1]} + # Do not change the original set. So do not use |= operator + cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)} + + for sgn2 in left_to_map: + # symmetry must match. constraints mean gn2>gn iff sgn2>sgn + if (sgn, sgn2) in constraints: + gn2_cands = set(gn_ID_to_node[gn_node_to_ID[gn] + 1 :]) + elif (sgn2, sgn) in constraints: + gn2_cands = set(gn_ID_to_node[: gn_node_to_ID[gn]]) + else: + continue # pragma: no cover + # Do not change the original set. So do not use |= operator + cand_sets[sgn2] = cand_sets[sgn2] | {frozenset(gn2_cands)} + + # The next node is the one that is unmapped and has fewest candidates + # Use the heuristic of the min size of the frozensets rather than + # intersection of all frozensets to delay computing intersections. + new_sgn = min( + left_to_map, key=lambda n: min(len(x) for x in cand_sets[n]) + ) + new_sgn_candidates = frozenset.intersection(*cand_sets[new_sgn]) + if not new_sgn_candidates: + continue + cand_sets[new_sgn] = {new_sgn_candidates} + queue.append((new_sgn, cand_sets, iter(new_sgn_candidates))) + break + else: # all gn candidates tried for sgn. + queue.pop() + if sgn in mapping: + del rev_mapping[mapping[sgn]] + del mapping[sgn] + + def _largest_common_subgraph(self, candidates, constraints, to_be_mapped=None): + """ + Find all largest common subgraphs honoring constraints. + """ + # to_be_mapped is a set of frozensets of subgraph nodes + if to_be_mapped is None: + to_be_mapped = {frozenset(self.subgraph.nodes)} + + # The LCS problem is basically a repeated subgraph isomorphism problem + # with smaller and smaller subgraphs. We store the nodes that are + # "part of" the subgraph in to_be_mapped, and we make it a little + # smaller every iteration. + + current_size = len(next(iter(to_be_mapped), [])) + + found_iso = False + if current_size <= len(self.graph): + # There's no point in trying to find isomorphisms of + # graph >= subgraph if subgraph has more nodes than graph. + + # Try the isomorphism first with the nodes with lowest ID. So sort + # them. Those are more likely to be part of the final correspondence. + # In theory, this makes finding the first answer(s) faster. + for nodes in sorted(to_be_mapped, key=sorted): + # Find the isomorphism between subgraph[to_be_mapped] <= graph + next_sgn = min(nodes, key=lambda n: min(len(x) for x in candidates[n])) + isomorphs = self._map_nodes( + next_sgn, candidates, constraints, to_be_mapped=nodes + ) + + # This is effectively `yield from isomorphs`, except that we look + # whether an item was yielded. + try: + item = next(isomorphs) + except StopIteration: + pass + else: + yield item + yield from isomorphs + found_iso = True + + # BASECASE + if found_iso or current_size == 1: + # Shrinking has no point because either 1) we end up with a smaller + # common subgraph (and we want the largest), or 2) there'll be no + # more subgraph. + return + + left_to_be_mapped = set() + for nodes in to_be_mapped: + for sgn in nodes: + # We're going to remove sgn from to_be_mapped, but subject to + # symmetry constraints. We know that for every constraint we + # have those subgraph nodes are equal. So whenever we would + # remove the lower part of a constraint, remove the higher + # instead. This is all dealth with by _remove_node. And because + # left_to_be_mapped is a set, we don't do double work. + + # And finally, make the subgraph one node smaller. + # REDUCTION + new_nodes = self._remove_node(sgn, nodes, constraints) + left_to_be_mapped.add(new_nodes) + # COMBINATION + yield from self._largest_common_subgraph( + candidates, constraints, to_be_mapped=left_to_be_mapped + ) + + @staticmethod + def _remove_node(node, nodes, constraints): + """ + Returns a new set where node has been removed from nodes, subject to + symmetry constraints. We know, that for every constraint we have + those subgraph nodes are equal. So whenever we would remove the + lower part of a constraint, remove the higher instead. + """ + while True: + for low, high in constraints: + if low == node and high in nodes: + node = high + break + else: # no break, couldn't find node in constraints + return frozenset(nodes - {node}) + + @staticmethod + def _get_permutations_by_length(items): + """ + Get all permutations of items, but only permute items with the same + length. + + >>> found = list(ISMAGS._get_permutations_by_length([{1}, {2}, {3, 4}, {4, 5}])) + >>> answer = [ + ... (({1}, {2}), ({3, 4}, {4, 5})), + ... (({1}, {2}), ({4, 5}, {3, 4})), + ... (({2}, {1}), ({3, 4}, {4, 5})), + ... (({2}, {1}), ({4, 5}, {3, 4})), + ... ] + >>> found == answer + True + """ + by_len = defaultdict(list) + for item in items: + by_len[len(item)].append(item) + + return list( + itertools.product( + *(itertools.permutations(by_len[l]) for l in sorted(by_len)) + ) + ) + + def _refine_opp(cls, graph, top, bottom, edge_colors): + def equal_color(node1, node2): + return color_degree[node1] == color_degree[node2] + + top = cls._refine_node_partition(graph, top, edge_colors) + + possible_bottoms = [bottom] + while possible_bottoms: + bottom = possible_bottoms.pop() + node_colors = node_to_part_ID_dict(bottom) + color_degree = color_degree_by_node(graph, node_colors, edge_colors) + if all(are_all_equal(color_degree[n] for n in p) for p in bottom): + if len(top) == len(bottom): + yield top, bottom + # else Non-isomorphic OPP (pruned here) + # either way continue to next possible bottom + continue + # refine bottom partition + more_bottoms = [[]] + for part in bottom: + if len(part) == 1 or are_all_equal(color_degree[node] for node in part): + for new_bottom in more_bottoms: + new_bottom.append(part) + else: + # This part needs to be refined + refined_part = make_partition(part, equal_color, check=False) + R = len(refined_part) + if R == 1 or R == len({len(p) for p in refined_part}): + # no two parts have same length -- simple case + for n_p in more_bottoms: + n_p.extend(sorted(refined_part, key=len)) + else: + # Any part might match any other part with the same size. + # Before refinement they were the same color. So we need to + # include all possible orderings/colors within each size. + permutations = cls._get_permutations_by_length(refined_part) + # Add all permutations of the refined parts to each possible + # bottom. So the number of new possible bottoms is multiplied + # by the number of permutations of the refined parts. + new_partitions = [] + for new_partition in more_bottoms: + for p in permutations: + # p is tuple-of-tuples-of-sets. Flatten to list-of-sets + flat_p = [s for tup in p for s in tup] + new_partitions.append(new_partition + flat_p) + more_bottoms = new_partitions + + # reverse more_bottoms to keep the "finding identity" bottom first + possible_bottoms.extend(more_bottoms[::-1]) + + @staticmethod + def _find_permutations(top_partition, bottom_partition): + """ + Return a set of 2-tuples of nodes. These nodes are not equal + but are mapped to each other in the symmetry represented by this OPP. + Swapping all the 2-tuples of nodes in this set permutes the nodes + but retains the graph structure. Thus it is a symmetry of the subgraph. + """ + # Find permutations + permutations = set() + for top, bot in zip(top_partition, bottom_partition): + if len(top) > 1 or len(bot) > 1: + # ignore parts with > 1 element when they are equal + # These are called Matching OPPs in Katebi 2012. + # Symmetries in matching partitions are built by considering + # only parts that have 1 element. + if top == bot: + continue + raise IndexError( + "Not all nodes are matched. This is" + f" impossible: {top_partition}, {bottom_partition}" + ) + # top and bot have only one element + elif top != bot: + permutations.add(frozenset((next(iter(top)), next(iter(bot))))) + return permutations + + def _process_ordered_pair_partitions( + self, + graph, + top_partition, + bottom_partition, + edge_colors, + ): + if all(len(top) <= 1 for top in top_partition): + # no symmetries. Each node unique. + return {} + + # first mapping found is the identity mapping + finding_identity = True + + orbit_id = {node: orbit_i for orbit_i, node in enumerate(graph)} + orbits = [{node} for node in graph] + cosets = {} + + node_to_ID = {n: i for i, n in enumerate(graph)} + sort_by_ID = node_to_ID.__getitem__ + + def _load_next_queue_entry(queue, top_partition, bottom_partition): + # find smallest node (by ID) in a |part|>1 and its partition index + unmapped_nodes = ( + (node_to_ID[node], node, idx) + for idx, t_part in enumerate(top_partition) + for node in t_part + if len(t_part) > 1 + ) + _, node, part_i = min(unmapped_nodes) + b_part = bottom_partition[part_i] + node2_iter = iter(sorted(b_part, key=sort_by_ID)) + + queue.append([top_partition, bottom_partition, node, part_i, node2_iter]) + + queue = [] + _load_next_queue_entry(queue, top_partition, bottom_partition) + + while queue: + tops, bottoms, node, part_i, node2_iter = queue[-1] + + for node2 in node2_iter: + if node != node2 and orbit_id[node] == orbit_id[node2]: + # Orbit prune + continue + + # couple node to node2 + new_top_part = {node} + new_bot_part = {node2} + + new_top = [top.copy() for top in tops] + new_top[part_i] -= new_top_part + new_top.insert(part_i, new_top_part) + + new_bot = [bot.copy() for bot in bottoms] + new_bot[part_i] -= new_bot_part + new_bot.insert(part_i, new_bot_part) + + # collect OPPs + opps = self._refine_opp(graph, new_top, new_bot, edge_colors) + new_q = [] + for opp in opps: + # Use OPP to find any of: Identity, Automorphism or Matching OPPs + # else load the OPP onto queue for further exploration + # Note that we check for Orbit pruning later because orbits may + # be updated while OPP is sitting on the queue. + # Note that we check for Non-isomorphic OPPs in `_refine_opp`. + if finding_identity: + # Note: allow zero size parts in identity check + # b/c largest_common_subgraph allows empty parts + if all(len(top) <= 1 for top in opp[0]): + # Identity found. Set flag. Can now prune Matching OPPs + finding_identity = False + continue + elif all(len(t) <= 1 or t == b for t, b in zip(*opp)): + # Found a symmetry! (Full mapping or Matching OPP) + # update orbits using the permutations from the OPP. + permutations = self._find_permutations(*opp) + for n1, n2 in permutations: + orb1 = orbit_id[n1] + orb2 = orbit_id[n2] + if orb1 != orb2: + orbit_set2 = orbits[orb2] + orbits[orb1].update(orbit_set2) + orbits[orb2] = set() + orbit_id.update((n, orb1) for n in orbit_set2) + continue + + _load_next_queue_entry(new_q, *opp) + # reverse order to maintain node order DFS (Identity comes first) + queue.extend(new_q[::-1]) + break + else: # no more node2 options + queue.pop() + if node not in cosets: + # coset of `node` is its orbit at the time `node` has completed + # its first DFS traversal. DFS is about to go to previous node. + # Make copy so future orbit changes do not change the coset. + cosets[node] = orbits[orbit_id[node]].copy() + return cosets diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorph.py new file mode 100644 index 0000000000000000000000000000000000000000..f49594a603035abd5278124aa9638c5b5eb6e8c7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorph.py @@ -0,0 +1,336 @@ +""" +Graph isomorphism functions. +""" + +import itertools +from collections import Counter + +import networkx as nx +from networkx.exception import NetworkXError + +__all__ = [ + "could_be_isomorphic", + "fast_could_be_isomorphic", + "faster_could_be_isomorphic", + "is_isomorphic", +] + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}) +def could_be_isomorphic(G1, G2, *, properties="dtc"): + """Returns False if graphs are definitely not isomorphic. + True does NOT guarantee isomorphism. + + Parameters + ---------- + G1, G2 : graphs + The two graphs `G1` and `G2` must be the same type. + + properties : str, default="dct" + Determines which properties of the graph are checked. Each character + indicates a particular property as follows: + + - if ``"d"`` in `properties`: degree of each node + - if ``"t"`` in `properties`: number of triangles for each node + - if ``"c"`` in `properties`: number of maximal cliques for each node + + Unrecognized characters are ignored. The default is ``"dtc"``, which + compares the sequence of ``(degree, num_triangles, num_cliques)`` properties + between `G1` and `G2`. Generally, ``properties="dt"`` would be faster, and + ``properties="d"`` faster still. See Notes for additional details on + property selection. + + Returns + ------- + bool + A Boolean value representing whether `G1` could be isomorphic with `G2` + according to the specified `properties`. + + Notes + ----- + The triangle sequence contains the number of triangles each node is part of. + The clique sequence contains for each node the number of maximal cliques + involving that node. + + Some properties are faster to compute than others. And there are other + properties we could include and don't. But of the three properties listed here, + comparing the degree distributions is the fastest. The "triangles" property + is slower (and also a stricter version of "could") and the "maximal cliques" + property is slower still, but usually faster than doing a full isomorphism + check. + """ + + # Check global properties + if G1.order() != G2.order(): + return False + + properties_to_check = set(properties) + G1_props, G2_props = [], [] + + def _properties_consistent(): + # Ravel the properties into a table with # nodes rows and # properties columns + G1_ptable = [tuple(p[n] for p in G1_props) for n in G1] + G2_ptable = [tuple(p[n] for p in G2_props) for n in G2] + + return sorted(G1_ptable) == sorted(G2_ptable) + + # The property table is built and checked as each individual property is + # added. The reason for this is the building/checking the property table + # is in general much faster than computing the properties, making it + # worthwhile to check multiple times to enable early termination when + # a subset of properties don't match + + # Degree sequence + if "d" in properties_to_check: + G1_props.append(G1.degree()) + G2_props.append(G2.degree()) + if not _properties_consistent(): + return False + # Sequence of triangles per node + if "t" in properties_to_check: + G1_props.append(nx.triangles(G1)) + G2_props.append(nx.triangles(G2)) + if not _properties_consistent(): + return False + # Sequence of maximal cliques per node + if "c" in properties_to_check: + G1_props.append(Counter(itertools.chain.from_iterable(nx.find_cliques(G1)))) + G2_props.append(Counter(itertools.chain.from_iterable(nx.find_cliques(G2)))) + if not _properties_consistent(): + return False + + # All checked conditions passed + return True + + +def graph_could_be_isomorphic(G1, G2): + """ + .. deprecated:: 3.5 + + `graph_could_be_isomorphic` is a deprecated alias for `could_be_isomorphic`. + Use `could_be_isomorphic` instead. + """ + import warnings + + warnings.warn( + "graph_could_be_isomorphic is deprecated, use `could_be_isomorphic` instead.", + category=DeprecationWarning, + stacklevel=2, + ) + return could_be_isomorphic(G1, G2) + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}) +def fast_could_be_isomorphic(G1, G2): + """Returns False if graphs are definitely not isomorphic. + + True does NOT guarantee isomorphism. + + Parameters + ---------- + G1, G2 : graphs + The two graphs G1 and G2 must be the same type. + + Notes + ----- + Checks for matching degree and triangle sequences. The triangle + sequence contains the number of triangles each node is part of. + """ + # Check global properties + if G1.order() != G2.order(): + return False + + # Check local properties + d1 = G1.degree() + t1 = nx.triangles(G1) + props1 = [[d, t1[v]] for v, d in d1] + props1.sort() + + d2 = G2.degree() + t2 = nx.triangles(G2) + props2 = [[d, t2[v]] for v, d in d2] + props2.sort() + + if props1 != props2: + return False + + # OK... + return True + + +def fast_graph_could_be_isomorphic(G1, G2): + """ + .. deprecated:: 3.5 + + `fast_graph_could_be_isomorphic` is a deprecated alias for + `fast_could_be_isomorphic`. Use `fast_could_be_isomorphic` instead. + """ + import warnings + + warnings.warn( + "fast_graph_could_be_isomorphic is deprecated, use fast_could_be_isomorphic instead", + category=DeprecationWarning, + stacklevel=2, + ) + return fast_could_be_isomorphic(G1, G2) + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}) +def faster_could_be_isomorphic(G1, G2): + """Returns False if graphs are definitely not isomorphic. + + True does NOT guarantee isomorphism. + + Parameters + ---------- + G1, G2 : graphs + The two graphs G1 and G2 must be the same type. + + Notes + ----- + Checks for matching degree sequences. + """ + # Check global properties + if G1.order() != G2.order(): + return False + + # Check local properties + d1 = sorted(d for n, d in G1.degree()) + d2 = sorted(d for n, d in G2.degree()) + + if d1 != d2: + return False + + # OK... + return True + + +def faster_graph_could_be_isomorphic(G1, G2): + """ + .. deprecated:: 3.5 + + `faster_graph_could_be_isomorphic` is a deprecated alias for + `faster_could_be_isomorphic`. Use `faster_could_be_isomorphic` instead. + """ + import warnings + + warnings.warn( + "faster_graph_could_be_isomorphic is deprecated, use faster_could_be_isomorphic instead", + category=DeprecationWarning, + stacklevel=2, + ) + return faster_could_be_isomorphic(G1, G2) + + +@nx._dispatchable( + graphs={"G1": 0, "G2": 1}, + preserve_edge_attrs="edge_match", + preserve_node_attrs="node_match", +) +def is_isomorphic(G1, G2, node_match=None, edge_match=None): + """Returns True if the graphs G1 and G2 are isomorphic and False otherwise. + + Parameters + ---------- + G1, G2: graphs + The two graphs G1 and G2 must be the same type. + + node_match : callable + A function that returns True if node n1 in G1 and n2 in G2 should + be considered equal during the isomorphism test. + If node_match is not specified then node attributes are not considered. + + The function will be called like + + node_match(G1.nodes[n1], G2.nodes[n2]). + + That is, the function will receive the node attribute dictionaries + for n1 and n2 as inputs. + + edge_match : callable + A function that returns True if the edge attribute dictionary + for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should + be considered equal during the isomorphism test. If edge_match is + not specified then edge attributes are not considered. + + The function will be called like + + edge_match(G1[u1][v1], G2[u2][v2]). + + That is, the function will receive the edge attribute dictionaries + of the edges under consideration. + + Notes + ----- + Uses the vf2 algorithm [1]_. + + Examples + -------- + >>> import networkx.algorithms.isomorphism as iso + + For digraphs G1 and G2, using 'weight' edge attribute (default: 1) + + >>> G1 = nx.DiGraph() + >>> G2 = nx.DiGraph() + >>> nx.add_path(G1, [1, 2, 3, 4], weight=1) + >>> nx.add_path(G2, [10, 20, 30, 40], weight=2) + >>> em = iso.numerical_edge_match("weight", 1) + >>> nx.is_isomorphic(G1, G2) # no weights considered + True + >>> nx.is_isomorphic(G1, G2, edge_match=em) # match weights + False + + For multidigraphs G1 and G2, using 'fill' node attribute (default: '') + + >>> G1 = nx.MultiDiGraph() + >>> G2 = nx.MultiDiGraph() + >>> G1.add_nodes_from([1, 2, 3], fill="red") + >>> G2.add_nodes_from([10, 20, 30, 40], fill="red") + >>> nx.add_path(G1, [1, 2, 3, 4], weight=3, linewidth=2.5) + >>> nx.add_path(G2, [10, 20, 30, 40], weight=3) + >>> nm = iso.categorical_node_match("fill", "red") + >>> nx.is_isomorphic(G1, G2, node_match=nm) + True + + For multidigraphs G1 and G2, using 'weight' edge attribute (default: 7) + + >>> G1.add_edge(1, 2, weight=7) + 1 + >>> G2.add_edge(10, 20) + 1 + >>> em = iso.numerical_multiedge_match("weight", 7, rtol=1e-6) + >>> nx.is_isomorphic(G1, G2, edge_match=em) + True + + For multigraphs G1 and G2, using 'weight' and 'linewidth' edge attributes + with default values 7 and 2.5. Also using 'fill' node attribute with + default value 'red'. + + >>> em = iso.numerical_multiedge_match(["weight", "linewidth"], [7, 2.5]) + >>> nm = iso.categorical_node_match("fill", "red") + >>> nx.is_isomorphic(G1, G2, edge_match=em, node_match=nm) + True + + See Also + -------- + numerical_node_match, numerical_edge_match, numerical_multiedge_match + categorical_node_match, categorical_edge_match, categorical_multiedge_match + + References + ---------- + .. [1] L. P. Cordella, P. Foggia, C. Sansone, M. Vento, + "An Improved Algorithm for Matching Large Graphs", + 3rd IAPR-TC15 Workshop on Graph-based Representations in + Pattern Recognition, Cuen, pp. 149-159, 2001. + https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs + """ + if G1.is_directed() and G2.is_directed(): + GM = nx.algorithms.isomorphism.DiGraphMatcher + elif (not G1.is_directed()) and (not G2.is_directed()): + GM = nx.algorithms.isomorphism.GraphMatcher + else: + raise NetworkXError("Graphs G1 and G2 are not of the same type.") + + gm = GM(G1, G2, node_match=node_match, edge_match=edge_match) + + return gm.is_isomorphic() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py new file mode 100644 index 0000000000000000000000000000000000000000..587503a9dee1479947db31af6912ff01f0dcf037 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py @@ -0,0 +1,1262 @@ +""" +************* +VF2 Algorithm +************* + +An implementation of VF2 algorithm for graph isomorphism testing. + +The simplest interface to use this module is to call the +:func:`is_isomorphic ` +function. + +Introduction +------------ + +The GraphMatcher and DiGraphMatcher are responsible for matching +graphs or directed graphs in a predetermined manner. This +usually means a check for an isomorphism, though other checks +are also possible. For example, a subgraph of one graph +can be checked for isomorphism to a second graph. + +Matching is done via syntactic feasibility. It is also possible +to check for semantic feasibility. Feasibility, then, is defined +as the logical AND of the two functions. + +To include a semantic check, the (Di)GraphMatcher class should be +subclassed, and the +:meth:`semantic_feasibility ` +function should be redefined. By default, the semantic feasibility function always +returns ``True``. The effect of this is that semantics are not +considered in the matching of G1 and G2. + +Examples +-------- + +Suppose G1 and G2 are isomorphic graphs. Verification is as follows: + +>>> from networkx.algorithms import isomorphism +>>> G1 = nx.path_graph(4) +>>> G2 = nx.path_graph(4) +>>> GM = isomorphism.GraphMatcher(G1, G2) +>>> GM.is_isomorphic() +True + +GM.mapping stores the isomorphism mapping from G1 to G2. + +>>> GM.mapping +{0: 0, 1: 1, 2: 2, 3: 3} + + +Suppose G1 and G2 are isomorphic directed graphs. +Verification is as follows: + +>>> G1 = nx.path_graph(4, create_using=nx.DiGraph) +>>> G2 = nx.path_graph(4, create_using=nx.DiGraph) +>>> DiGM = isomorphism.DiGraphMatcher(G1, G2) +>>> DiGM.is_isomorphic() +True + +DiGM.mapping stores the isomorphism mapping from G1 to G2. + +>>> DiGM.mapping +{0: 0, 1: 1, 2: 2, 3: 3} + + + +Subgraph Isomorphism +-------------------- +Graph theory literature can be ambiguous about the meaning of the +above statement, and we seek to clarify it now. + +In the VF2 literature, a mapping ``M`` is said to be a graph-subgraph +isomorphism iff ``M`` is an isomorphism between ``G2`` and a subgraph of ``G1``. +Thus, to say that ``G1`` and ``G2`` are graph-subgraph isomorphic is to say +that a subgraph of ``G1`` is isomorphic to ``G2``. + +Other literature uses the phrase 'subgraph isomorphic' as in '``G1`` does +not have a subgraph isomorphic to ``G2``'. Another use is as an in adverb +for isomorphic. Thus, to say that ``G1`` and ``G2`` are subgraph isomorphic +is to say that a subgraph of ``G1`` is isomorphic to ``G2``. + +Finally, the term 'subgraph' can have multiple meanings. In this +context, 'subgraph' always means a 'node-induced subgraph'. Edge-induced +subgraph isomorphisms are not directly supported, but one should be +able to perform the check by making use of +:func:`line_graph `. For +subgraphs which are not induced, the term 'monomorphism' is preferred +over 'isomorphism'. + +Let ``G = (N, E)`` be a graph with a set of nodes ``N`` and set of edges ``E``. + +If ``G' = (N', E')`` is a subgraph, then: + ``N'`` is a subset of ``N`` and + ``E'`` is a subset of ``E``. + +If ``G' = (N', E')`` is a node-induced subgraph, then: + ``N'`` is a subset of ``N`` and + ``E'`` is the subset of edges in ``E`` relating nodes in ``N'``. + +If ``G' = (N', E')`` is an edge-induced subgraph, then: + ``N'`` is the subset of nodes in ``N`` related by edges in ``E'`` and + ``E'`` is a subset of ``E``. + +If ``G' = (N', E')`` is a monomorphism, then: + ``N'`` is a subset of ``N`` and + ``E'`` is a subset of the set of edges in ``E`` relating nodes in ``N'``. + +Note that if ``G'`` is a node-induced subgraph of ``G``, then it is always a +subgraph monomorphism of ``G``, but the opposite is not always true, as a +monomorphism can have fewer edges. + +References +---------- +[1] Luigi P. Cordella, Pasquale Foggia, Carlo Sansone, Mario Vento, + "A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs", + IEEE Transactions on Pattern Analysis and Machine Intelligence, + vol. 26, no. 10, pp. 1367-1372, Oct., 2004. + http://ieeexplore.ieee.org/iel5/34/29305/01323804.pdf + +[2] L. P. Cordella, P. Foggia, C. Sansone, M. Vento, "An Improved + Algorithm for Matching Large Graphs", 3rd IAPR-TC15 Workshop + on Graph-based Representations in Pattern Recognition, Cuen, + pp. 149-159, 2001. + https://www.researchgate.net/publication/200034365_An_Improved_Algorithm_for_Matching_Large_Graphs + +See Also +-------- +:meth:`semantic_feasibility ` +:meth:`syntactic_feasibility ` + +Notes +----- + +The implementation handles both directed and undirected graphs as well +as multigraphs. + +In general, the subgraph isomorphism problem is NP-complete whereas the +graph isomorphism problem is most likely not NP-complete (although no +polynomial-time algorithm is known to exist). + +""" + +# This work was originally coded by Christopher Ellison +# as part of the Computational Mechanics Python (CMPy) project. +# James P. Crutchfield, principal investigator. +# Complexity Sciences Center and Physics Department, UC Davis. + +import sys + +import networkx as nx + +__all__ = ["GraphMatcher", "DiGraphMatcher"] + + +class GraphMatcher: + """Implementation of VF2 algorithm for matching undirected graphs. + + Suitable for Graph and MultiGraph instances. + """ + + def __init__(self, G1, G2): + """Initialize GraphMatcher. + + Parameters + ---------- + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism. + + Examples + -------- + To create a GraphMatcher which checks for syntactic feasibility: + + >>> from networkx.algorithms import isomorphism + >>> G1 = nx.path_graph(4) + >>> G2 = nx.path_graph(4) + >>> GM = isomorphism.GraphMatcher(G1, G2) + """ + if G1.is_directed() != G2.is_directed(): + raise nx.NetworkXError("G1 and G2 must have the same directedness") + + is_directed_matcher = self._is_directed_matcher() + if not is_directed_matcher and (G1.is_directed() or G2.is_directed()): + raise nx.NetworkXError( + "(Multi-)GraphMatcher() not defined for directed graphs. " + "Use (Multi-)DiGraphMatcher() instead." + ) + + if is_directed_matcher and not (G1.is_directed() and G2.is_directed()): + raise nx.NetworkXError( + "(Multi-)DiGraphMatcher() not defined for undirected graphs. " + "Use (Multi-)GraphMatcher() instead." + ) + + self.G1 = G1 + self.G2 = G2 + self.G1_nodes = set(G1.nodes()) + self.G2_nodes = set(G2.nodes()) + self.G2_node_order = {n: i for i, n in enumerate(G2)} + + # Set recursion limit. + self.old_recursion_limit = sys.getrecursionlimit() + expected_max_recursion_level = len(self.G2) + if self.old_recursion_limit < 1.5 * expected_max_recursion_level: + # Give some breathing room. + sys.setrecursionlimit(int(1.5 * expected_max_recursion_level)) + + # Declare that we will be searching for a graph-graph isomorphism. + self.test = "graph" + + # Initialize state + self.initialize() + + def _is_directed_matcher(self): + return False + + def reset_recursion_limit(self): + """Restores the recursion limit.""" + # TODO: + # Currently, we use recursion and set the recursion level higher. + # It would be nice to restore the level, but because the + # (Di)GraphMatcher classes make use of cyclic references, garbage + # collection will never happen when we define __del__() to + # restore the recursion level. The result is a memory leak. + # So for now, we do not automatically restore the recursion level, + # and instead provide a method to do this manually. Eventually, + # we should turn this into a non-recursive implementation. + sys.setrecursionlimit(self.old_recursion_limit) + + def candidate_pairs_iter(self): + """Iterator over candidate pairs of nodes in G1 and G2.""" + + # All computations are done using the current state! + + G1_nodes = self.G1_nodes + G2_nodes = self.G2_nodes + min_key = self.G2_node_order.__getitem__ + + # First we compute the inout-terminal sets. + T1_inout = [node for node in self.inout_1 if node not in self.core_1] + T2_inout = [node for node in self.inout_2 if node not in self.core_2] + + # If T1_inout and T2_inout are both nonempty. + # P(s) = T1_inout x {min T2_inout} + if T1_inout and T2_inout: + node_2 = min(T2_inout, key=min_key) + for node_1 in T1_inout: + yield node_1, node_2 + + else: + # If T1_inout and T2_inout were both empty.... + # P(s) = (N_1 - M_1) x {min (N_2 - M_2)} + # if not (T1_inout or T2_inout): # as suggested by [2], incorrect + if 1: # as inferred from [1], correct + # First we determine the candidate node for G2 + other_node = min(G2_nodes - set(self.core_2), key=min_key) + for node in self.G1: + if node not in self.core_1: + yield node, other_node + + # For all other cases, we don't have any candidate pairs. + + def initialize(self): + """Reinitializes the state of the algorithm. + + This method should be redefined if using something other than GMState. + If only subclassing GraphMatcher, a redefinition is not necessary. + + """ + + # core_1[n] contains the index of the node paired with n, which is m, + # provided n is in the mapping. + # core_2[m] contains the index of the node paired with m, which is n, + # provided m is in the mapping. + self.core_1 = {} + self.core_2 = {} + + # See the paper for definitions of M_x and T_x^{y} + + # inout_1[n] is non-zero if n is in M_1 or in T_1^{inout} + # inout_2[m] is non-zero if m is in M_2 or in T_2^{inout} + # + # The value stored is the depth of the SSR tree when the node became + # part of the corresponding set. + self.inout_1 = {} + self.inout_2 = {} + # Practically, these sets simply store the nodes in the subgraph. + + self.state = GMState(self) + + # Provide a convenient way to access the isomorphism mapping. + self.mapping = self.core_1.copy() + + def is_isomorphic(self): + """Returns True if G1 and G2 are isomorphic graphs.""" + + # Let's do two very quick checks! + # QUESTION: Should we call faster_graph_could_be_isomorphic(G1,G2)? + # For now, I just copy the code. + + # Check global properties + if self.G1.order() != self.G2.order(): + return False + + # Check local properties + d1 = sorted(d for n, d in self.G1.degree()) + d2 = sorted(d for n, d in self.G2.degree()) + if d1 != d2: + return False + + try: + x = next(self.isomorphisms_iter()) + return True + except StopIteration: + return False + + def isomorphisms_iter(self): + """Generator over isomorphisms between G1 and G2.""" + # Declare that we are looking for a graph-graph isomorphism. + self.test = "graph" + self.initialize() + yield from self.match() + + def match(self): + """Extends the isomorphism mapping. + + This function is called recursively to determine if a complete + isomorphism can be found between G1 and G2. It cleans up the class + variables after each recursive call. If an isomorphism is found, + we yield the mapping. + + """ + if len(self.core_1) == len(self.G2): + # Save the final mapping, otherwise garbage collection deletes it. + self.mapping = self.core_1.copy() + # The mapping is complete. + yield self.mapping + else: + for G1_node, G2_node in self.candidate_pairs_iter(): + if self.syntactic_feasibility(G1_node, G2_node): + if self.semantic_feasibility(G1_node, G2_node): + # Recursive call, adding the feasible state. + newstate = self.state.__class__(self, G1_node, G2_node) + yield from self.match() + + # restore data structures + newstate.restore() + + def semantic_feasibility(self, G1_node, G2_node): + """Returns True if adding (G1_node, G2_node) is semantically feasible. + + The semantic feasibility function should return True if it is + acceptable to add the candidate pair (G1_node, G2_node) to the current + partial isomorphism mapping. The logic should focus on semantic + information contained in the edge data or a formalized node class. + + By acceptable, we mean that the subsequent mapping can still become a + complete isomorphism mapping. Thus, if adding the candidate pair + definitely makes it so that the subsequent mapping cannot become a + complete isomorphism mapping, then this function must return False. + + The default semantic feasibility function always returns True. The + effect is that semantics are not considered in the matching of G1 + and G2. + + The semantic checks might differ based on the what type of test is + being performed. A keyword description of the test is stored in + self.test. Here is a quick description of the currently implemented + tests:: + + test='graph' + Indicates that the graph matcher is looking for a graph-graph + isomorphism. + + test='subgraph' + Indicates that the graph matcher is looking for a subgraph-graph + isomorphism such that a subgraph of G1 is isomorphic to G2. + + test='mono' + Indicates that the graph matcher is looking for a subgraph-graph + monomorphism such that a subgraph of G1 is monomorphic to G2. + + Any subclass which redefines semantic_feasibility() must maintain + the above form to keep the match() method functional. Implementations + should consider multigraphs. + """ + return True + + def subgraph_is_isomorphic(self): + """Returns `True` if a subgraph of ``G1`` is isomorphic to ``G2``. + + Examples + -------- + When creating the `GraphMatcher`, the order of the arguments is important + + >>> G = nx.Graph([("A", "B"), ("B", "C"), ("A", "C")]) + >>> H = nx.Graph([(0, 1), (1, 2), (0, 2), (1, 3), (0, 4)]) + + Check whether a subgraph of G is isomorphic to H: + + >>> isomatcher = nx.isomorphism.GraphMatcher(G, H) + >>> isomatcher.subgraph_is_isomorphic() + False + + Check whether a subgraph of H is isomorphic to G: + + >>> isomatcher = nx.isomorphism.GraphMatcher(H, G) + >>> isomatcher.subgraph_is_isomorphic() + True + """ + try: + x = next(self.subgraph_isomorphisms_iter()) + return True + except StopIteration: + return False + + def subgraph_is_monomorphic(self): + """Returns `True` if a subgraph of ``G1`` is monomorphic to ``G2``. + + Examples + -------- + When creating the `GraphMatcher`, the order of the arguments is important. + + >>> G = nx.Graph([("A", "B"), ("B", "C")]) + >>> H = nx.Graph([(0, 1), (1, 2), (0, 2)]) + + Check whether a subgraph of G is monomorphic to H: + + >>> isomatcher = nx.isomorphism.GraphMatcher(G, H) + >>> isomatcher.subgraph_is_monomorphic() + False + + Check whether a subgraph of H is monomorphic to G: + + >>> isomatcher = nx.isomorphism.GraphMatcher(H, G) + >>> isomatcher.subgraph_is_monomorphic() + True + """ + try: + x = next(self.subgraph_monomorphisms_iter()) + return True + except StopIteration: + return False + + def subgraph_isomorphisms_iter(self): + """Generator over isomorphisms between a subgraph of ``G1`` and ``G2``. + + Examples + -------- + When creating the `GraphMatcher`, the order of the arguments is important + + >>> G = nx.Graph([("A", "B"), ("B", "C"), ("A", "C")]) + >>> H = nx.Graph([(0, 1), (1, 2), (0, 2), (1, 3), (0, 4)]) + + Yield isomorphic mappings between ``H`` and subgraphs of ``G``: + + >>> isomatcher = nx.isomorphism.GraphMatcher(G, H) + >>> list(isomatcher.subgraph_isomorphisms_iter()) + [] + + Yield isomorphic mappings between ``G`` and subgraphs of ``H``: + + >>> isomatcher = nx.isomorphism.GraphMatcher(H, G) + >>> next(isomatcher.subgraph_isomorphisms_iter()) + {0: 'A', 1: 'B', 2: 'C'} + + """ + # Declare that we are looking for graph-subgraph isomorphism. + self.test = "subgraph" + self.initialize() + yield from self.match() + + def subgraph_monomorphisms_iter(self): + """Generator over monomorphisms between a subgraph of ``G1`` and ``G2``. + + Examples + -------- + When creating the `GraphMatcher`, the order of the arguments is important. + + >>> G = nx.Graph([("A", "B"), ("B", "C")]) + >>> H = nx.Graph([(0, 1), (1, 2), (0, 2)]) + + Yield monomorphic mappings between ``H`` and subgraphs of ``G``: + + >>> isomatcher = nx.isomorphism.GraphMatcher(G, H) + >>> list(isomatcher.subgraph_monomorphisms_iter()) + [] + + Yield monomorphic mappings between ``G`` and subgraphs of ``H``: + + >>> isomatcher = nx.isomorphism.GraphMatcher(H, G) + >>> next(isomatcher.subgraph_monomorphisms_iter()) + {0: 'A', 1: 'B', 2: 'C'} + """ + # Declare that we are looking for graph-subgraph monomorphism. + self.test = "mono" + self.initialize() + yield from self.match() + + def syntactic_feasibility(self, G1_node, G2_node): + """Returns True if adding (G1_node, G2_node) is syntactically feasible. + + This function returns True if it is adding the candidate pair + to the current partial isomorphism/monomorphism mapping is allowable. + The addition is allowable if the inclusion of the candidate pair does + not make it impossible for an isomorphism/monomorphism to be found. + """ + + # The VF2 algorithm was designed to work with graphs having, at most, + # one edge connecting any two nodes. This is not the case when + # dealing with an MultiGraphs. + # + # Basically, when we test the look-ahead rules R_neighbor, we will + # make sure that the number of edges are checked. We also add + # a R_self check to verify that the number of selfloops is acceptable. + # + # Users might be comparing Graph instances with MultiGraph instances. + # So the generic GraphMatcher class must work with MultiGraphs. + # Care must be taken since the value in the innermost dictionary is a + # singlet for Graph instances. For MultiGraphs, the value in the + # innermost dictionary is a list. + + ### + # Test at each step to get a return value as soon as possible. + ### + + # Look ahead 0 + + # R_self + + # The number of selfloops for G1_node must equal the number of + # self-loops for G2_node. Without this check, we would fail on + # R_neighbor at the next recursion level. But it is good to prune the + # search tree now. + + if self.test == "mono": + if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges( + G2_node, G2_node + ): + return False + else: + if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges( + G2_node, G2_node + ): + return False + + # R_neighbor + + # For each neighbor n' of n in the partial mapping, the corresponding + # node m' is a neighbor of m, and vice versa. Also, the number of + # edges must be equal. + if self.test != "mono": + for neighbor in self.G1[G1_node]: + if neighbor in self.core_1: + if self.core_1[neighbor] not in self.G2[G2_node]: + return False + elif self.G1.number_of_edges( + neighbor, G1_node + ) != self.G2.number_of_edges(self.core_1[neighbor], G2_node): + return False + + for neighbor in self.G2[G2_node]: + if neighbor in self.core_2: + if self.core_2[neighbor] not in self.G1[G1_node]: + return False + elif self.test == "mono": + if self.G1.number_of_edges( + self.core_2[neighbor], G1_node + ) < self.G2.number_of_edges(neighbor, G2_node): + return False + else: + if self.G1.number_of_edges( + self.core_2[neighbor], G1_node + ) != self.G2.number_of_edges(neighbor, G2_node): + return False + + if self.test != "mono": + # Look ahead 1 + + # R_terminout + # The number of neighbors of n in T_1^{inout} is equal to the + # number of neighbors of m that are in T_2^{inout}, and vice versa. + num1 = 0 + for neighbor in self.G1[G1_node]: + if (neighbor in self.inout_1) and (neighbor not in self.core_1): + num1 += 1 + num2 = 0 + for neighbor in self.G2[G2_node]: + if (neighbor in self.inout_2) and (neighbor not in self.core_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # Look ahead 2 + + # R_new + + # The number of neighbors of n that are neither in the core_1 nor + # T_1^{inout} is equal to the number of neighbors of m + # that are neither in core_2 nor T_2^{inout}. + num1 = 0 + for neighbor in self.G1[G1_node]: + if neighbor not in self.inout_1: + num1 += 1 + num2 = 0 + for neighbor in self.G2[G2_node]: + if neighbor not in self.inout_2: + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # Otherwise, this node pair is syntactically feasible! + return True + + +class DiGraphMatcher(GraphMatcher): + """Implementation of VF2 algorithm for matching directed graphs. + + Suitable for DiGraph and MultiDiGraph instances. + """ + + def __init__(self, G1, G2): + """Initialize DiGraphMatcher. + + G1 and G2 should be nx.Graph or nx.MultiGraph instances. + + Examples + -------- + To create a GraphMatcher which checks for syntactic feasibility: + + >>> from networkx.algorithms import isomorphism + >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) + >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) + >>> DiGM = isomorphism.DiGraphMatcher(G1, G2) + """ + super().__init__(G1, G2) + + def _is_directed_matcher(self): + return True + + def candidate_pairs_iter(self): + """Iterator over candidate pairs of nodes in G1 and G2.""" + + # All computations are done using the current state! + + G1_nodes = self.G1_nodes + G2_nodes = self.G2_nodes + min_key = self.G2_node_order.__getitem__ + + # First we compute the out-terminal sets. + T1_out = [node for node in self.out_1 if node not in self.core_1] + T2_out = [node for node in self.out_2 if node not in self.core_2] + + # If T1_out and T2_out are both nonempty. + # P(s) = T1_out x {min T2_out} + if T1_out and T2_out: + node_2 = min(T2_out, key=min_key) + for node_1 in T1_out: + yield node_1, node_2 + + # If T1_out and T2_out were both empty.... + # We compute the in-terminal sets. + + # elif not (T1_out or T2_out): # as suggested by [2], incorrect + else: # as suggested by [1], correct + T1_in = [node for node in self.in_1 if node not in self.core_1] + T2_in = [node for node in self.in_2 if node not in self.core_2] + + # If T1_in and T2_in are both nonempty. + # P(s) = T1_out x {min T2_out} + if T1_in and T2_in: + node_2 = min(T2_in, key=min_key) + for node_1 in T1_in: + yield node_1, node_2 + + # If all terminal sets are empty... + # P(s) = (N_1 - M_1) x {min (N_2 - M_2)} + + # elif not (T1_in or T2_in): # as suggested by [2], incorrect + else: # as inferred from [1], correct + node_2 = min(G2_nodes - set(self.core_2), key=min_key) + for node_1 in G1_nodes: + if node_1 not in self.core_1: + yield node_1, node_2 + + # For all other cases, we don't have any candidate pairs. + + def initialize(self): + """Reinitializes the state of the algorithm. + + This method should be redefined if using something other than DiGMState. + If only subclassing GraphMatcher, a redefinition is not necessary. + """ + + # core_1[n] contains the index of the node paired with n, which is m, + # provided n is in the mapping. + # core_2[m] contains the index of the node paired with m, which is n, + # provided m is in the mapping. + self.core_1 = {} + self.core_2 = {} + + # See the paper for definitions of M_x and T_x^{y} + + # in_1[n] is non-zero if n is in M_1 or in T_1^{in} + # out_1[n] is non-zero if n is in M_1 or in T_1^{out} + # + # in_2[m] is non-zero if m is in M_2 or in T_2^{in} + # out_2[m] is non-zero if m is in M_2 or in T_2^{out} + # + # The value stored is the depth of the search tree when the node became + # part of the corresponding set. + self.in_1 = {} + self.in_2 = {} + self.out_1 = {} + self.out_2 = {} + + self.state = DiGMState(self) + + # Provide a convenient way to access the isomorphism mapping. + self.mapping = self.core_1.copy() + + def syntactic_feasibility(self, G1_node, G2_node): + """Returns True if adding (G1_node, G2_node) is syntactically feasible. + + This function returns True if it is adding the candidate pair + to the current partial isomorphism/monomorphism mapping is allowable. + The addition is allowable if the inclusion of the candidate pair does + not make it impossible for an isomorphism/monomorphism to be found. + """ + + # The VF2 algorithm was designed to work with graphs having, at most, + # one edge connecting any two nodes. This is not the case when + # dealing with an MultiGraphs. + # + # Basically, when we test the look-ahead rules R_pred and R_succ, we + # will make sure that the number of edges are checked. We also add + # a R_self check to verify that the number of selfloops is acceptable. + + # Users might be comparing DiGraph instances with MultiDiGraph + # instances. So the generic DiGraphMatcher class must work with + # MultiDiGraphs. Care must be taken since the value in the innermost + # dictionary is a singlet for DiGraph instances. For MultiDiGraphs, + # the value in the innermost dictionary is a list. + + ### + # Test at each step to get a return value as soon as possible. + ### + + # Look ahead 0 + + # R_self + + # The number of selfloops for G1_node must equal the number of + # self-loops for G2_node. Without this check, we would fail on R_pred + # at the next recursion level. This should prune the tree even further. + if self.test == "mono": + if self.G1.number_of_edges(G1_node, G1_node) < self.G2.number_of_edges( + G2_node, G2_node + ): + return False + else: + if self.G1.number_of_edges(G1_node, G1_node) != self.G2.number_of_edges( + G2_node, G2_node + ): + return False + + # R_pred + + # For each predecessor n' of n in the partial mapping, the + # corresponding node m' is a predecessor of m, and vice versa. Also, + # the number of edges must be equal + if self.test != "mono": + for predecessor in self.G1.pred[G1_node]: + if predecessor in self.core_1: + if self.core_1[predecessor] not in self.G2.pred[G2_node]: + return False + elif self.G1.number_of_edges( + predecessor, G1_node + ) != self.G2.number_of_edges(self.core_1[predecessor], G2_node): + return False + + for predecessor in self.G2.pred[G2_node]: + if predecessor in self.core_2: + if self.core_2[predecessor] not in self.G1.pred[G1_node]: + return False + elif self.test == "mono": + if self.G1.number_of_edges( + self.core_2[predecessor], G1_node + ) < self.G2.number_of_edges(predecessor, G2_node): + return False + else: + if self.G1.number_of_edges( + self.core_2[predecessor], G1_node + ) != self.G2.number_of_edges(predecessor, G2_node): + return False + + # R_succ + + # For each successor n' of n in the partial mapping, the corresponding + # node m' is a successor of m, and vice versa. Also, the number of + # edges must be equal. + if self.test != "mono": + for successor in self.G1[G1_node]: + if successor in self.core_1: + if self.core_1[successor] not in self.G2[G2_node]: + return False + elif self.G1.number_of_edges( + G1_node, successor + ) != self.G2.number_of_edges(G2_node, self.core_1[successor]): + return False + + for successor in self.G2[G2_node]: + if successor in self.core_2: + if self.core_2[successor] not in self.G1[G1_node]: + return False + elif self.test == "mono": + if self.G1.number_of_edges( + G1_node, self.core_2[successor] + ) < self.G2.number_of_edges(G2_node, successor): + return False + else: + if self.G1.number_of_edges( + G1_node, self.core_2[successor] + ) != self.G2.number_of_edges(G2_node, successor): + return False + + if self.test != "mono": + # Look ahead 1 + + # R_termin + # The number of predecessors of n that are in T_1^{in} is equal to the + # number of predecessors of m that are in T_2^{in}. + num1 = 0 + for predecessor in self.G1.pred[G1_node]: + if (predecessor in self.in_1) and (predecessor not in self.core_1): + num1 += 1 + num2 = 0 + for predecessor in self.G2.pred[G2_node]: + if (predecessor in self.in_2) and (predecessor not in self.core_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # The number of successors of n that are in T_1^{in} is equal to the + # number of successors of m that are in T_2^{in}. + num1 = 0 + for successor in self.G1[G1_node]: + if (successor in self.in_1) and (successor not in self.core_1): + num1 += 1 + num2 = 0 + for successor in self.G2[G2_node]: + if (successor in self.in_2) and (successor not in self.core_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # R_termout + + # The number of predecessors of n that are in T_1^{out} is equal to the + # number of predecessors of m that are in T_2^{out}. + num1 = 0 + for predecessor in self.G1.pred[G1_node]: + if (predecessor in self.out_1) and (predecessor not in self.core_1): + num1 += 1 + num2 = 0 + for predecessor in self.G2.pred[G2_node]: + if (predecessor in self.out_2) and (predecessor not in self.core_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # The number of successors of n that are in T_1^{out} is equal to the + # number of successors of m that are in T_2^{out}. + num1 = 0 + for successor in self.G1[G1_node]: + if (successor in self.out_1) and (successor not in self.core_1): + num1 += 1 + num2 = 0 + for successor in self.G2[G2_node]: + if (successor in self.out_2) and (successor not in self.core_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # Look ahead 2 + + # R_new + + # The number of predecessors of n that are neither in the core_1 nor + # T_1^{in} nor T_1^{out} is equal to the number of predecessors of m + # that are neither in core_2 nor T_2^{in} nor T_2^{out}. + num1 = 0 + for predecessor in self.G1.pred[G1_node]: + if (predecessor not in self.in_1) and (predecessor not in self.out_1): + num1 += 1 + num2 = 0 + for predecessor in self.G2.pred[G2_node]: + if (predecessor not in self.in_2) and (predecessor not in self.out_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # The number of successors of n that are neither in the core_1 nor + # T_1^{in} nor T_1^{out} is equal to the number of successors of m + # that are neither in core_2 nor T_2^{in} nor T_2^{out}. + num1 = 0 + for successor in self.G1[G1_node]: + if (successor not in self.in_1) and (successor not in self.out_1): + num1 += 1 + num2 = 0 + for successor in self.G2[G2_node]: + if (successor not in self.in_2) and (successor not in self.out_2): + num2 += 1 + if self.test == "graph": + if num1 != num2: + return False + else: # self.test == 'subgraph' + if not (num1 >= num2): + return False + + # Otherwise, this node pair is syntactically feasible! + return True + + def subgraph_is_isomorphic(self): + """Returns `True` if a subgraph of ``G1`` is isomorphic to ``G2``. + + Examples + -------- + When creating the `DiGraphMatcher`, the order of the arguments is important + + >>> G = nx.DiGraph([("A", "B"), ("B", "A"), ("B", "C"), ("C", "B")]) + >>> H = nx.DiGraph(nx.path_graph(5)) + + Check whether a subgraph of G is isomorphic to H: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H) + >>> isomatcher.subgraph_is_isomorphic() + False + + Check whether a subgraph of H is isomorphic to G: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G) + >>> isomatcher.subgraph_is_isomorphic() + True + """ + return super().subgraph_is_isomorphic() + + def subgraph_is_monomorphic(self): + """Returns `True` if a subgraph of ``G1`` is monomorphic to ``G2``. + + Examples + -------- + When creating the `DiGraphMatcher`, the order of the arguments is important. + + >>> G = nx.DiGraph([("A", "B"), ("C", "B"), ("D", "C")]) + >>> H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 2)]) + + Check whether a subgraph of G is monomorphic to H: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H) + >>> isomatcher.subgraph_is_monomorphic() + False + + Check whether a subgraph of H is isomorphic to G: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G) + >>> isomatcher.subgraph_is_monomorphic() + True + """ + return super().subgraph_is_monomorphic() + + def subgraph_isomorphisms_iter(self): + """Generator over isomorphisms between a subgraph of ``G1`` and ``G2``. + + Examples + -------- + When creating the `DiGraphMatcher`, the order of the arguments is important + + >>> G = nx.DiGraph([("B", "C"), ("C", "B"), ("C", "D"), ("D", "C")]) + >>> H = nx.DiGraph(nx.path_graph(5)) + + Yield isomorphic mappings between ``H`` and subgraphs of ``G``: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H) + >>> list(isomatcher.subgraph_isomorphisms_iter()) + [] + + Yield isomorphic mappings between ``G`` and subgraphs of ``H``: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G) + >>> next(isomatcher.subgraph_isomorphisms_iter()) + {0: 'B', 1: 'C', 2: 'D'} + """ + return super().subgraph_isomorphisms_iter() + + def subgraph_monomorphisms_iter(self): + """Generator over monomorphisms between a subgraph of ``G1`` and ``G2``. + + Examples + -------- + When creating the `DiGraphMatcher`, the order of the arguments is important. + + >>> G = nx.DiGraph([("A", "B"), ("C", "B"), ("D", "C")]) + >>> H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 2)]) + + Yield monomorphic mappings between ``H`` and subgraphs of ``G``: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(G, H) + >>> list(isomatcher.subgraph_monomorphisms_iter()) + [] + + Yield monomorphic mappings between ``G`` and subgraphs of ``H``: + + >>> isomatcher = nx.isomorphism.DiGraphMatcher(H, G) + >>> next(isomatcher.subgraph_monomorphisms_iter()) + {3: 'A', 2: 'B', 1: 'C', 0: 'D'} + """ + return super().subgraph_monomorphisms_iter() + + +class GMState: + """Internal representation of state for the GraphMatcher class. + + This class is used internally by the GraphMatcher class. It is used + only to store state specific data. There will be at most G2.order() of + these objects in memory at a time, due to the depth-first search + strategy employed by the VF2 algorithm. + """ + + def __init__(self, GM, G1_node=None, G2_node=None): + """Initializes GMState object. + + Pass in the GraphMatcher to which this GMState belongs and the + new node pair that will be added to the GraphMatcher's current + isomorphism mapping. + """ + self.GM = GM + + # Initialize the last stored node pair. + self.G1_node = None + self.G2_node = None + self.depth = len(GM.core_1) + + if G1_node is None or G2_node is None: + # Then we reset the class variables + GM.core_1 = {} + GM.core_2 = {} + GM.inout_1 = {} + GM.inout_2 = {} + + # Watch out! G1_node == 0 should evaluate to True. + if G1_node is not None and G2_node is not None: + # Add the node pair to the isomorphism mapping. + GM.core_1[G1_node] = G2_node + GM.core_2[G2_node] = G1_node + + # Store the node that was added last. + self.G1_node = G1_node + self.G2_node = G2_node + + # Now we must update the other two vectors. + # We will add only if it is not in there already! + self.depth = len(GM.core_1) + + # First we add the new nodes... + if G1_node not in GM.inout_1: + GM.inout_1[G1_node] = self.depth + if G2_node not in GM.inout_2: + GM.inout_2[G2_node] = self.depth + + # Now we add every other node... + + # Updates for T_1^{inout} + new_nodes = set() + for node in GM.core_1: + new_nodes.update( + [neighbor for neighbor in GM.G1[node] if neighbor not in GM.core_1] + ) + for node in new_nodes: + if node not in GM.inout_1: + GM.inout_1[node] = self.depth + + # Updates for T_2^{inout} + new_nodes = set() + for node in GM.core_2: + new_nodes.update( + [neighbor for neighbor in GM.G2[node] if neighbor not in GM.core_2] + ) + for node in new_nodes: + if node not in GM.inout_2: + GM.inout_2[node] = self.depth + + def restore(self): + """Deletes the GMState object and restores the class variables.""" + # First we remove the node that was added from the core vectors. + # Watch out! G1_node == 0 should evaluate to True. + if self.G1_node is not None and self.G2_node is not None: + del self.GM.core_1[self.G1_node] + del self.GM.core_2[self.G2_node] + + # Now we revert the other two vectors. + # Thus, we delete all entries which have this depth level. + for vector in (self.GM.inout_1, self.GM.inout_2): + for node in list(vector.keys()): + if vector[node] == self.depth: + del vector[node] + + +class DiGMState: + """Internal representation of state for the DiGraphMatcher class. + + This class is used internally by the DiGraphMatcher class. It is used + only to store state specific data. There will be at most G2.order() of + these objects in memory at a time, due to the depth-first search + strategy employed by the VF2 algorithm. + + """ + + def __init__(self, GM, G1_node=None, G2_node=None): + """Initializes DiGMState object. + + Pass in the DiGraphMatcher to which this DiGMState belongs and the + new node pair that will be added to the GraphMatcher's current + isomorphism mapping. + """ + self.GM = GM + + # Initialize the last stored node pair. + self.G1_node = None + self.G2_node = None + self.depth = len(GM.core_1) + + if G1_node is None or G2_node is None: + # Then we reset the class variables + GM.core_1 = {} + GM.core_2 = {} + GM.in_1 = {} + GM.in_2 = {} + GM.out_1 = {} + GM.out_2 = {} + + # Watch out! G1_node == 0 should evaluate to True. + if G1_node is not None and G2_node is not None: + # Add the node pair to the isomorphism mapping. + GM.core_1[G1_node] = G2_node + GM.core_2[G2_node] = G1_node + + # Store the node that was added last. + self.G1_node = G1_node + self.G2_node = G2_node + + # Now we must update the other four vectors. + # We will add only if it is not in there already! + self.depth = len(GM.core_1) + + # First we add the new nodes... + for vector in (GM.in_1, GM.out_1): + if G1_node not in vector: + vector[G1_node] = self.depth + for vector in (GM.in_2, GM.out_2): + if G2_node not in vector: + vector[G2_node] = self.depth + + # Now we add every other node... + + # Updates for T_1^{in} + new_nodes = set() + for node in GM.core_1: + new_nodes.update( + [ + predecessor + for predecessor in GM.G1.predecessors(node) + if predecessor not in GM.core_1 + ] + ) + for node in new_nodes: + if node not in GM.in_1: + GM.in_1[node] = self.depth + + # Updates for T_2^{in} + new_nodes = set() + for node in GM.core_2: + new_nodes.update( + [ + predecessor + for predecessor in GM.G2.predecessors(node) + if predecessor not in GM.core_2 + ] + ) + for node in new_nodes: + if node not in GM.in_2: + GM.in_2[node] = self.depth + + # Updates for T_1^{out} + new_nodes = set() + for node in GM.core_1: + new_nodes.update( + [ + successor + for successor in GM.G1.successors(node) + if successor not in GM.core_1 + ] + ) + for node in new_nodes: + if node not in GM.out_1: + GM.out_1[node] = self.depth + + # Updates for T_2^{out} + new_nodes = set() + for node in GM.core_2: + new_nodes.update( + [ + successor + for successor in GM.G2.successors(node) + if successor not in GM.core_2 + ] + ) + for node in new_nodes: + if node not in GM.out_2: + GM.out_2[node] = self.depth + + def restore(self): + """Deletes the DiGMState object and restores the class variables.""" + + # First we remove the node that was added from the core vectors. + # Watch out! G1_node == 0 should evaluate to True. + if self.G1_node is not None and self.G2_node is not None: + del self.GM.core_1[self.G1_node] + del self.GM.core_2[self.G2_node] + + # Now we revert the other four vectors. + # Thus, we delete all entries which have this depth level. + for vector in (self.GM.in_1, self.GM.in_2, self.GM.out_1, self.GM.out_2): + for node in list(vector.keys()): + if vector[node] == self.depth: + del vector[node] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/matchhelpers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/matchhelpers.py new file mode 100644 index 0000000000000000000000000000000000000000..b48820d4d1896a8be1153f3e82feb2c3a5239761 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/matchhelpers.py @@ -0,0 +1,352 @@ +"""Functions which help end users define customize node_match and +edge_match functions to use during isomorphism checks. +""" + +import math +import types +from itertools import permutations + +__all__ = [ + "categorical_node_match", + "categorical_edge_match", + "categorical_multiedge_match", + "numerical_node_match", + "numerical_edge_match", + "numerical_multiedge_match", + "generic_node_match", + "generic_edge_match", + "generic_multiedge_match", +] + + +def copyfunc(f, name=None): + """Returns a deepcopy of a function.""" + return types.FunctionType( + f.__code__, f.__globals__, name or f.__name__, f.__defaults__, f.__closure__ + ) + + +def allclose(x, y, rtol=1.0000000000000001e-05, atol=1e-08): + """Returns True if x and y are sufficiently close, elementwise. + + Parameters + ---------- + rtol : float + The relative error tolerance. + atol : float + The absolute error tolerance. + + """ + # assume finite weights, see numpy.allclose() for reference + return all(math.isclose(xi, yi, rel_tol=rtol, abs_tol=atol) for xi, yi in zip(x, y)) + + +categorical_doc = """ +Returns a comparison function for a categorical node attribute. + +The value(s) of the attr(s) must be hashable and comparable via the == +operator since they are placed into a set([]) object. If the sets from +G1 and G2 are the same, then the constructed function returns True. + +Parameters +---------- +attr : string | list + The categorical node attribute to compare, or a list of categorical + node attributes to compare. +default : value | list + The default value for the categorical node attribute, or a list of + default values for the categorical node attributes. + +Returns +------- +match : function + The customized, categorical `node_match` function. + +Examples +-------- +>>> import networkx.algorithms.isomorphism as iso +>>> nm = iso.categorical_node_match("size", 1) +>>> nm = iso.categorical_node_match(["color", "size"], ["red", 2]) + +""" + + +def categorical_node_match(attr, default): + if isinstance(attr, str): + + def match(data1, data2): + return data1.get(attr, default) == data2.get(attr, default) + + else: + attrs = list(zip(attr, default)) # Python 3 + + def match(data1, data2): + return all(data1.get(attr, d) == data2.get(attr, d) for attr, d in attrs) + + return match + + +categorical_edge_match = copyfunc(categorical_node_match, "categorical_edge_match") + + +def categorical_multiedge_match(attr, default): + if isinstance(attr, str): + + def match(datasets1, datasets2): + values1 = {data.get(attr, default) for data in datasets1.values()} + values2 = {data.get(attr, default) for data in datasets2.values()} + return values1 == values2 + + else: + attrs = list(zip(attr, default)) # Python 3 + + def match(datasets1, datasets2): + values1 = set() + for data1 in datasets1.values(): + x = tuple(data1.get(attr, d) for attr, d in attrs) + values1.add(x) + values2 = set() + for data2 in datasets2.values(): + x = tuple(data2.get(attr, d) for attr, d in attrs) + values2.add(x) + return values1 == values2 + + return match + + +# Docstrings for categorical functions. +categorical_node_match.__doc__ = categorical_doc +categorical_edge_match.__doc__ = categorical_doc.replace("node", "edge") +tmpdoc = categorical_doc.replace("node", "edge") +tmpdoc = tmpdoc.replace("categorical_edge_match", "categorical_multiedge_match") +categorical_multiedge_match.__doc__ = tmpdoc + + +numerical_doc = """ +Returns a comparison function for a numerical node attribute. + +The value(s) of the attr(s) must be numerical and sortable. If the +sorted list of values from G1 and G2 are the same within some +tolerance, then the constructed function returns True. + +Parameters +---------- +attr : string | list + The numerical node attribute to compare, or a list of numerical + node attributes to compare. +default : value | list + The default value for the numerical node attribute, or a list of + default values for the numerical node attributes. +rtol : float + The relative error tolerance. +atol : float + The absolute error tolerance. + +Returns +------- +match : function + The customized, numerical `node_match` function. + +Examples +-------- +>>> import networkx.algorithms.isomorphism as iso +>>> nm = iso.numerical_node_match("weight", 1.0) +>>> nm = iso.numerical_node_match(["weight", "linewidth"], [0.25, 0.5]) + +""" + + +def numerical_node_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08): + if isinstance(attr, str): + + def match(data1, data2): + return math.isclose( + data1.get(attr, default), + data2.get(attr, default), + rel_tol=rtol, + abs_tol=atol, + ) + + else: + attrs = list(zip(attr, default)) # Python 3 + + def match(data1, data2): + values1 = [data1.get(attr, d) for attr, d in attrs] + values2 = [data2.get(attr, d) for attr, d in attrs] + return allclose(values1, values2, rtol=rtol, atol=atol) + + return match + + +numerical_edge_match = copyfunc(numerical_node_match, "numerical_edge_match") + + +def numerical_multiedge_match(attr, default, rtol=1.0000000000000001e-05, atol=1e-08): + if isinstance(attr, str): + + def match(datasets1, datasets2): + values1 = sorted(data.get(attr, default) for data in datasets1.values()) + values2 = sorted(data.get(attr, default) for data in datasets2.values()) + return allclose(values1, values2, rtol=rtol, atol=atol) + + else: + attrs = list(zip(attr, default)) # Python 3 + + def match(datasets1, datasets2): + values1 = [] + for data1 in datasets1.values(): + x = tuple(data1.get(attr, d) for attr, d in attrs) + values1.append(x) + values2 = [] + for data2 in datasets2.values(): + x = tuple(data2.get(attr, d) for attr, d in attrs) + values2.append(x) + values1.sort() + values2.sort() + for xi, yi in zip(values1, values2): + if not allclose(xi, yi, rtol=rtol, atol=atol): + return False + else: + return True + + return match + + +# Docstrings for numerical functions. +numerical_node_match.__doc__ = numerical_doc +numerical_edge_match.__doc__ = numerical_doc.replace("node", "edge") +tmpdoc = numerical_doc.replace("node", "edge") +tmpdoc = tmpdoc.replace("numerical_edge_match", "numerical_multiedge_match") +numerical_multiedge_match.__doc__ = tmpdoc + + +generic_doc = """ +Returns a comparison function for a generic attribute. + +The value(s) of the attr(s) are compared using the specified +operators. If all the attributes are equal, then the constructed +function returns True. + +Parameters +---------- +attr : string | list + The node attribute to compare, or a list of node attributes + to compare. +default : value | list + The default value for the node attribute, or a list of + default values for the node attributes. +op : callable | list + The operator to use when comparing attribute values, or a list + of operators to use when comparing values for each attribute. + +Returns +------- +match : function + The customized, generic `node_match` function. + +Examples +-------- +>>> from operator import eq +>>> from math import isclose +>>> from networkx.algorithms.isomorphism import generic_node_match +>>> nm = generic_node_match("weight", 1.0, isclose) +>>> nm = generic_node_match("color", "red", eq) +>>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq]) + +""" + + +def generic_node_match(attr, default, op): + if isinstance(attr, str): + + def match(data1, data2): + return op(data1.get(attr, default), data2.get(attr, default)) + + else: + attrs = list(zip(attr, default, op)) # Python 3 + + def match(data1, data2): + for attr, d, operator in attrs: + if not operator(data1.get(attr, d), data2.get(attr, d)): + return False + else: + return True + + return match + + +generic_edge_match = copyfunc(generic_node_match, "generic_edge_match") + + +def generic_multiedge_match(attr, default, op): + """Returns a comparison function for a generic attribute. + + The value(s) of the attr(s) are compared using the specified + operators. If all the attributes are equal, then the constructed + function returns True. Potentially, the constructed edge_match + function can be slow since it must verify that no isomorphism + exists between the multiedges before it returns False. + + Parameters + ---------- + attr : string | list + The edge attribute to compare, or a list of node attributes + to compare. + default : value | list + The default value for the edge attribute, or a list of + default values for the edgeattributes. + op : callable | list + The operator to use when comparing attribute values, or a list + of operators to use when comparing values for each attribute. + + Returns + ------- + match : function + The customized, generic `edge_match` function. + + Examples + -------- + >>> from operator import eq + >>> from math import isclose + >>> from networkx.algorithms.isomorphism import generic_node_match + >>> nm = generic_node_match("weight", 1.0, isclose) + >>> nm = generic_node_match("color", "red", eq) + >>> nm = generic_node_match(["weight", "color"], [1.0, "red"], [isclose, eq]) + + """ + + # This is slow, but generic. + # We must test every possible isomorphism between the edges. + if isinstance(attr, str): + attr = [attr] + default = [default] + op = [op] + attrs = list(zip(attr, default)) # Python 3 + + def match(datasets1, datasets2): + values1 = [] + for data1 in datasets1.values(): + x = tuple(data1.get(attr, d) for attr, d in attrs) + values1.append(x) + values2 = [] + for data2 in datasets2.values(): + x = tuple(data2.get(attr, d) for attr, d in attrs) + values2.append(x) + for vals2 in permutations(values2): + for xi, yi in zip(values1, vals2): + if not all(map(lambda x, y, z: z(x, y), xi, yi, op)): + # This is not an isomorphism, go to next permutation. + break + else: + # Then we found an isomorphism. + return True + else: + # Then there are no isomorphisms between the multiedges. + return False + + return match + + +# Docstrings for numerical functions. +generic_node_match.__doc__ = generic_doc +generic_edge_match.__doc__ = generic_doc.replace("node", "edge") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py new file mode 100644 index 0000000000000000000000000000000000000000..62cacc77887efa99026c117687bb9ad82cebd4dd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py @@ -0,0 +1,308 @@ +""" +***************************** +Time-respecting VF2 Algorithm +***************************** + +An extension of the VF2 algorithm for time-respecting graph isomorphism +testing in temporal graphs. + +A temporal graph is one in which edges contain a datetime attribute, +denoting when interaction occurred between the incident nodes. A +time-respecting subgraph of a temporal graph is a subgraph such that +all interactions incident to a node occurred within a time threshold, +delta, of each other. A directed time-respecting subgraph has the +added constraint that incoming interactions to a node must precede +outgoing interactions from the same node - this enforces a sense of +directed flow. + +Introduction +------------ + +The TimeRespectingGraphMatcher and TimeRespectingDiGraphMatcher +extend the GraphMatcher and DiGraphMatcher classes, respectively, +to include temporal constraints on matches. This is achieved through +a semantic check, via the semantic_feasibility() function. + +As well as including G1 (the graph in which to seek embeddings) and +G2 (the subgraph structure of interest), the name of the temporal +attribute on the edges and the time threshold, delta, must be supplied +as arguments to the matching constructors. + +A delta of zero is the strictest temporal constraint on the match - +only embeddings in which all interactions occur at the same time will +be returned. A delta of one day will allow embeddings in which +adjacent interactions occur up to a day apart. + +Examples +-------- + +Examples will be provided when the datetime type has been incorporated. + + +Temporal Subgraph Isomorphism +----------------------------- + +A brief discussion of the somewhat diverse current literature will be +included here. + +References +---------- + +[1] Redmond, U. and Cunningham, P. Temporal subgraph isomorphism. In: +The 2013 IEEE/ACM International Conference on Advances in Social +Networks Analysis and Mining (ASONAM). Niagara Falls, Canada; 2013: +pages 1451 - 1452. [65] + +For a discussion of the literature on temporal networks: + +[3] P. Holme and J. Saramaki. Temporal networks. Physics Reports, +519(3):97–125, 2012. + +Notes +----- + +Handles directed and undirected graphs and graphs with parallel edges. + +""" + +import networkx as nx + +from .isomorphvf2 import DiGraphMatcher, GraphMatcher + +__all__ = ["TimeRespectingGraphMatcher", "TimeRespectingDiGraphMatcher"] + + +class TimeRespectingGraphMatcher(GraphMatcher): + def __init__(self, G1, G2, temporal_attribute_name, delta): + """Initialize TimeRespectingGraphMatcher. + + G1 and G2 should be nx.Graph or nx.MultiGraph instances. + + Examples + -------- + To create a TimeRespectingGraphMatcher which checks for + syntactic and semantic feasibility: + + >>> from networkx.algorithms import isomorphism + >>> from datetime import timedelta + >>> G1 = nx.Graph(nx.path_graph(4, create_using=nx.Graph())) + + >>> G2 = nx.Graph(nx.path_graph(4, create_using=nx.Graph())) + + >>> GM = isomorphism.TimeRespectingGraphMatcher( + ... G1, G2, "date", timedelta(days=1) + ... ) + """ + self.temporal_attribute_name = temporal_attribute_name + self.delta = delta + super().__init__(G1, G2) + + def one_hop(self, Gx, Gx_node, neighbors): + """ + Edges one hop out from a node in the mapping should be + time-respecting with respect to each other. + """ + dates = [] + for n in neighbors: + if isinstance(Gx, nx.Graph): # Graph G[u][v] returns the data dictionary. + dates.append(Gx[Gx_node][n][self.temporal_attribute_name]) + else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. + for edge in Gx[Gx_node][ + n + ].values(): # Iterates all edges between node pair. + dates.append(edge[self.temporal_attribute_name]) + if any(x is None for x in dates): + raise ValueError("Datetime not supplied for at least one edge.") + return not dates or max(dates) - min(dates) <= self.delta + + def two_hop(self, Gx, core_x, Gx_node, neighbors): + """ + Paths of length 2 from Gx_node should be time-respecting. + """ + return all( + self.one_hop(Gx, v, [n for n in Gx[v] if n in core_x] + [Gx_node]) + for v in neighbors + ) + + def semantic_feasibility(self, G1_node, G2_node): + """Returns True if adding (G1_node, G2_node) is semantically + feasible. + + Any subclass which redefines semantic_feasibility() must + maintain the self.tests if needed, to keep the match() method + functional. Implementations should consider multigraphs. + """ + neighbors = [n for n in self.G1[G1_node] if n in self.core_1] + if not self.one_hop(self.G1, G1_node, neighbors): # Fail fast on first node. + return False + if not self.two_hop(self.G1, self.core_1, G1_node, neighbors): + return False + # Otherwise, this node is semantically feasible! + return True + + +class TimeRespectingDiGraphMatcher(DiGraphMatcher): + def __init__(self, G1, G2, temporal_attribute_name, delta): + """Initialize TimeRespectingDiGraphMatcher. + + G1 and G2 should be nx.DiGraph or nx.MultiDiGraph instances. + + Examples + -------- + To create a TimeRespectingDiGraphMatcher which checks for + syntactic and semantic feasibility: + + >>> from networkx.algorithms import isomorphism + >>> from datetime import timedelta + >>> G1 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) + + >>> G2 = nx.DiGraph(nx.path_graph(4, create_using=nx.DiGraph())) + + >>> GM = isomorphism.TimeRespectingDiGraphMatcher( + ... G1, G2, "date", timedelta(days=1) + ... ) + """ + self.temporal_attribute_name = temporal_attribute_name + self.delta = delta + super().__init__(G1, G2) + + def get_pred_dates(self, Gx, Gx_node, core_x, pred): + """ + Get the dates of edges from predecessors. + """ + pred_dates = [] + if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary. + for n in pred: + pred_dates.append(Gx[n][Gx_node][self.temporal_attribute_name]) + else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. + for n in pred: + for edge in Gx[n][ + Gx_node + ].values(): # Iterates all edge data between node pair. + pred_dates.append(edge[self.temporal_attribute_name]) + return pred_dates + + def get_succ_dates(self, Gx, Gx_node, core_x, succ): + """ + Get the dates of edges to successors. + """ + succ_dates = [] + if isinstance(Gx, nx.DiGraph): # Graph G[u][v] returns the data dictionary. + for n in succ: + succ_dates.append(Gx[Gx_node][n][self.temporal_attribute_name]) + else: # MultiGraph G[u][v] returns a dictionary of key -> data dictionary. + for n in succ: + for edge in Gx[Gx_node][ + n + ].values(): # Iterates all edge data between node pair. + succ_dates.append(edge[self.temporal_attribute_name]) + return succ_dates + + def one_hop(self, Gx, Gx_node, core_x, pred, succ): + """ + The ego node. + """ + pred_dates = self.get_pred_dates(Gx, Gx_node, core_x, pred) + succ_dates = self.get_succ_dates(Gx, Gx_node, core_x, succ) + return self.test_one(pred_dates, succ_dates) and self.test_two( + pred_dates, succ_dates + ) + + def two_hop_pred(self, Gx, Gx_node, core_x, pred): + """ + The predecessors of the ego node. + """ + return all( + self.one_hop( + Gx, + p, + core_x, + self.preds(Gx, core_x, p), + self.succs(Gx, core_x, p, Gx_node), + ) + for p in pred + ) + + def two_hop_succ(self, Gx, Gx_node, core_x, succ): + """ + The successors of the ego node. + """ + return all( + self.one_hop( + Gx, + s, + core_x, + self.preds(Gx, core_x, s, Gx_node), + self.succs(Gx, core_x, s), + ) + for s in succ + ) + + def preds(self, Gx, core_x, v, Gx_node=None): + pred = [n for n in Gx.predecessors(v) if n in core_x] + if Gx_node: + pred.append(Gx_node) + return pred + + def succs(self, Gx, core_x, v, Gx_node=None): + succ = [n for n in Gx.successors(v) if n in core_x] + if Gx_node: + succ.append(Gx_node) + return succ + + def test_one(self, pred_dates, succ_dates): + """ + Edges one hop out from Gx_node in the mapping should be + time-respecting with respect to each other, regardless of + direction. + """ + time_respecting = True + dates = pred_dates + succ_dates + + if any(x is None for x in dates): + raise ValueError("Date or datetime not supplied for at least one edge.") + + dates.sort() # Small to large. + if 0 < len(dates) and not (dates[-1] - dates[0] <= self.delta): + time_respecting = False + return time_respecting + + def test_two(self, pred_dates, succ_dates): + """ + Edges from a dual Gx_node in the mapping should be ordered in + a time-respecting manner. + """ + time_respecting = True + pred_dates.sort() + succ_dates.sort() + # First out before last in; negative of the necessary condition for time-respect. + if ( + 0 < len(succ_dates) + and 0 < len(pred_dates) + and succ_dates[0] < pred_dates[-1] + ): + time_respecting = False + return time_respecting + + def semantic_feasibility(self, G1_node, G2_node): + """Returns True if adding (G1_node, G2_node) is semantically + feasible. + + Any subclass which redefines semantic_feasibility() must + maintain the self.tests if needed, to keep the match() method + functional. Implementations should consider multigraphs. + """ + pred, succ = ( + [n for n in self.G1.predecessors(G1_node) if n in self.core_1], + [n for n in self.G1.successors(G1_node) if n in self.core_1], + ) + if not self.one_hop( + self.G1, G1_node, self.core_1, pred, succ + ): # Fail fast on first node. + return False + if not self.two_hop_pred(self.G1, G1_node, self.core_1, pred): + return False + if not self.two_hop_succ(self.G1, G1_node, self.core_1, succ): + return False + # Otherwise, this node is semantically feasible! + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py new file mode 100644 index 0000000000000000000000000000000000000000..59892ce416c9c747e7e1e22b78d3ef62f50401b8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py @@ -0,0 +1,264 @@ +""" +An algorithm for finding if two undirected trees are isomorphic, +and if so returns an isomorphism between the two sets of nodes. + +This algorithm uses a routine to tell if two rooted trees (trees with a +specified root node) are isomorphic, which may be independently useful. + +This implements an algorithm from: +The Design and Analysis of Computer Algorithms +by Aho, Hopcroft, and Ullman +Addison-Wesley Publishing 1974 +Example 3.2 pp. 84-86. + +A more understandable version of this algorithm is described in: +Homework Assignment 5 +McGill University SOCS 308-250B, Winter 2002 +by Matthew Suderman +http://crypto.cs.mcgill.ca/~crepeau/CS250/2004/HW5+.pdf +""" + +from collections import defaultdict + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +__all__ = ["rooted_tree_isomorphism", "tree_isomorphism"] + + +@nx._dispatchable(graphs={"t1": 0, "t2": 2}, returns_graph=True) +def root_trees(t1, root1, t2, root2): + """Create a single digraph dT of free trees t1 and t2 + # with roots root1 and root2 respectively + # rename the nodes with consecutive integers + # so that all nodes get a unique name between both trees + + # our new "fake" root node is 0 + # t1 is numbers from 1 ... n + # t2 is numbered from n+1 to 2n + """ + + dT = nx.DiGraph() + + newroot1 = 1 # left root will be 1 + newroot2 = nx.number_of_nodes(t1) + 1 # right will be n+1 + + # may be overlap in node names here so need separate maps + # given the old name, what is the new + namemap1 = {root1: newroot1} + namemap2 = {root2: newroot2} + + # add an edge from our new root to root1 and root2 + dT.add_edge(0, namemap1[root1]) + dT.add_edge(0, namemap2[root2]) + + for i, (v1, v2) in enumerate(nx.bfs_edges(t1, root1)): + namemap1[v2] = i + namemap1[root1] + 1 + dT.add_edge(namemap1[v1], namemap1[v2]) + + for i, (v1, v2) in enumerate(nx.bfs_edges(t2, root2)): + namemap2[v2] = i + namemap2[root2] + 1 + dT.add_edge(namemap2[v1], namemap2[v2]) + + # now we really want the inverse of namemap1 and namemap2 + # giving the old name given the new + # since the values of namemap1 and namemap2 are unique + # there won't be collisions + namemap = {} + for old, new in namemap1.items(): + namemap[new] = old + for old, new in namemap2.items(): + namemap[new] = old + + return (dT, namemap, newroot1, newroot2) + + +@nx._dispatchable(graphs={"t1": 0, "t2": 2}) +def rooted_tree_isomorphism(t1, root1, t2, root2): + """ + Return an isomorphic mapping between rooted trees `t1` and `t2` with roots + `root1` and `root2`, respectively. + + These trees may be either directed or undirected, + but if they are directed, all edges should flow from the root. + + It returns the isomorphism, a mapping of the nodes of `t1` onto the nodes + of `t2`, such that two trees are then identical. + + Note that two trees may have more than one isomorphism, and this + routine just returns one valid mapping. + This is a subroutine used to implement `tree_isomorphism`, but will + be somewhat faster if you already have rooted trees. + + Parameters + ---------- + t1 : NetworkX graph + One of the trees being compared + + root1 : node + A node of `t1` which is the root of the tree + + t2 : NetworkX graph + The other tree being compared + + root2 : node + a node of `t2` which is the root of the tree + + Returns + ------- + isomorphism : list + A list of pairs in which the left element is a node in `t1` + and the right element is a node in `t2`. The pairs are in + arbitrary order. If the nodes in one tree is mapped to the names in + the other, then trees will be identical. Note that an isomorphism + will not necessarily be unique. + + If `t1` and `t2` are not isomorphic, then it returns the empty list. + + Raises + ------ + NetworkXError + If either `t1` or `t2` is not a tree + """ + + if not nx.is_tree(t1): + raise nx.NetworkXError("t1 is not a tree") + if not nx.is_tree(t2): + raise nx.NetworkXError("t2 is not a tree") + + # get the rooted tree formed by combining them + # with unique names + (dT, namemap, newroot1, newroot2) = root_trees(t1, root1, t2, root2) + + # Group nodes by their distance from the root + L = defaultdict(list) + for n, dist in nx.shortest_path_length(dT, source=0).items(): + L[dist].append(n) + + # height + h = max(L) + + # each node has a label, initially set to 0 + label = {v: 0 for v in dT} + # and also ordered_labels and ordered_children + # which will store ordered tuples + ordered_labels = {v: () for v in dT} + ordered_children = {v: () for v in dT} + + # nothing to do on last level so start on h-1 + # also nothing to do for our fake level 0, so skip that + for i in range(h - 1, 0, -1): + # update the ordered_labels and ordered_children + # for any children + for v in L[i]: + # nothing to do if no children + if dT.out_degree(v) > 0: + # get all the pairs of labels and nodes of children and sort by labels + # reverse=True to preserve DFS order, see gh-7945 + s = sorted(((label[u], u) for u in dT.successors(v)), reverse=True) + + # invert to give a list of two tuples + # the sorted labels, and the corresponding children + ordered_labels[v], ordered_children[v] = list(zip(*s)) + + # now collect and sort the sorted ordered_labels + # for all nodes in L[i], carrying along the node + forlabel = sorted((ordered_labels[v], v) for v in L[i]) + + # now assign labels to these nodes, according to the sorted order + # starting from 0, where identical ordered_labels get the same label + current = 0 + for i, (ol, v) in enumerate(forlabel): + # advance to next label if not 0, and different from previous + if (i != 0) and (ol != forlabel[i - 1][0]): + current += 1 + label[v] = current + + # they are isomorphic if the labels of newroot1 and newroot2 are 0 + isomorphism = [] + if label[newroot1] == 0 and label[newroot2] == 0: + # now lets get the isomorphism by walking the ordered_children + stack = [(newroot1, newroot2)] + while stack: + curr_v, curr_w = stack.pop() + isomorphism.append((curr_v, curr_w)) + stack.extend(zip(ordered_children[curr_v], ordered_children[curr_w])) + + # get the mapping back in terms of the old names + # return in sorted order for neatness + isomorphism = [(namemap[u], namemap[v]) for (u, v) in isomorphism] + + return isomorphism + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(graphs={"t1": 0, "t2": 1}) +def tree_isomorphism(t1, t2): + """ + Return an isomorphic mapping between two trees `t1` and `t2`. + + If `t1` and `t2` are not isomorphic, an empty list is returned. + Note that two trees may have more than one isomorphism, and this routine just + returns one valid mapping. + + Parameters + ---------- + t1 : undirected NetworkX graph + One of the trees being compared + + t2 : undirected NetworkX graph + The other tree being compared + + Returns + ------- + isomorphism : list + A list of pairs in which the left element is a node in `t1` + and the right element is a node in `t2`. The pairs are in + arbitrary order. If the nodes in one tree is mapped to the names in + the other, then trees will be identical. Note that an isomorphism + will not necessarily be unique. + + If `t1` and `t2` are not isomorphic, then it returns the empty list. + + Raises + ------ + NetworkXError + If either `t1` or `t2` is not a tree + + Notes + ----- + This runs in ``O(n*log(n))`` time for trees with ``n`` nodes. + """ + if not nx.is_tree(t1): + raise nx.NetworkXError("t1 is not a tree") + if not nx.is_tree(t2): + raise nx.NetworkXError("t2 is not a tree") + + # To be isomorphic, t1 and t2 must have the same number of nodes and sorted + # degree sequences + if not nx.faster_could_be_isomorphic(t1, t2): + return [] + + # A tree can have either 1 or 2 centers. + # If the number doesn't match then t1 and t2 are not isomorphic. + center1 = nx.center(t1) + center2 = nx.center(t2) + + if len(center1) != len(center2): + return [] + + # If there is only 1 center in each, then use it. + if len(center1) == 1: + return rooted_tree_isomorphism(t1, center1[0], t2, center2[0]) + + # If there both have 2 centers, then try the first for t1 + # with the first for t2. + attempts = rooted_tree_isomorphism(t1, center1[0], t2, center2[0]) + + # If that worked we're done. + if len(attempts) > 0: + return attempts + + # Otherwise, try center1[0] with the center2[1], and see if that works + return rooted_tree_isomorphism(t1, center1[0], t2, center2[1]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2pp.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2pp.py new file mode 100644 index 0000000000000000000000000000000000000000..658829ea8d54a7d481c9b17c03be8226d14e1952 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2pp.py @@ -0,0 +1,1102 @@ +""" +*************** +VF2++ Algorithm +*************** + +An implementation of the VF2++ algorithm [1]_ for Graph Isomorphism testing. + +The simplest interface to use this module is to call: + +`vf2pp_is_isomorphic`: to check whether two graphs are isomorphic. +`vf2pp_isomorphism`: to obtain the node mapping between two graphs, +in case they are isomorphic. +`vf2pp_all_isomorphisms`: to generate all possible mappings between two graphs, +if isomorphic. + +Introduction +------------ +The VF2++ algorithm, follows a similar logic to that of VF2, while also +introducing new easy-to-check cutting rules and determining the optimal access +order of nodes. It is also implemented in a non-recursive manner, which saves +both time and space, when compared to its previous counterpart. + +The optimal node ordering is obtained after taking into consideration both the +degree but also the label rarity of each node. +This way we place the nodes that are more likely to match, first in the order, +thus examining the most promising branches in the beginning. +The rules also consider node labels, making it easier to prune unfruitful +branches early in the process. + +Examples +-------- + +Suppose G1 and G2 are Isomorphic Graphs. Verification is as follows: + +Without node labels: + +>>> import networkx as nx +>>> G1 = nx.path_graph(4) +>>> G2 = nx.path_graph(4) +>>> nx.vf2pp_is_isomorphic(G1, G2, node_label=None) +True +>>> nx.vf2pp_isomorphism(G1, G2, node_label=None) +{1: 1, 2: 2, 0: 0, 3: 3} + +With node labels: + +>>> G1 = nx.path_graph(4) +>>> G2 = nx.path_graph(4) +>>> mapped = {1: 1, 2: 2, 3: 3, 0: 0} +>>> nx.set_node_attributes( +... G1, dict(zip(G1, ["blue", "red", "green", "yellow"])), "label" +... ) +>>> nx.set_node_attributes( +... G2, +... dict(zip([mapped[u] for u in G1], ["blue", "red", "green", "yellow"])), +... "label", +... ) +>>> nx.vf2pp_is_isomorphic(G1, G2, node_label="label") +True +>>> nx.vf2pp_isomorphism(G1, G2, node_label="label") +{1: 1, 2: 2, 0: 0, 3: 3} + +References +---------- +.. [1] Jüttner, Alpár & Madarasi, Péter. (2018). "VF2++—An improved subgraph + isomorphism algorithm". Discrete Applied Mathematics. 242. + https://doi.org/10.1016/j.dam.2018.02.018 + +""" + +import collections + +import networkx as nx + +__all__ = ["vf2pp_isomorphism", "vf2pp_is_isomorphic", "vf2pp_all_isomorphisms"] + +_GraphParameters = collections.namedtuple( + "_GraphParameters", + [ + "G1", + "G2", + "G1_labels", + "G2_labels", + "nodes_of_G1Labels", + "nodes_of_G2Labels", + "G2_nodes_of_degree", + ], +) + +_StateParameters = collections.namedtuple( + "_StateParameters", + [ + "mapping", + "reverse_mapping", + "T1", + "T1_in", + "T1_tilde", + "T1_tilde_in", + "T2", + "T2_in", + "T2_tilde", + "T2_tilde_in", + ], +) + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"}) +def vf2pp_isomorphism(G1, G2, node_label=None, default_label=None): + """Return an isomorphic mapping between `G1` and `G2` if it exists. + + Parameters + ---------- + G1, G2 : NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism. + + node_label : str, optional + The name of the node attribute to be used when comparing nodes. + The default is `None`, meaning node attributes are not considered + in the comparison. Any node that doesn't have the `node_label` + attribute uses `default_label` instead. + + default_label : scalar + Default value to use when a node doesn't have an attribute + named `node_label`. Default is `None`. + + Returns + ------- + dict or None + Node mapping if the two graphs are isomorphic. None otherwise. + """ + try: + mapping = next(vf2pp_all_isomorphisms(G1, G2, node_label, default_label)) + return mapping + except StopIteration: + return None + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"}) +def vf2pp_is_isomorphic(G1, G2, node_label=None, default_label=None): + """Examines whether G1 and G2 are isomorphic. + + Parameters + ---------- + G1, G2 : NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism. + + node_label : str, optional + The name of the node attribute to be used when comparing nodes. + The default is `None`, meaning node attributes are not considered + in the comparison. Any node that doesn't have the `node_label` + attribute uses `default_label` instead. + + default_label : scalar + Default value to use when a node doesn't have an attribute + named `node_label`. Default is `None`. + + Returns + ------- + bool + True if the two graphs are isomorphic, False otherwise. + """ + if vf2pp_isomorphism(G1, G2, node_label, default_label) is not None: + return True + return False + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}, node_attrs={"node_label": "default_label"}) +def vf2pp_all_isomorphisms(G1, G2, node_label=None, default_label=None): + """Yields all the possible mappings between G1 and G2. + + Parameters + ---------- + G1, G2 : NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism. + + node_label : str, optional + The name of the node attribute to be used when comparing nodes. + The default is `None`, meaning node attributes are not considered + in the comparison. Any node that doesn't have the `node_label` + attribute uses `default_label` instead. + + default_label : scalar + Default value to use when a node doesn't have an attribute + named `node_label`. Default is `None`. + + Yields + ------ + dict + Isomorphic mapping between the nodes in `G1` and `G2`. + """ + if G1.number_of_nodes() == 0 or G2.number_of_nodes() == 0: + return False + + # Create the degree dicts based on graph type + if G1.is_directed(): + G1_degree = { + n: (in_degree, out_degree) + for (n, in_degree), (_, out_degree) in zip(G1.in_degree, G1.out_degree) + } + G2_degree = { + n: (in_degree, out_degree) + for (n, in_degree), (_, out_degree) in zip(G2.in_degree, G2.out_degree) + } + else: + G1_degree = dict(G1.degree) + G2_degree = dict(G2.degree) + + if not G1.is_directed(): + find_candidates = _find_candidates + restore_Tinout = _restore_Tinout + else: + find_candidates = _find_candidates_Di + restore_Tinout = _restore_Tinout_Di + + # Check that both graphs have the same number of nodes and degree sequence + if G1.order() != G2.order(): + return False + if sorted(G1_degree.values()) != sorted(G2_degree.values()): + return False + + # Initialize parameters and cache necessary information about degree and labels + graph_params, state_params = _initialize_parameters( + G1, G2, G2_degree, node_label, default_label + ) + + # Check if G1 and G2 have the same labels, and that number of nodes per label + # is equal between the two graphs + if not _precheck_label_properties(graph_params): + return False + + # Calculate the optimal node ordering + node_order = _matching_order(graph_params) + + # Initialize the stack + stack = [] + candidates = iter( + find_candidates(node_order[0], graph_params, state_params, G1_degree) + ) + stack.append((node_order[0], candidates)) + + mapping = state_params.mapping + reverse_mapping = state_params.reverse_mapping + + # Index of the node from the order, currently being examined + matching_node = 1 + + while stack: + current_node, candidate_nodes = stack[-1] + + try: + candidate = next(candidate_nodes) + except StopIteration: + # If no remaining candidates, return to a previous state, and follow another branch + stack.pop() + matching_node -= 1 + if stack: + # Pop the previously added u-v pair, and look for a different candidate _v for u + popped_node1, _ = stack[-1] + popped_node2 = mapping[popped_node1] + mapping.pop(popped_node1) + reverse_mapping.pop(popped_node2) + restore_Tinout(popped_node1, popped_node2, graph_params, state_params) + continue + + if _feasibility(current_node, candidate, graph_params, state_params): + # Terminate if mapping is extended to its full + if len(mapping) == G2.number_of_nodes() - 1: + cp_mapping = mapping.copy() + cp_mapping[current_node] = candidate + yield cp_mapping + continue + + # Feasibility rules pass, so extend the mapping and update the parameters + mapping[current_node] = candidate + reverse_mapping[candidate] = current_node + _update_Tinout(current_node, candidate, graph_params, state_params) + # Append the next node and its candidates to the stack + candidates = iter( + find_candidates( + node_order[matching_node], graph_params, state_params, G1_degree + ) + ) + stack.append((node_order[matching_node], candidates)) + matching_node += 1 + + +def _precheck_label_properties(graph_params): + G1, G2, G1_labels, G2_labels, nodes_of_G1Labels, nodes_of_G2Labels, _ = graph_params + if any( + label not in nodes_of_G1Labels or len(nodes_of_G1Labels[label]) != len(nodes) + for label, nodes in nodes_of_G2Labels.items() + ): + return False + return True + + +def _initialize_parameters(G1, G2, G2_degree, node_label=None, default_label=-1): + """Initializes all the necessary parameters for VF2++ + + Parameters + ---------- + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + Returns + ------- + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2 + G1_labels,G2_labels: dict + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1. + It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_out, T2_out: set + Ti_out contains all the nodes from Gi, that are neither in the mapping + nor in Ti + """ + G1_labels = dict(G1.nodes(data=node_label, default=default_label)) + G2_labels = dict(G2.nodes(data=node_label, default=default_label)) + + graph_params = _GraphParameters( + G1, + G2, + G1_labels, + G2_labels, + nx.utils.groups(G1_labels), + nx.utils.groups(G2_labels), + nx.utils.groups(G2_degree), + ) + + T1, T1_in = set(), set() + T2, T2_in = set(), set() + if G1.is_directed(): + T1_tilde, T1_tilde_in = ( + set(G1.nodes()), + set(), + ) # todo: do we need Ti_tilde_in? What nodes does it have? + T2_tilde, T2_tilde_in = set(G2.nodes()), set() + else: + T1_tilde, T1_tilde_in = set(G1.nodes()), set() + T2_tilde, T2_tilde_in = set(G2.nodes()), set() + + state_params = _StateParameters( + {}, + {}, + T1, + T1_in, + T1_tilde, + T1_tilde_in, + T2, + T2_in, + T2_tilde, + T2_tilde_in, + ) + + return graph_params, state_params + + +def _matching_order(graph_params): + """The node ordering as introduced in VF2++. + + Notes + ----- + Taking into account the structure of the Graph and the node labeling, the + nodes are placed in an order such that, most of the unfruitful/infeasible + branches of the search space can be pruned on high levels, significantly + decreasing the number of visited states. The premise is that, the algorithm + will be able to recognize inconsistencies early, proceeding to go deep into + the search tree only if it's needed. + + Parameters + ---------- + graph_params: namedtuple + Contains: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism. + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively. + + Returns + ------- + node_order: list + The ordering of the nodes. + """ + G1, G2, G1_labels, _, _, nodes_of_G2Labels, _ = graph_params + if not G1 and not G2: + return {} + + if G1.is_directed(): + G1 = G1.to_undirected(as_view=True) + + V1_unordered = set(G1.nodes()) + label_rarity = {label: len(nodes) for label, nodes in nodes_of_G2Labels.items()} + used_degrees = {node: 0 for node in G1} + node_order = [] + + while V1_unordered: + max_rarity = min(label_rarity[G1_labels[x]] for x in V1_unordered) + rarest_nodes = [ + n for n in V1_unordered if label_rarity[G1_labels[n]] == max_rarity + ] + max_node = max(rarest_nodes, key=G1.degree) + + for dlevel_nodes in nx.bfs_layers(G1, max_node): + nodes_to_add = dlevel_nodes.copy() + while nodes_to_add: + max_used_degree = max(used_degrees[n] for n in nodes_to_add) + max_used_degree_nodes = [ + n for n in nodes_to_add if used_degrees[n] == max_used_degree + ] + max_degree = max(G1.degree[n] for n in max_used_degree_nodes) + max_degree_nodes = [ + n for n in max_used_degree_nodes if G1.degree[n] == max_degree + ] + next_node = min( + max_degree_nodes, key=lambda x: label_rarity[G1_labels[x]] + ) + + node_order.append(next_node) + for node in G1.neighbors(next_node): + used_degrees[node] += 1 + + nodes_to_add.remove(next_node) + label_rarity[G1_labels[next_node]] -= 1 + V1_unordered.discard(next_node) + + return node_order + + +def _find_candidates( + u, graph_params, state_params, G1_degree +): # todo: make the 4th argument the degree of u + """Given node u of G1, finds the candidates of u from G2. + + Parameters + ---------- + u: Graph node + The node from G1 for which to find the candidates from G2. + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes + of G1. It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_tilde, T2_tilde: set + Ti_tilde contains all the nodes from Gi, that are neither in the + mapping nor in Ti + + Returns + ------- + candidates: set + The nodes from G2 which are candidates for u. + """ + G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params + mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params + + covered_nbrs = [nbr for nbr in G1[u] if nbr in mapping] + if not covered_nbrs: + candidates = set(nodes_of_G2Labels[G1_labels[u]]) + candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]]) + candidates.intersection_update(T2_tilde) + candidates.difference_update(reverse_mapping) + if G1.is_multigraph(): + candidates.difference_update( + { + node + for node in candidates + if G1.number_of_edges(u, u) != G2.number_of_edges(node, node) + } + ) + return candidates + + nbr1 = covered_nbrs[0] + common_nodes = set(G2[mapping[nbr1]]) + + for nbr1 in covered_nbrs[1:]: + common_nodes.intersection_update(G2[mapping[nbr1]]) + + common_nodes.difference_update(reverse_mapping) + common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]]) + common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]]) + if G1.is_multigraph(): + common_nodes.difference_update( + { + node + for node in common_nodes + if G1.number_of_edges(u, u) != G2.number_of_edges(node, node) + } + ) + return common_nodes + + +def _find_candidates_Di(u, graph_params, state_params, G1_degree): + G1, G2, G1_labels, _, _, nodes_of_G2Labels, G2_nodes_of_degree = graph_params + mapping, reverse_mapping, _, _, _, _, _, _, T2_tilde, _ = state_params + + covered_successors = [succ for succ in G1[u] if succ in mapping] + covered_predecessors = [pred for pred in G1.pred[u] if pred in mapping] + + if not (covered_successors or covered_predecessors): + candidates = set(nodes_of_G2Labels[G1_labels[u]]) + candidates.intersection_update(G2_nodes_of_degree[G1_degree[u]]) + candidates.intersection_update(T2_tilde) + candidates.difference_update(reverse_mapping) + if G1.is_multigraph(): + candidates.difference_update( + { + node + for node in candidates + if G1.number_of_edges(u, u) != G2.number_of_edges(node, node) + } + ) + return candidates + + if covered_successors: + succ1 = covered_successors[0] + common_nodes = set(G2.pred[mapping[succ1]]) + + for succ1 in covered_successors[1:]: + common_nodes.intersection_update(G2.pred[mapping[succ1]]) + else: + pred1 = covered_predecessors.pop() + common_nodes = set(G2[mapping[pred1]]) + + for pred1 in covered_predecessors: + common_nodes.intersection_update(G2[mapping[pred1]]) + + common_nodes.difference_update(reverse_mapping) + common_nodes.intersection_update(G2_nodes_of_degree[G1_degree[u]]) + common_nodes.intersection_update(nodes_of_G2Labels[G1_labels[u]]) + if G1.is_multigraph(): + common_nodes.difference_update( + { + node + for node in common_nodes + if G1.number_of_edges(u, u) != G2.number_of_edges(node, node) + } + ) + return common_nodes + + +def _feasibility(node1, node2, graph_params, state_params): + """Given a candidate pair of nodes u and v from G1 and G2 respectively, + checks if it's feasible to extend the mapping, i.e. if u and v can be matched. + + Notes + ----- + This function performs all the necessary checking by applying both consistency + and cutting rules. + + Parameters + ---------- + node1, node2: Graph node + The candidate pair of nodes being checked for matching + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes + of G1. It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_out, T2_out: set + Ti_out contains all the nodes from Gi, that are neither in the mapping + nor in Ti + + Returns + ------- + True if all checks are successful, False otherwise. + """ + G1 = graph_params.G1 + + if _cut_PT(node1, node2, graph_params, state_params): + return False + + if G1.is_multigraph(): + if not _consistent_PT(node1, node2, graph_params, state_params): + return False + + return True + + +def _cut_PT(u, v, graph_params, state_params): + """Implements the cutting rules for the ISO problem. + + Parameters + ---------- + u, v: Graph node + The two candidate nodes being examined. + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes + of G1. It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_tilde, T2_tilde: set + Ti_out contains all the nodes from Gi, that are neither in the + mapping nor in Ti + + Returns + ------- + True if we should prune this branch, i.e. the node pair failed the cutting checks. False otherwise. + """ + G1, G2, G1_labels, G2_labels, _, _, _ = graph_params + ( + _, + _, + T1, + T1_in, + T1_tilde, + _, + T2, + T2_in, + T2_tilde, + _, + ) = state_params + + u_labels_predecessors, v_labels_predecessors = {}, {} + if G1.is_directed(): + u_labels_predecessors = nx.utils.groups( + {n1: G1_labels[n1] for n1 in G1.pred[u]} + ) + v_labels_predecessors = nx.utils.groups( + {n2: G2_labels[n2] for n2 in G2.pred[v]} + ) + + if set(u_labels_predecessors.keys()) != set(v_labels_predecessors.keys()): + return True + + u_labels_successors = nx.utils.groups({n1: G1_labels[n1] for n1 in G1[u]}) + v_labels_successors = nx.utils.groups({n2: G2_labels[n2] for n2 in G2[v]}) + + # if the neighbors of u, do not have the same labels as those of v, NOT feasible. + if set(u_labels_successors.keys()) != set(v_labels_successors.keys()): + return True + + for label, G1_nbh in u_labels_successors.items(): + G2_nbh = v_labels_successors[label] + + if G1.is_multigraph(): + # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2 + u_nbrs_edges = sorted(G1.number_of_edges(u, x) for x in G1_nbh) + v_nbrs_edges = sorted(G2.number_of_edges(v, x) for x in G2_nbh) + if any( + u_nbr_edges != v_nbr_edges + for u_nbr_edges, v_nbr_edges in zip(u_nbrs_edges, v_nbrs_edges) + ): + return True + + if len(T1.intersection(G1_nbh)) != len(T2.intersection(G2_nbh)): + return True + if len(T1_tilde.intersection(G1_nbh)) != len(T2_tilde.intersection(G2_nbh)): + return True + if G1.is_directed() and len(T1_in.intersection(G1_nbh)) != len( + T2_in.intersection(G2_nbh) + ): + return True + + if not G1.is_directed(): + return False + + for label, G1_pred in u_labels_predecessors.items(): + G2_pred = v_labels_predecessors[label] + + if G1.is_multigraph(): + # Check for every neighbor in the neighborhood, if u-nbr1 has same edges as v-nbr2 + u_pred_edges = sorted(G1.number_of_edges(u, x) for x in G1_pred) + v_pred_edges = sorted(G2.number_of_edges(v, x) for x in G2_pred) + if any( + u_nbr_edges != v_nbr_edges + for u_nbr_edges, v_nbr_edges in zip(u_pred_edges, v_pred_edges) + ): + return True + + if len(T1.intersection(G1_pred)) != len(T2.intersection(G2_pred)): + return True + if len(T1_tilde.intersection(G1_pred)) != len(T2_tilde.intersection(G2_pred)): + return True + if len(T1_in.intersection(G1_pred)) != len(T2_in.intersection(G2_pred)): + return True + + return False + + +def _consistent_PT(u, v, graph_params, state_params): + """Checks the consistency of extending the mapping using the current node pair. + + Parameters + ---------- + u, v: Graph node + The two candidate nodes being examined. + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1. + It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_out, T2_out: set + Ti_out contains all the nodes from Gi, that are neither in the mapping + nor in Ti + + Returns + ------- + True if the pair passes all the consistency checks successfully. False otherwise. + """ + G1, G2 = graph_params.G1, graph_params.G2 + mapping, reverse_mapping = state_params.mapping, state_params.reverse_mapping + + for neighbor in G1[u]: + if neighbor in mapping: + if G1.number_of_edges(u, neighbor) != G2.number_of_edges( + v, mapping[neighbor] + ): + return False + + for neighbor in G2[v]: + if neighbor in reverse_mapping: + if G1.number_of_edges(u, reverse_mapping[neighbor]) != G2.number_of_edges( + v, neighbor + ): + return False + + if not G1.is_directed(): + return True + + for predecessor in G1.pred[u]: + if predecessor in mapping: + if G1.number_of_edges(predecessor, u) != G2.number_of_edges( + mapping[predecessor], v + ): + return False + + for predecessor in G2.pred[v]: + if predecessor in reverse_mapping: + if G1.number_of_edges( + reverse_mapping[predecessor], u + ) != G2.number_of_edges(predecessor, v): + return False + + return True + + +def _update_Tinout(new_node1, new_node2, graph_params, state_params): + """Updates the Ti/Ti_out (i=1,2) when a new node pair u-v is added to the mapping. + + Notes + ----- + This function should be called right after the feasibility checks are passed, + and node1 is mapped to node2. The purpose of this function is to avoid brute + force computing of Ti/Ti_out by iterating over all nodes of the graph and + checking which nodes satisfy the necessary conditions. Instead, in every step + of the algorithm we focus exclusively on the two nodes that are being added + to the mapping, incrementally updating Ti/Ti_out. + + Parameters + ---------- + new_node1, new_node2: Graph node + The two new nodes, added to the mapping. + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1. + It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_tilde, T2_tilde: set + Ti_out contains all the nodes from Gi, that are neither in the mapping nor in Ti + """ + G1, G2, _, _, _, _, _ = graph_params + ( + mapping, + reverse_mapping, + T1, + T1_in, + T1_tilde, + T1_tilde_in, + T2, + T2_in, + T2_tilde, + T2_tilde_in, + ) = state_params + + uncovered_successors_G1 = {succ for succ in G1[new_node1] if succ not in mapping} + uncovered_successors_G2 = { + succ for succ in G2[new_node2] if succ not in reverse_mapping + } + + # Add the uncovered neighbors of node1 and node2 in T1 and T2 respectively + T1.update(uncovered_successors_G1) + T2.update(uncovered_successors_G2) + T1.discard(new_node1) + T2.discard(new_node2) + + T1_tilde.difference_update(uncovered_successors_G1) + T2_tilde.difference_update(uncovered_successors_G2) + T1_tilde.discard(new_node1) + T2_tilde.discard(new_node2) + + if not G1.is_directed(): + return + + uncovered_predecessors_G1 = { + pred for pred in G1.pred[new_node1] if pred not in mapping + } + uncovered_predecessors_G2 = { + pred for pred in G2.pred[new_node2] if pred not in reverse_mapping + } + + T1_in.update(uncovered_predecessors_G1) + T2_in.update(uncovered_predecessors_G2) + T1_in.discard(new_node1) + T2_in.discard(new_node2) + + T1_tilde.difference_update(uncovered_predecessors_G1) + T2_tilde.difference_update(uncovered_predecessors_G2) + T1_tilde.discard(new_node1) + T2_tilde.discard(new_node2) + + +def _restore_Tinout(popped_node1, popped_node2, graph_params, state_params): + """Restores the previous version of Ti/Ti_out when a node pair is deleted + from the mapping. + + Parameters + ---------- + popped_node1, popped_node2: Graph node + The two nodes deleted from the mapping. + + graph_params: namedtuple + Contains all the Graph-related parameters: + + G1,G2: NetworkX Graph or MultiGraph instances. + The two graphs to check for isomorphism or monomorphism + + G1_labels,G2_labels: dict + The label of every node in G1 and G2 respectively + + state_params: namedtuple + Contains all the State-related parameters: + + mapping: dict + The mapping as extended so far. Maps nodes of G1 to nodes of G2 + + reverse_mapping: dict + The reverse mapping as extended so far. Maps nodes from G2 to nodes of G1. + It's basically "mapping" reversed + + T1, T2: set + Ti contains uncovered neighbors of covered nodes from Gi, i.e. nodes + that are not in the mapping, but are neighbors of nodes that are. + + T1_tilde, T2_tilde: set + Ti_out contains all the nodes from Gi, that are neither in the mapping + nor in Ti + """ + # If the node we want to remove from the mapping, has at least one covered + # neighbor, add it to T1. + G1, G2, _, _, _, _, _ = graph_params + ( + mapping, + reverse_mapping, + T1, + T1_in, + T1_tilde, + T1_tilde_in, + T2, + T2_in, + T2_tilde, + T2_tilde_in, + ) = state_params + + is_added = False + for neighbor in G1[popped_node1]: + if neighbor in mapping: + # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1 + is_added = True + T1.add(popped_node1) + else: + # check if its neighbor has another connection with a covered node. + # If not, only then exclude it from T1 + if any(nbr in mapping for nbr in G1[neighbor]): + continue + T1.discard(neighbor) + T1_tilde.add(neighbor) + + # Case where the node is not present in neither the mapping nor T1. + # By definition, it should belong to T1_tilde + if not is_added: + T1_tilde.add(popped_node1) + + is_added = False + for neighbor in G2[popped_node2]: + if neighbor in reverse_mapping: + is_added = True + T2.add(popped_node2) + else: + if any(nbr in reverse_mapping for nbr in G2[neighbor]): + continue + T2.discard(neighbor) + T2_tilde.add(neighbor) + + if not is_added: + T2_tilde.add(popped_node2) + + +def _restore_Tinout_Di(popped_node1, popped_node2, graph_params, state_params): + # If the node we want to remove from the mapping, has at least one covered neighbor, add it to T1. + G1, G2, _, _, _, _, _ = graph_params + ( + mapping, + reverse_mapping, + T1, + T1_in, + T1_tilde, + T1_tilde_in, + T2, + T2_in, + T2_tilde, + T2_tilde_in, + ) = state_params + + is_added = False + for successor in G1[popped_node1]: + if successor in mapping: + # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1 + is_added = True + T1_in.add(popped_node1) + else: + # check if its neighbor has another connection with a covered node. + # If not, only then exclude it from T1 + if not any(pred in mapping for pred in G1.pred[successor]): + T1.discard(successor) + + if not any(succ in mapping for succ in G1[successor]): + T1_in.discard(successor) + + if successor not in T1: + if successor not in T1_in: + T1_tilde.add(successor) + + for predecessor in G1.pred[popped_node1]: + if predecessor in mapping: + # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1 + is_added = True + T1.add(popped_node1) + else: + # check if its neighbor has another connection with a covered node. + # If not, only then exclude it from T1 + if not any(pred in mapping for pred in G1.pred[predecessor]): + T1.discard(predecessor) + + if not any(succ in mapping for succ in G1[predecessor]): + T1_in.discard(predecessor) + + if not (predecessor in T1 or predecessor in T1_in): + T1_tilde.add(predecessor) + + # Case where the node is not present in neither the mapping nor T1. + # By definition it should belong to T1_tilde + if not is_added: + T1_tilde.add(popped_node1) + + is_added = False + for successor in G2[popped_node2]: + if successor in reverse_mapping: + is_added = True + T2_in.add(popped_node2) + else: + if not any(pred in reverse_mapping for pred in G2.pred[successor]): + T2.discard(successor) + + if not any(succ in reverse_mapping for succ in G2[successor]): + T2_in.discard(successor) + + if successor not in T2: + if successor not in T2_in: + T2_tilde.add(successor) + + for predecessor in G2.pred[popped_node2]: + if predecessor in reverse_mapping: + # if a neighbor of the excluded node1 is in the mapping, keep node1 in T1 + is_added = True + T2.add(popped_node2) + else: + # check if its neighbor has another connection with a covered node. + # If not, only then exclude it from T1 + if not any(pred in reverse_mapping for pred in G2.pred[predecessor]): + T2.discard(predecessor) + + if not any(succ in reverse_mapping for succ in G2[predecessor]): + T2_in.discard(predecessor) + + if not (predecessor in T2 or predecessor in T2_in): + T2_tilde.add(predecessor) + + if not is_added: + T2_tilde.add(popped_node2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py new file mode 100644 index 0000000000000000000000000000000000000000..6fcf8a15f6ec0ef517d225a9d0095cfe5dc26ab2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py @@ -0,0 +1,192 @@ +""" +Module to simplify the specification of user-defined equality functions for +node and edge attributes during isomorphism checks. + +During the construction of an isomorphism, the algorithm considers two +candidate nodes n1 in G1 and n2 in G2. The graphs G1 and G2 are then +compared with respect to properties involving n1 and n2, and if the outcome +is good, then the candidate nodes are considered isomorphic. NetworkX +provides a simple mechanism for users to extend the comparisons to include +node and edge attributes. + +Node attributes are handled by the node_match keyword. When considering +n1 and n2, the algorithm passes their node attribute dictionaries to +node_match, and if it returns False, then n1 and n2 cannot be +considered to be isomorphic. + +Edge attributes are handled by the edge_match keyword. When considering +n1 and n2, the algorithm must verify that outgoing edges from n1 are +commensurate with the outgoing edges for n2. If the graph is directed, +then a similar check is also performed for incoming edges. + +Focusing only on outgoing edges, we consider pairs of nodes (n1, v1) from +G1 and (n2, v2) from G2. For graphs and digraphs, there is only one edge +between (n1, v1) and only one edge between (n2, v2). Those edge attribute +dictionaries are passed to edge_match, and if it returns False, then +n1 and n2 cannot be considered isomorphic. For multigraphs and +multidigraphs, there can be multiple edges between (n1, v1) and also +multiple edges between (n2, v2). Now, there must exist an isomorphism +from "all the edges between (n1, v1)" to "all the edges between (n2, v2)". +So, all of the edge attribute dictionaries are passed to edge_match, and +it must determine if there is an isomorphism between the two sets of edges. +""" + +from . import isomorphvf2 as vf2 + +__all__ = ["GraphMatcher", "DiGraphMatcher", "MultiGraphMatcher", "MultiDiGraphMatcher"] + + +def _semantic_feasibility(self, G1_node, G2_node): + """Returns True if mapping G1_node to G2_node is semantically feasible.""" + # Make sure the nodes match + if self.node_match is not None: + nm = self.node_match(self.G1.nodes[G1_node], self.G2.nodes[G2_node]) + if not nm: + return False + + # Make sure the edges match + if self.edge_match is not None: + # Cached lookups + G1nbrs = self.G1_adj[G1_node] + G2nbrs = self.G2_adj[G2_node] + core_1 = self.core_1 + edge_match = self.edge_match + + for neighbor in G1nbrs: + # G1_node is not in core_1, so we must handle R_self separately + if neighbor == G1_node: + if G2_node in G2nbrs and not edge_match( + G1nbrs[G1_node], G2nbrs[G2_node] + ): + return False + elif neighbor in core_1: + G2_nbr = core_1[neighbor] + if G2_nbr in G2nbrs and not edge_match( + G1nbrs[neighbor], G2nbrs[G2_nbr] + ): + return False + # syntactic check has already verified that neighbors are symmetric + + return True + + +class GraphMatcher(vf2.GraphMatcher): + """VF2 isomorphism checker for undirected graphs.""" + + def __init__(self, G1, G2, node_match=None, edge_match=None): + """Initialize graph matcher. + + Parameters + ---------- + G1, G2: graph + The graphs to be tested. + + node_match: callable + A function that returns True iff node n1 in G1 and n2 in G2 + should be considered equal during the isomorphism test. The + function will be called like:: + + node_match(G1.nodes[n1], G2.nodes[n2]) + + That is, the function will receive the node attribute dictionaries + of the nodes under consideration. If None, then no attributes are + considered when testing for an isomorphism. + + edge_match: callable + A function that returns True iff the edge attribute dictionary for + the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be + considered equal during the isomorphism test. The function will be + called like:: + + edge_match(G1[u1][v1], G2[u2][v2]) + + That is, the function will receive the edge attribute dictionaries + of the edges under consideration. If None, then no attributes are + considered when testing for an isomorphism. + + """ + vf2.GraphMatcher.__init__(self, G1, G2) + + self.node_match = node_match + self.edge_match = edge_match + + # These will be modified during checks to minimize code repeat. + self.G1_adj = self.G1.adj + self.G2_adj = self.G2.adj + + semantic_feasibility = _semantic_feasibility + + +class DiGraphMatcher(vf2.DiGraphMatcher): + """VF2 isomorphism checker for directed graphs.""" + + def __init__(self, G1, G2, node_match=None, edge_match=None): + """Initialize graph matcher. + + Parameters + ---------- + G1, G2 : graph + The graphs to be tested. + + node_match : callable + A function that returns True iff node n1 in G1 and n2 in G2 + should be considered equal during the isomorphism test. The + function will be called like:: + + node_match(G1.nodes[n1], G2.nodes[n2]) + + That is, the function will receive the node attribute dictionaries + of the nodes under consideration. If None, then no attributes are + considered when testing for an isomorphism. + + edge_match : callable + A function that returns True iff the edge attribute dictionary for + the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should be + considered equal during the isomorphism test. The function will be + called like:: + + edge_match(G1[u1][v1], G2[u2][v2]) + + That is, the function will receive the edge attribute dictionaries + of the edges under consideration. If None, then no attributes are + considered when testing for an isomorphism. + + """ + vf2.DiGraphMatcher.__init__(self, G1, G2) + + self.node_match = node_match + self.edge_match = edge_match + + # These will be modified during checks to minimize code repeat. + self.G1_adj = self.G1.adj + self.G2_adj = self.G2.adj + + def semantic_feasibility(self, G1_node, G2_node): + """Returns True if mapping G1_node to G2_node is semantically feasible.""" + + # Test node_match and also test edge_match on successors + feasible = _semantic_feasibility(self, G1_node, G2_node) + if not feasible: + return False + + # Test edge_match on predecessors + self.G1_adj = self.G1.pred + self.G2_adj = self.G2.pred + feasible = _semantic_feasibility(self, G1_node, G2_node) + self.G1_adj = self.G1.adj + self.G2_adj = self.G2.adj + + return feasible + + +# The "semantics" of edge_match are different for multi(di)graphs, but +# the implementation is the same. So, technically we do not need to +# provide "multi" versions, but we do so to match NetworkX's base classes. + + +class MultiGraphMatcher(GraphMatcher): + """VF2 isomorphism checker for undirected multigraphs.""" + + +class MultiDiGraphMatcher(DiGraphMatcher): + """VF2 isomorphism checker for directed multigraphs.""" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6009f000814753ab436278e2d2cc38e961e80f3f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/__init__.py @@ -0,0 +1,2 @@ +from networkx.algorithms.link_analysis.hits_alg import * +from networkx.algorithms.link_analysis.pagerank_alg import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/hits_alg.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/hits_alg.py new file mode 100644 index 0000000000000000000000000000000000000000..3a292f9b2b755d0711f6f8d760f3036736864a74 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/hits_alg.py @@ -0,0 +1,337 @@ +"""Hubs and authorities analysis of graph structure.""" + +import networkx as nx + +__all__ = ["hits"] + + +@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}}) +def hits(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True): + """Returns HITS hubs and authorities values for nodes. + + The HITS algorithm computes two numbers for a node. + Authorities estimates the node value based on the incoming links. + Hubs estimates the node value based on outgoing links. + + Parameters + ---------- + G : graph + A NetworkX graph + + max_iter : integer, optional + Maximum number of iterations in power method. + + tol : float, optional + Error tolerance used to check convergence in power method iteration. + + nstart : dictionary, optional + Starting value of each node for power method iteration. + + normalized : bool (default=True) + Normalize results by the sum of all of the values. + + Returns + ------- + (hubs,authorities) : two-tuple of dictionaries + Two dictionaries keyed by node containing the hub and authority + values. + + Raises + ------ + PowerIterationFailedConvergence + If the algorithm fails to converge to the specified tolerance + within the specified number of iterations of the power iteration + method. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> h, a = nx.hits(G) + + Notes + ----- + The eigenvector calculation is done by the power iteration method + and has no guarantee of convergence. The iteration will stop + after max_iter iterations or an error tolerance of + number_of_nodes(G)*tol has been reached. + + The HITS algorithm was designed for directed graphs but this + algorithm does not check if the input graph is directed and will + execute on undirected graphs. + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + https://epubs.siam.org/doi/epdf/10.1137/S0036144503424786 + .. [2] Jon Kleinberg, + Authoritative sources in a hyperlinked environment + Journal of the ACM 46 (5): 604-32, 1999. + https://www.cs.cornell.edu/home/kleinber/auth.pdf + doi:10.1145/324133.324140. + """ + import numpy as np + import scipy as sp + + if len(G) == 0: + return {}, {} + A = nx.adjacency_matrix(G, nodelist=list(G), dtype=float) + + if nstart is not None: + nstart = np.array(list(nstart.values())) + if max_iter <= 0: + raise nx.PowerIterationFailedConvergence(max_iter) + try: + _, _, vt = sp.sparse.linalg.svds(A, k=1, v0=nstart, maxiter=max_iter, tol=tol) + except sp.sparse.linalg.ArpackNoConvergence as exc: + raise nx.PowerIterationFailedConvergence(max_iter) from exc + + a = vt.flatten().real + h = A @ a + if normalized: + h /= h.sum() + a /= a.sum() + hubs = dict(zip(G, map(float, h))) + authorities = dict(zip(G, map(float, a))) + return hubs, authorities + + +def _hits_python(G, max_iter=100, tol=1.0e-8, nstart=None, normalized=True): + if isinstance(G, nx.MultiGraph | nx.MultiDiGraph): + raise Exception("hits() not defined for graphs with multiedges.") + if len(G) == 0: + return {}, {} + # choose fixed starting vector if not given + if nstart is None: + h = dict.fromkeys(G, 1.0 / G.number_of_nodes()) + else: + h = nstart + # normalize starting vector + s = 1.0 / sum(h.values()) + for k in h: + h[k] *= s + for _ in range(max_iter): # power iteration: make up to max_iter iterations + hlast = h + h = dict.fromkeys(hlast.keys(), 0) + a = dict.fromkeys(hlast.keys(), 0) + # this "matrix multiply" looks odd because it is + # doing a left multiply a^T=hlast^T*G + for n in h: + for nbr in G[n]: + a[nbr] += hlast[n] * G[n][nbr].get("weight", 1) + # now multiply h=Ga + for n in h: + for nbr in G[n]: + h[n] += a[nbr] * G[n][nbr].get("weight", 1) + # normalize vector + s = 1.0 / max(h.values()) + for n in h: + h[n] *= s + # normalize vector + s = 1.0 / max(a.values()) + for n in a: + a[n] *= s + # check convergence, l1 norm + err = sum(abs(h[n] - hlast[n]) for n in h) + if err < tol: + break + else: + raise nx.PowerIterationFailedConvergence(max_iter) + if normalized: + s = 1.0 / sum(a.values()) + for n in a: + a[n] *= s + s = 1.0 / sum(h.values()) + for n in h: + h[n] *= s + return h, a + + +def _hits_numpy(G, normalized=True): + """Returns HITS hubs and authorities values for nodes. + + The HITS algorithm computes two numbers for a node. + Authorities estimates the node value based on the incoming links. + Hubs estimates the node value based on outgoing links. + + Parameters + ---------- + G : graph + A NetworkX graph + + normalized : bool (default=True) + Normalize results by the sum of all of the values. + + Returns + ------- + (hubs,authorities) : two-tuple of dictionaries + Two dictionaries keyed by node containing the hub and authority + values. + + Examples + -------- + >>> G = nx.path_graph(4) + + The `hubs` and `authorities` are given by the eigenvectors corresponding to the + maximum eigenvalues of the hubs_matrix and the authority_matrix, respectively. + + The ``hubs`` and ``authority`` matrices are computed from the adjacency + matrix: + + >>> adj_ary = nx.to_numpy_array(G) + >>> hubs_matrix = adj_ary @ adj_ary.T + >>> authority_matrix = adj_ary.T @ adj_ary + + `_hits_numpy` maps the eigenvector corresponding to the maximum eigenvalue + of the respective matrices to the nodes in `G`: + + >>> from networkx.algorithms.link_analysis.hits_alg import _hits_numpy + >>> hubs, authority = _hits_numpy(G) + + Notes + ----- + The eigenvector calculation uses NumPy's interface to LAPACK. + + The HITS algorithm was designed for directed graphs but this + algorithm does not check if the input graph is directed and will + execute on undirected graphs. + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + http://citeseer.ist.psu.edu/713792.html + .. [2] Jon Kleinberg, + Authoritative sources in a hyperlinked environment + Journal of the ACM 46 (5): 604-32, 1999. + doi:10.1145/324133.324140. + http://www.cs.cornell.edu/home/kleinber/auth.pdf. + """ + import numpy as np + + if len(G) == 0: + return {}, {} + adj_ary = nx.to_numpy_array(G) + # Hub matrix + H = adj_ary @ adj_ary.T + e, ev = np.linalg.eig(H) + h = ev[:, np.argmax(e)] # eigenvector corresponding to the maximum eigenvalue + # Authority matrix + A = adj_ary.T @ adj_ary + e, ev = np.linalg.eig(A) + a = ev[:, np.argmax(e)] # eigenvector corresponding to the maximum eigenvalue + if normalized: + h /= h.sum() + a /= a.sum() + else: + h /= h.max() + a /= a.max() + hubs = dict(zip(G, map(float, h))) + authorities = dict(zip(G, map(float, a))) + return hubs, authorities + + +def _hits_scipy(G, max_iter=100, tol=1.0e-6, nstart=None, normalized=True): + """Returns HITS hubs and authorities values for nodes. + + + The HITS algorithm computes two numbers for a node. + Authorities estimates the node value based on the incoming links. + Hubs estimates the node value based on outgoing links. + + Parameters + ---------- + G : graph + A NetworkX graph + + max_iter : integer, optional + Maximum number of iterations in power method. + + tol : float, optional + Error tolerance used to check convergence in power method iteration. + + nstart : dictionary, optional + Starting value of each node for power method iteration. + + normalized : bool (default=True) + Normalize results by the sum of all of the values. + + Returns + ------- + (hubs,authorities) : two-tuple of dictionaries + Two dictionaries keyed by node containing the hub and authority + values. + + Examples + -------- + >>> from networkx.algorithms.link_analysis.hits_alg import _hits_scipy + >>> G = nx.path_graph(4) + >>> h, a = _hits_scipy(G) + + Notes + ----- + This implementation uses SciPy sparse matrices. + + The eigenvector calculation is done by the power iteration method + and has no guarantee of convergence. The iteration will stop + after max_iter iterations or an error tolerance of + number_of_nodes(G)*tol has been reached. + + The HITS algorithm was designed for directed graphs but this + algorithm does not check if the input graph is directed and will + execute on undirected graphs. + + Raises + ------ + PowerIterationFailedConvergence + If the algorithm fails to converge to the specified tolerance + within the specified number of iterations of the power iteration + method. + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + http://citeseer.ist.psu.edu/713792.html + .. [2] Jon Kleinberg, + Authoritative sources in a hyperlinked environment + Journal of the ACM 46 (5): 604-632, 1999. + doi:10.1145/324133.324140. + http://www.cs.cornell.edu/home/kleinber/auth.pdf. + """ + import numpy as np + + if len(G) == 0: + return {}, {} + A = nx.to_scipy_sparse_array(G, nodelist=list(G)) + (n, _) = A.shape # should be square + ATA = A.T @ A # authority matrix + # choose fixed starting vector if not given + if nstart is None: + x = np.ones((n, 1)) / n + else: + x = np.array([nstart.get(n, 0) for n in list(G)], dtype=float) + x /= x.sum() + + # power iteration on authority matrix + i = 0 + while True: + xlast = x + x = ATA @ x + x /= x.max() + # check convergence, l1 norm + err = np.absolute(x - xlast).sum() + if err < tol: + break + if i > max_iter: + raise nx.PowerIterationFailedConvergence(max_iter) + i += 1 + + a = x.flatten() + h = A @ a + if normalized: + h /= h.sum() + a /= a.sum() + hubs = dict(zip(G, map(float, h))) + authorities = dict(zip(G, map(float, a))) + return hubs, authorities diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py new file mode 100644 index 0000000000000000000000000000000000000000..220d17350113e0e11b80f638cf29bfb5afdc7759 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py @@ -0,0 +1,498 @@ +"""PageRank analysis of graph structure.""" + +import networkx as nx + +__all__ = ["pagerank", "google_matrix"] + + +@nx._dispatchable(edge_attrs="weight") +def pagerank( + G, + alpha=0.85, + personalization=None, + max_iter=100, + tol=1.0e-6, + nstart=None, + weight="weight", + dangling=None, +): + """Returns the PageRank of the nodes in the graph. + + PageRank computes a ranking of the nodes in the graph G based on + the structure of the incoming links. It was originally designed as + an algorithm to rank web pages. + + Parameters + ---------- + G : graph + A NetworkX graph. Undirected graphs will be converted to a directed + graph with two directed edges for each undirected edge. + + alpha : float, optional + Damping parameter for PageRank, default=0.85. + + personalization: dict, optional + The "personalization vector" consisting of a dictionary with a + key some subset of graph nodes and personalization value each of those. + At least one personalization value must be non-zero. + If not specified, a nodes personalization value will be zero. + By default, a uniform distribution is used. + + max_iter : integer, optional + Maximum number of iterations in power method eigenvalue solver. + + tol : float, optional + Error tolerance used to check convergence in power method solver. + The iteration will stop after a tolerance of ``len(G) * tol`` is reached. + + nstart : dictionary, optional + Starting value of PageRank iteration for each node. + + weight : key, optional + Edge data key to use as weight. If None weights are set to 1. + + dangling: dict, optional + The outedges to be assigned to any "dangling" nodes, i.e., nodes without + any outedges. The dict key is the node the outedge points to and the dict + value is the weight of that outedge. By default, dangling nodes are given + outedges according to the personalization vector (uniform if not + specified). This must be selected to result in an irreducible transition + matrix (see notes under google_matrix). It may be common to have the + dangling dict to be the same as the personalization dict. + + + Returns + ------- + pagerank : dictionary + Dictionary of nodes with PageRank as value + + Examples + -------- + >>> G = nx.DiGraph(nx.path_graph(4)) + >>> pr = nx.pagerank(G, alpha=0.9) + + Notes + ----- + The eigenvector calculation is done by the power iteration method + and has no guarantee of convergence. The iteration will stop after + an error tolerance of ``len(G) * tol`` has been reached. If the + number of iterations exceed `max_iter`, a + :exc:`networkx.exception.PowerIterationFailedConvergence` exception + is raised. + + The PageRank algorithm was designed for directed graphs but this + algorithm does not check if the input graph is directed and will + execute on undirected graphs by converting each edge in the + directed graph to two edges. + + See Also + -------- + google_matrix + :func:`~networkx.algorithms.bipartite.link_analysis.birank` + + Raises + ------ + PowerIterationFailedConvergence + If the algorithm fails to converge to the specified tolerance + within the specified number of iterations of the power iteration + method. + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + http://citeseer.ist.psu.edu/713792.html + .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry, + The PageRank citation ranking: Bringing order to the Web. 1999 + http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf + + """ + return _pagerank_scipy( + G, alpha, personalization, max_iter, tol, nstart, weight, dangling + ) + + +def _pagerank_python( + G, + alpha=0.85, + personalization=None, + max_iter=100, + tol=1.0e-6, + nstart=None, + weight="weight", + dangling=None, +): + if len(G) == 0: + return {} + + D = G.to_directed() + + # Create a copy in (right) stochastic form + W = nx.stochastic_graph(D, weight=weight) + N = W.number_of_nodes() + + # Choose fixed starting vector if not given + if nstart is None: + x = dict.fromkeys(W, 1.0 / N) + else: + # Normalized nstart vector + s = sum(nstart.values()) + x = {k: v / s for k, v in nstart.items()} + + if personalization is None: + # Assign uniform personalization vector if not given + p = dict.fromkeys(W, 1.0 / N) + else: + s = sum(personalization.values()) + p = {k: v / s for k, v in personalization.items()} + + if dangling is None: + # Use personalization vector if dangling vector not specified + dangling_weights = p + else: + s = sum(dangling.values()) + dangling_weights = {k: v / s for k, v in dangling.items()} + dangling_nodes = [n for n in W if W.out_degree(n, weight=weight) == 0.0] + + # power iteration: make up to max_iter iterations + for _ in range(max_iter): + xlast = x + x = dict.fromkeys(xlast.keys(), 0) + danglesum = alpha * sum(xlast[n] for n in dangling_nodes) + for n in x: + # this matrix multiply looks odd because it is + # doing a left multiply x^T=xlast^T*W + for _, nbr, wt in W.edges(n, data=weight): + x[nbr] += alpha * xlast[n] * wt + x[n] += danglesum * dangling_weights.get(n, 0) + (1.0 - alpha) * p.get(n, 0) + # check convergence, l1 norm + err = sum(abs(x[n] - xlast[n]) for n in x) + if err < N * tol: + return x + raise nx.PowerIterationFailedConvergence(max_iter) + + +@nx._dispatchable(edge_attrs="weight") +def google_matrix( + G, alpha=0.85, personalization=None, nodelist=None, weight="weight", dangling=None +): + """Returns the Google matrix of the graph. + + Parameters + ---------- + G : graph + A NetworkX graph. Undirected graphs will be converted to a directed + graph with two directed edges for each undirected edge. + + alpha : float + The damping factor. + + personalization: dict, optional + The "personalization vector" consisting of a dictionary with a + key some subset of graph nodes and personalization value each of those. + At least one personalization value must be non-zero. + If not specified, a nodes personalization value will be zero. + By default, a uniform distribution is used. + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : key, optional + Edge data key to use as weight. If None weights are set to 1. + + dangling: dict, optional + The outedges to be assigned to any "dangling" nodes, i.e., nodes without + any outedges. The dict key is the node the outedge points to and the dict + value is the weight of that outedge. By default, dangling nodes are given + outedges according to the personalization vector (uniform if not + specified) This must be selected to result in an irreducible transition + matrix (see notes below). It may be common to have the dangling dict to + be the same as the personalization dict. + + Returns + ------- + A : 2D NumPy ndarray + Google matrix of the graph + + Notes + ----- + The array returned represents the transition matrix that describes the + Markov chain used in PageRank. For PageRank to converge to a unique + solution (i.e., a unique stationary distribution in a Markov chain), the + transition matrix must be irreducible. In other words, it must be that + there exists a path between every pair of nodes in the graph, or else there + is the potential of "rank sinks." + + This implementation works with Multi(Di)Graphs. For multigraphs the + weight between two nodes is set to be the sum of all edge weights + between those nodes. + + See Also + -------- + pagerank + """ + import numpy as np + + if nodelist is None: + nodelist = list(G) + + A = nx.to_numpy_array(G, nodelist=nodelist, weight=weight) + N = len(G) + if N == 0: + return A + + # Personalization vector + if personalization is None: + p = np.repeat(1.0 / N, N) + else: + p = np.array([personalization.get(n, 0) for n in nodelist], dtype=float) + if p.sum() == 0: + raise ZeroDivisionError + p /= p.sum() + + # Dangling nodes + if dangling is None: + dangling_weights = p + else: + # Convert the dangling dictionary into an array in nodelist order + dangling_weights = np.array([dangling.get(n, 0) for n in nodelist], dtype=float) + dangling_weights /= dangling_weights.sum() + dangling_nodes = np.where(A.sum(axis=1) == 0)[0] + + # Assign dangling_weights to any dangling nodes (nodes with no out links) + A[dangling_nodes] = dangling_weights + + A /= A.sum(axis=1)[:, np.newaxis] # Normalize rows to sum to 1 + + return alpha * A + (1 - alpha) * p + + +def _pagerank_numpy( + G, alpha=0.85, personalization=None, weight="weight", dangling=None +): + """Returns the PageRank of the nodes in the graph. + + PageRank computes a ranking of the nodes in the graph G based on + the structure of the incoming links. It was originally designed as + an algorithm to rank web pages. + + Parameters + ---------- + G : graph + A NetworkX graph. Undirected graphs will be converted to a directed + graph with two directed edges for each undirected edge. + + alpha : float, optional + Damping parameter for PageRank, default=0.85. + + personalization: dict, optional + The "personalization vector" consisting of a dictionary with a + key some subset of graph nodes and personalization value each of those. + At least one personalization value must be non-zero. + If not specified, a nodes personalization value will be zero. + By default, a uniform distribution is used. + + weight : key, optional + Edge data key to use as weight. If None weights are set to 1. + + dangling: dict, optional + The outedges to be assigned to any "dangling" nodes, i.e., nodes without + any outedges. The dict key is the node the outedge points to and the dict + value is the weight of that outedge. By default, dangling nodes are given + outedges according to the personalization vector (uniform if not + specified) This must be selected to result in an irreducible transition + matrix (see notes under google_matrix). It may be common to have the + dangling dict to be the same as the personalization dict. + + Returns + ------- + pagerank : dictionary + Dictionary of nodes with PageRank as value. + + Examples + -------- + >>> from networkx.algorithms.link_analysis.pagerank_alg import _pagerank_numpy + >>> G = nx.DiGraph(nx.path_graph(4)) + >>> pr = _pagerank_numpy(G, alpha=0.9) + + Notes + ----- + The eigenvector calculation uses NumPy's interface to the LAPACK + eigenvalue solvers. This will be the fastest and most accurate + for small graphs. + + This implementation works with Multi(Di)Graphs. For multigraphs the + weight between two nodes is set to be the sum of all edge weights + between those nodes. + + See Also + -------- + pagerank, google_matrix + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + http://citeseer.ist.psu.edu/713792.html + .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry, + The PageRank citation ranking: Bringing order to the Web. 1999 + http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf + """ + import numpy as np + + if len(G) == 0: + return {} + M = google_matrix( + G, alpha, personalization=personalization, weight=weight, dangling=dangling + ) + # use numpy LAPACK solver + eigenvalues, eigenvectors = np.linalg.eig(M.T) + ind = np.argmax(eigenvalues) + # eigenvector of largest eigenvalue is at ind, normalized + largest = np.array(eigenvectors[:, ind]).flatten().real + norm = largest.sum() + return dict(zip(G, map(float, largest / norm))) + + +def _pagerank_scipy( + G, + alpha=0.85, + personalization=None, + max_iter=100, + tol=1.0e-6, + nstart=None, + weight="weight", + dangling=None, +): + """Returns the PageRank of the nodes in the graph. + + PageRank computes a ranking of the nodes in the graph G based on + the structure of the incoming links. It was originally designed as + an algorithm to rank web pages. + + Parameters + ---------- + G : graph + A NetworkX graph. Undirected graphs will be converted to a directed + graph with two directed edges for each undirected edge. + + alpha : float, optional + Damping parameter for PageRank, default=0.85. + + personalization: dict, optional + The "personalization vector" consisting of a dictionary with a + key some subset of graph nodes and personalization value each of those. + At least one personalization value must be non-zero. + If not specified, a nodes personalization value will be zero. + By default, a uniform distribution is used. + + max_iter : integer, optional + Maximum number of iterations in power method eigenvalue solver. + + tol : float, optional + Error tolerance used to check convergence in power method solver. + The iteration will stop after a tolerance of ``len(G) * tol`` is reached. + + nstart : dictionary, optional + Starting value of PageRank iteration for each node. + + weight : key, optional + Edge data key to use as weight. If None weights are set to 1. + + dangling: dict, optional + The outedges to be assigned to any "dangling" nodes, i.e., nodes without + any outedges. The dict key is the node the outedge points to and the dict + value is the weight of that outedge. By default, dangling nodes are given + outedges according to the personalization vector (uniform if not + specified) This must be selected to result in an irreducible transition + matrix (see notes under google_matrix). It may be common to have the + dangling dict to be the same as the personalization dict. + + Returns + ------- + pagerank : dictionary + Dictionary of nodes with PageRank as value + + Examples + -------- + >>> from networkx.algorithms.link_analysis.pagerank_alg import _pagerank_scipy + >>> G = nx.DiGraph(nx.path_graph(4)) + >>> pr = _pagerank_scipy(G, alpha=0.9) + + Notes + ----- + The eigenvector calculation uses power iteration with a SciPy + sparse matrix representation. + + This implementation works with Multi(Di)Graphs. For multigraphs the + weight between two nodes is set to be the sum of all edge weights + between those nodes. + + See Also + -------- + pagerank + + Raises + ------ + PowerIterationFailedConvergence + If the algorithm fails to converge to the specified tolerance + within the specified number of iterations of the power iteration + method. + + References + ---------- + .. [1] A. Langville and C. Meyer, + "A survey of eigenvector methods of web information retrieval." + http://citeseer.ist.psu.edu/713792.html + .. [2] Page, Lawrence; Brin, Sergey; Motwani, Rajeev and Winograd, Terry, + The PageRank citation ranking: Bringing order to the Web. 1999 + http://dbpubs.stanford.edu:8090/pub/showDoc.Fulltext?lang=en&doc=1999-66&format=pdf + """ + import numpy as np + import scipy as sp + + N = len(G) + if N == 0: + return {} + + nodelist = list(G) + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float) + S = A.sum(axis=1) + S[S != 0] = 1.0 / S[S != 0] + Q = sp.sparse.dia_array((S.T, 0), shape=A.shape).tocsr() + A = Q @ A + + # initial vector + if nstart is None: + x = np.repeat(1.0 / N, N) + else: + x = np.array([nstart.get(n, 0) for n in nodelist], dtype=float) + x /= x.sum() + + # Personalization vector + if personalization is None: + p = np.repeat(1.0 / N, N) + else: + p = np.array([personalization.get(n, 0) for n in nodelist], dtype=float) + if p.sum() == 0: + raise ZeroDivisionError + p /= p.sum() + # Dangling nodes + if dangling is None: + dangling_weights = p + else: + # Convert the dangling dictionary into an array in nodelist order + dangling_weights = np.array([dangling.get(n, 0) for n in nodelist], dtype=float) + dangling_weights /= dangling_weights.sum() + is_dangling = np.where(S == 0)[0] + + # power iteration: make up to max_iter iterations + for _ in range(max_iter): + xlast = x + x = alpha * (x @ A + sum(x[is_dangling]) * dangling_weights) + (1 - alpha) * p + # check convergence, l1 norm + err = np.absolute(x - xlast).sum() + if err < N * tol: + return dict(zip(nodelist, map(float, x))) + raise nx.PowerIterationFailedConvergence(max_iter) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_prediction.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..3615f26deb6d3c2f3c01e55f3fcf8ca3361968b3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/link_prediction.py @@ -0,0 +1,687 @@ +""" +Link prediction algorithms. +""" + +from math import log + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "resource_allocation_index", + "jaccard_coefficient", + "adamic_adar_index", + "preferential_attachment", + "cn_soundarajan_hopcroft", + "ra_index_soundarajan_hopcroft", + "within_inter_cluster", + "common_neighbor_centrality", +] + + +def _apply_prediction(G, func, ebunch=None): + """Applies the given function to each edge in the specified iterable + of edges. + + `G` is an instance of :class:`networkx.Graph`. + + `func` is a function on two inputs, each of which is a node in the + graph. The function can return anything, but it should return a + value representing a prediction of the likelihood of a "link" + joining the two nodes. + + `ebunch` is an iterable of pairs of nodes. If not specified, all + non-edges in the graph `G` will be used. + + """ + if ebunch is None: + ebunch = nx.non_edges(G) + else: + for u, v in ebunch: + if u not in G: + raise nx.NodeNotFound(f"Node {u} not in G.") + if v not in G: + raise nx.NodeNotFound(f"Node {v} not in G.") + return ((u, v, func(u, v)) for u, v in ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def resource_allocation_index(G, ebunch=None): + r"""Compute the resource allocation index of all node pairs in ebunch. + + Resource allocation index of `u` and `v` is defined as + + .. math:: + + \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{|\Gamma(w)|} + + where $\Gamma(u)$ denotes the set of neighbors of $u$. + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + Resource allocation index will be computed for each pair of + nodes given in the iterable. The pairs must be given as + 2-tuples (u, v) where u and v are nodes in the graph. If ebunch + is None then all nonexistent edges in the graph will be used. + Default value: None. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their resource allocation index. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> preds = nx.resource_allocation_index(G, [(0, 1), (2, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 1) -> 0.75000000 + (2, 3) -> 0.75000000 + + References + ---------- + .. [1] T. Zhou, L. Lu, Y.-C. Zhang. + Predicting missing links via local information. + Eur. Phys. J. B 71 (2009) 623. + https://arxiv.org/pdf/0901.0553.pdf + """ + + def predict(u, v): + return sum(1 / G.degree(w) for w in nx.common_neighbors(G, u, v)) + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def jaccard_coefficient(G, ebunch=None): + r"""Compute the Jaccard coefficient of all node pairs in ebunch. + + Jaccard coefficient of nodes `u` and `v` is defined as + + .. math:: + + \frac{|\Gamma(u) \cap \Gamma(v)|}{|\Gamma(u) \cup \Gamma(v)|} + + where $\Gamma(u)$ denotes the set of neighbors of $u$. + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + Jaccard coefficient will be computed for each pair of nodes + given in the iterable. The pairs must be given as 2-tuples + (u, v) where u and v are nodes in the graph. If ebunch is None + then all nonexistent edges in the graph will be used. + Default value: None. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their Jaccard coefficient. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> preds = nx.jaccard_coefficient(G, [(0, 1), (2, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 1) -> 0.60000000 + (2, 3) -> 0.60000000 + + References + ---------- + .. [1] D. Liben-Nowell, J. Kleinberg. + The Link Prediction Problem for Social Networks (2004). + http://www.cs.cornell.edu/home/kleinber/link-pred.pdf + """ + + def predict(u, v): + union_size = len(set(G[u]) | set(G[v])) + if union_size == 0: + return 0 + return len(nx.common_neighbors(G, u, v)) / union_size + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def adamic_adar_index(G, ebunch=None): + r"""Compute the Adamic-Adar index of all node pairs in ebunch. + + Adamic-Adar index of `u` and `v` is defined as + + .. math:: + + \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{1}{\log |\Gamma(w)|} + + where $\Gamma(u)$ denotes the set of neighbors of $u$. + This index leads to zero-division for nodes only connected via self-loops. + It is intended to be used when no self-loops are present. + + Parameters + ---------- + G : graph + NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + Adamic-Adar index will be computed for each pair of nodes given + in the iterable. The pairs must be given as 2-tuples (u, v) + where u and v are nodes in the graph. If ebunch is None then all + nonexistent edges in the graph will be used. + Default value: None. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their Adamic-Adar index. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> preds = nx.adamic_adar_index(G, [(0, 1), (2, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 1) -> 2.16404256 + (2, 3) -> 2.16404256 + + References + ---------- + .. [1] D. Liben-Nowell, J. Kleinberg. + The Link Prediction Problem for Social Networks (2004). + http://www.cs.cornell.edu/home/kleinber/link-pred.pdf + """ + + def predict(u, v): + return sum(1 / log(G.degree(w)) for w in nx.common_neighbors(G, u, v)) + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def common_neighbor_centrality(G, ebunch=None, alpha=0.8): + r"""Return the CCPA score for each pair of nodes. + + Compute the Common Neighbor and Centrality based Parameterized Algorithm(CCPA) + score of all node pairs in ebunch. + + CCPA score of `u` and `v` is defined as + + .. math:: + + \alpha \cdot (|\Gamma (u){\cap }^{}\Gamma (v)|)+(1-\alpha )\cdot \frac{N}{{d}_{uv}} + + where $\Gamma(u)$ denotes the set of neighbors of $u$, $\Gamma(v)$ denotes the + set of neighbors of $v$, $\alpha$ is parameter varies between [0,1], $N$ denotes + total number of nodes in the Graph and ${d}_{uv}$ denotes shortest distance + between $u$ and $v$. + + This algorithm is based on two vital properties of nodes, namely the number + of common neighbors and their centrality. Common neighbor refers to the common + nodes between two nodes. Centrality refers to the prestige that a node enjoys + in a network. + + .. seealso:: + + :func:`common_neighbors` + + Parameters + ---------- + G : graph + NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + Preferential attachment score will be computed for each pair of + nodes given in the iterable. The pairs must be given as + 2-tuples (u, v) where u and v are nodes in the graph. If ebunch + is None then all nonexistent edges in the graph will be used. + Default value: None. + + alpha : Parameter defined for participation of Common Neighbor + and Centrality Algorithm share. Values for alpha should + normally be between 0 and 1. Default value set to 0.8 + because author found better performance at 0.8 for all the + dataset. + Default value: 0.8 + + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their Common Neighbor and Centrality based + Parameterized Algorithm(CCPA) score. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NetworkXAlgorithmError + If self loops exist in `ebunch` or in `G` (if `ebunch` is `None`). + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> preds = nx.common_neighbor_centrality(G, [(0, 1), (2, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p}") + (0, 1) -> 3.4000000000000004 + (2, 3) -> 3.4000000000000004 + + References + ---------- + .. [1] Ahmad, I., Akhtar, M.U., Noor, S. et al. + Missing Link Prediction using Common Neighbor and Centrality based Parameterized Algorithm. + Sci Rep 10, 364 (2020). + https://doi.org/10.1038/s41598-019-57304-y + """ + + # When alpha == 1, the CCPA score simplifies to the number of common neighbors. + if alpha == 1: + + def predict(u, v): + if u == v: + raise nx.NetworkXAlgorithmError("Self loops are not supported") + + return len(nx.common_neighbors(G, u, v)) + + else: + spl = dict(nx.shortest_path_length(G)) + inf = float("inf") + + def predict(u, v): + if u == v: + raise nx.NetworkXAlgorithmError("Self loops are not supported") + path_len = spl[u].get(v, inf) + + n_nbrs = len(nx.common_neighbors(G, u, v)) + return alpha * n_nbrs + (1 - alpha) * len(G) / path_len + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def preferential_attachment(G, ebunch=None): + r"""Compute the preferential attachment score of all node pairs in ebunch. + + Preferential attachment score of `u` and `v` is defined as + + .. math:: + + |\Gamma(u)| |\Gamma(v)| + + where $\Gamma(u)$ denotes the set of neighbors of $u$. + + Parameters + ---------- + G : graph + NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + Preferential attachment score will be computed for each pair of + nodes given in the iterable. The pairs must be given as + 2-tuples (u, v) where u and v are nodes in the graph. If ebunch + is None then all nonexistent edges in the graph will be used. + Default value: None. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their preferential attachment score. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> preds = nx.preferential_attachment(G, [(0, 1), (2, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p}") + (0, 1) -> 16 + (2, 3) -> 16 + + References + ---------- + .. [1] D. Liben-Nowell, J. Kleinberg. + The Link Prediction Problem for Social Networks (2004). + http://www.cs.cornell.edu/home/kleinber/link-pred.pdf + """ + + def predict(u, v): + return G.degree(u) * G.degree(v) + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(node_attrs="community") +def cn_soundarajan_hopcroft(G, ebunch=None, community="community"): + r"""Count the number of common neighbors of all node pairs in ebunch + using community information. + + For two nodes $u$ and $v$, this function computes the number of + common neighbors and bonus one for each common neighbor belonging to + the same community as $u$ and $v$. Mathematically, + + .. math:: + + |\Gamma(u) \cap \Gamma(v)| + \sum_{w \in \Gamma(u) \cap \Gamma(v)} f(w) + + where $f(w)$ equals 1 if $w$ belongs to the same community as $u$ + and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of + neighbors of $u$. + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + The score will be computed for each pair of nodes given in the + iterable. The pairs must be given as 2-tuples (u, v) where u + and v are nodes in the graph. If ebunch is None then all + nonexistent edges in the graph will be used. + Default value: None. + + community : string, optional (default = 'community') + Nodes attribute name containing the community information. + G[u][community] identifies which community u belongs to. Each + node belongs to at most one community. Default value: 'community'. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their score. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NetworkXAlgorithmError + If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`). + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(3) + >>> G.nodes[0]["community"] = 0 + >>> G.nodes[1]["community"] = 0 + >>> G.nodes[2]["community"] = 0 + >>> preds = nx.cn_soundarajan_hopcroft(G, [(0, 2)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p}") + (0, 2) -> 2 + + References + ---------- + .. [1] Sucheta Soundarajan and John Hopcroft. + Using community information to improve the precision of link + prediction methods. + In Proceedings of the 21st international conference companion on + World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608. + http://doi.acm.org/10.1145/2187980.2188150 + """ + + def predict(u, v): + Cu = _community(G, u, community) + Cv = _community(G, v, community) + cnbors = nx.common_neighbors(G, u, v) + neighbors = ( + sum(_community(G, w, community) == Cu for w in cnbors) if Cu == Cv else 0 + ) + return len(cnbors) + neighbors + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(node_attrs="community") +def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"): + r"""Compute the resource allocation index of all node pairs in + ebunch using community information. + + For two nodes $u$ and $v$, this function computes the resource + allocation index considering only common neighbors belonging to the + same community as $u$ and $v$. Mathematically, + + .. math:: + + \sum_{w \in \Gamma(u) \cap \Gamma(v)} \frac{f(w)}{|\Gamma(w)|} + + where $f(w)$ equals 1 if $w$ belongs to the same community as $u$ + and $v$ or 0 otherwise and $\Gamma(u)$ denotes the set of + neighbors of $u$. + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + The score will be computed for each pair of nodes given in the + iterable. The pairs must be given as 2-tuples (u, v) where u + and v are nodes in the graph. If ebunch is None then all + nonexistent edges in the graph will be used. + Default value: None. + + community : string, optional (default = 'community') + Nodes attribute name containing the community information. + G[u][community] identifies which community u belongs to. Each + node belongs to at most one community. Default value: 'community'. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their score. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NetworkXAlgorithmError + If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`). + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + >>> G.nodes[0]["community"] = 0 + >>> G.nodes[1]["community"] = 0 + >>> G.nodes[2]["community"] = 1 + >>> G.nodes[3]["community"] = 0 + >>> preds = nx.ra_index_soundarajan_hopcroft(G, [(0, 3)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 3) -> 0.50000000 + + References + ---------- + .. [1] Sucheta Soundarajan and John Hopcroft. + Using community information to improve the precision of link + prediction methods. + In Proceedings of the 21st international conference companion on + World Wide Web (WWW '12 Companion). ACM, New York, NY, USA, 607-608. + http://doi.acm.org/10.1145/2187980.2188150 + """ + + def predict(u, v): + Cu = _community(G, u, community) + Cv = _community(G, v, community) + if Cu != Cv: + return 0 + cnbors = nx.common_neighbors(G, u, v) + return sum(1 / G.degree(w) for w in cnbors if _community(G, w, community) == Cu) + + return _apply_prediction(G, predict, ebunch) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(node_attrs="community") +def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"): + """Compute the ratio of within- and inter-cluster common neighbors + of all node pairs in ebunch. + + For two nodes `u` and `v`, if a common neighbor `w` belongs to the + same community as them, `w` is considered as within-cluster common + neighbor of `u` and `v`. Otherwise, it is considered as + inter-cluster common neighbor of `u` and `v`. The ratio between the + size of the set of within- and inter-cluster common neighbors is + defined as the WIC measure. [1]_ + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + ebunch : iterable of node pairs, optional (default = None) + The WIC measure will be computed for each pair of nodes given in + the iterable. The pairs must be given as 2-tuples (u, v) where + u and v are nodes in the graph. If ebunch is None then all + nonexistent edges in the graph will be used. + Default value: None. + + delta : float, optional (default = 0.001) + Value to prevent division by zero in case there is no + inter-cluster common neighbor between two nodes. See [1]_ for + details. Default value: 0.001. + + community : string, optional (default = 'community') + Nodes attribute name containing the community information. + G[u][community] identifies which community u belongs to. Each + node belongs to at most one community. Default value: 'community'. + + Returns + ------- + piter : iterator + An iterator of 3-tuples in the form (u, v, p) where (u, v) is a + pair of nodes and p is their WIC measure. + + Raises + ------ + NetworkXNotImplemented + If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`. + + NetworkXAlgorithmError + - If `delta` is less than or equal to zero. + - If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`). + + NodeNotFound + If `ebunch` has a node that is not in `G`. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 4), (2, 4), (3, 4)]) + >>> G.nodes[0]["community"] = 0 + >>> G.nodes[1]["community"] = 1 + >>> G.nodes[2]["community"] = 0 + >>> G.nodes[3]["community"] = 0 + >>> G.nodes[4]["community"] = 0 + >>> preds = nx.within_inter_cluster(G, [(0, 4)]) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 4) -> 1.99800200 + >>> preds = nx.within_inter_cluster(G, [(0, 4)], delta=0.5) + >>> for u, v, p in preds: + ... print(f"({u}, {v}) -> {p:.8f}") + (0, 4) -> 1.33333333 + + References + ---------- + .. [1] Jorge Carlos Valverde-Rebaza and Alneu de Andrade Lopes. + Link prediction in complex networks based on cluster information. + In Proceedings of the 21st Brazilian conference on Advances in + Artificial Intelligence (SBIA'12) + https://doi.org/10.1007/978-3-642-34459-6_10 + """ + if delta <= 0: + raise nx.NetworkXAlgorithmError("Delta must be greater than zero") + + def predict(u, v): + Cu = _community(G, u, community) + Cv = _community(G, v, community) + if Cu != Cv: + return 0 + cnbors = nx.common_neighbors(G, u, v) + within = {w for w in cnbors if _community(G, w, community) == Cu} + inter = cnbors - within + return len(within) / (len(inter) + delta) + + return _apply_prediction(G, predict, ebunch) + + +def _community(G, u, community): + """Get the community of the given node.""" + node_u = G.nodes[u] + try: + return node_u[community] + except KeyError as err: + raise nx.NetworkXAlgorithmError( + f"No community information available for Node {u}" + ) from err diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/lowest_common_ancestors.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/lowest_common_ancestors.py new file mode 100644 index 0000000000000000000000000000000000000000..963a7839800b11d621a35e3e44b87711cbe40d63 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/lowest_common_ancestors.py @@ -0,0 +1,280 @@ +"""Algorithms for finding the lowest common ancestor of trees and DAGs.""" + +from collections import defaultdict +from collections.abc import Mapping, Set +from itertools import combinations_with_replacement + +import networkx as nx +from networkx.utils import UnionFind, arbitrary_element, not_implemented_for + +__all__ = [ + "all_pairs_lowest_common_ancestor", + "tree_all_pairs_lowest_common_ancestor", + "lowest_common_ancestor", +] + + +@not_implemented_for("undirected") +@nx._dispatchable +def all_pairs_lowest_common_ancestor(G, pairs=None): + """Return the lowest common ancestor of all pairs or the provided pairs + + Parameters + ---------- + G : NetworkX directed graph + + pairs : iterable of pairs of nodes, optional (default: all pairs) + The pairs of nodes of interest. + If None, will find the LCA of all pairs of nodes. + + Yields + ------ + ((node1, node2), lca) : 2-tuple + Where lca is least common ancestor of node1 and node2. + Note that for the default case, the order of the node pair is not considered, + e.g. you will not get both ``(a, b)`` and ``(b, a)`` + + Raises + ------ + NetworkXPointlessConcept + If `G` is null. + NetworkXError + If `G` is not a DAG. + + Examples + -------- + >>> from pprint import pprint + + The default behavior is to yield the lowest common ancestor for all + possible combinations of nodes in `G`, including self-pairings: + + >>> G = nx.DiGraph([(0, 1), (0, 3), (1, 2)]) + >>> pprint(dict(nx.all_pairs_lowest_common_ancestor(G))) + {(0, 0): 0, + (0, 1): 0, + (0, 2): 0, + (0, 3): 0, + (1, 1): 1, + (1, 2): 1, + (1, 3): 0, + (2, 2): 2, + (3, 2): 0, + (3, 3): 3} + + The pairs argument can be used to limit the output to only the + specified node pairings: + + >>> dict(nx.all_pairs_lowest_common_ancestor(G, pairs=[(1, 2), (2, 3)])) + {(1, 2): 1, (2, 3): 0} + + Notes + ----- + Only defined on non-null directed acyclic graphs. + + See Also + -------- + lowest_common_ancestor + """ + if not nx.is_directed_acyclic_graph(G): + raise nx.NetworkXError("LCA only defined on directed acyclic graphs.") + if len(G) == 0: + raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.") + + if pairs is None: + pairs = combinations_with_replacement(G, 2) + else: + # Convert iterator to iterable, if necessary. Trim duplicates. + pairs = dict.fromkeys(pairs) + # Verify that each of the nodes in the provided pairs is in G + nodeset = set(G) + for pair in pairs: + if set(pair) - nodeset: + raise nx.NodeNotFound( + f"Node(s) {set(pair) - nodeset} from pair {pair} not in G." + ) + + # Once input validation is done, construct the generator + def generate_lca_from_pairs(G, pairs): + ancestor_cache = {} + + for v, w in pairs: + if v not in ancestor_cache: + ancestor_cache[v] = nx.ancestors(G, v) + ancestor_cache[v].add(v) + if w not in ancestor_cache: + ancestor_cache[w] = nx.ancestors(G, w) + ancestor_cache[w].add(w) + + common_ancestors = ancestor_cache[v] & ancestor_cache[w] + + if common_ancestors: + common_ancestor = next(iter(common_ancestors)) + while True: + successor = None + for lower_ancestor in G.successors(common_ancestor): + if lower_ancestor in common_ancestors: + successor = lower_ancestor + break + if successor is None: + break + common_ancestor = successor + yield ((v, w), common_ancestor) + + return generate_lca_from_pairs(G, pairs) + + +@not_implemented_for("undirected") +@nx._dispatchable +def lowest_common_ancestor(G, node1, node2, default=None): + """Compute the lowest common ancestor of the given pair of nodes. + + Parameters + ---------- + G : NetworkX directed graph + + node1, node2 : nodes in the graph. + + default : object + Returned if no common ancestor between `node1` and `node2` + + Returns + ------- + The lowest common ancestor of node1 and node2, + or default if they have no common ancestors. + + Examples + -------- + >>> G = nx.DiGraph() + >>> nx.add_path(G, (0, 1, 2, 3)) + >>> nx.add_path(G, (0, 4, 3)) + >>> nx.lowest_common_ancestor(G, 2, 4) + 0 + + See Also + -------- + all_pairs_lowest_common_ancestor""" + + ans = list(all_pairs_lowest_common_ancestor(G, pairs=[(node1, node2)])) + if ans: + assert len(ans) == 1 + return ans[0][1] + return default + + +@not_implemented_for("undirected") +@nx._dispatchable +def tree_all_pairs_lowest_common_ancestor(G, root=None, pairs=None): + r"""Yield the lowest common ancestor for sets of pairs in a tree. + + Parameters + ---------- + G : NetworkX directed graph (must be a tree) + + root : node, optional (default: None) + The root of the subtree to operate on. + If None, assume the entire graph has exactly one source and use that. + + pairs : iterable or iterator of pairs of nodes, optional (default: None) + The pairs of interest. If None, Defaults to all pairs of nodes + under `root` that have a lowest common ancestor. + + Returns + ------- + lcas : generator of tuples `((u, v), lca)` where `u` and `v` are nodes + in `pairs` and `lca` is their lowest common ancestor. + + Examples + -------- + >>> import pprint + >>> G = nx.DiGraph([(1, 3), (2, 4), (1, 2)]) + >>> pprint.pprint(dict(nx.tree_all_pairs_lowest_common_ancestor(G))) + {(1, 1): 1, + (2, 1): 1, + (2, 2): 2, + (3, 1): 1, + (3, 2): 1, + (3, 3): 3, + (3, 4): 1, + (4, 1): 1, + (4, 2): 2, + (4, 4): 4} + + We can also use `pairs` argument to specify the pairs of nodes for which we + want to compute lowest common ancestors. Here is an example: + + >>> dict(nx.tree_all_pairs_lowest_common_ancestor(G, pairs=[(1, 4), (2, 3)])) + {(2, 3): 1, (1, 4): 1} + + Notes + ----- + Only defined on non-null trees represented with directed edges from + parents to children. Uses Tarjan's off-line lowest-common-ancestors + algorithm. Runs in time $O(4 \times (V + E + P))$ time, where 4 is the largest + value of the inverse Ackermann function likely to ever come up in actual + use, and $P$ is the number of pairs requested (or $V^2$ if all are needed). + + Tarjan, R. E. (1979), "Applications of path compression on balanced trees", + Journal of the ACM 26 (4): 690-715, doi:10.1145/322154.322161. + + See Also + -------- + all_pairs_lowest_common_ancestor: similar routine for general DAGs + lowest_common_ancestor: just a single pair for general DAGs + """ + if len(G) == 0: + raise nx.NetworkXPointlessConcept("LCA meaningless on null graphs.") + + # Index pairs of interest for efficient lookup from either side. + if pairs is not None: + pair_dict = defaultdict(set) + # See note on all_pairs_lowest_common_ancestor. + if not isinstance(pairs, Mapping | Set): + pairs = set(pairs) + for u, v in pairs: + for n in (u, v): + if n not in G: + msg = f"The node {str(n)} is not in the digraph." + raise nx.NodeNotFound(msg) + pair_dict[u].add(v) + pair_dict[v].add(u) + + # If root is not specified, find the exactly one node with in degree 0 and + # use it. Raise an error if none are found, or more than one is. Also check + # for any nodes with in degree larger than 1, which would imply G is not a + # tree. + if root is None: + for n, deg in G.in_degree: + if deg == 0: + if root is not None: + msg = "No root specified and tree has multiple sources." + raise nx.NetworkXError(msg) + root = n + # checking deg>1 is not sufficient for MultiDiGraphs + elif deg > 1 and len(G.pred[n]) > 1: + msg = "Tree LCA only defined on trees; use DAG routine." + raise nx.NetworkXError(msg) + if root is None: + raise nx.NetworkXError("Graph contains a cycle.") + + # Iterative implementation of Tarjan's offline lca algorithm + # as described in CLRS on page 521 (2nd edition)/page 584 (3rd edition) + uf = UnionFind() + ancestors = {} + for node in G: + ancestors[node] = uf[node] + + colors = defaultdict(bool) + for node in nx.dfs_postorder_nodes(G, root): + colors[node] = True + for v in pair_dict[node] if pairs is not None else G: + if colors[v]: + # If the user requested both directions of a pair, give it. + # Otherwise, just give one. + if pairs is not None and (node, v) in pairs: + yield (node, v), ancestors[uf[v]] + if pairs is None or (v, node) in pairs: + yield (v, node), ancestors[uf[v]] + if node != root: + parent = arbitrary_element(G.pred[node]) + uf.union(parent, node) + ancestors[uf[parent]] = parent diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/matching.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/matching.py new file mode 100644 index 0000000000000000000000000000000000000000..b2dc7c63e569f26e4f0933a26fb7f8bebdff8f0e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/matching.py @@ -0,0 +1,1148 @@ +"""Functions for computing and verifying matchings in a graph.""" + +from itertools import combinations, repeat + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "is_matching", + "is_maximal_matching", + "is_perfect_matching", + "max_weight_matching", + "min_weight_matching", + "maximal_matching", +] + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable +def maximal_matching(G): + r"""Find a maximal matching in the graph. + + A matching is a subset of edges in which no node occurs more than once. + A maximal matching cannot add more edges and still be a matching. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + Returns + ------- + matching : set + A maximal matching of the graph. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)]) + >>> sorted(nx.maximal_matching(G)) + [(1, 2), (3, 5)] + + Notes + ----- + The algorithm greedily selects a maximal matching M of the graph G + (i.e. no superset of M exists). It runs in $O(|E|)$ time. + """ + matching = set() + nodes = set() + for edge in G.edges(): + # If the edge isn't covered, add it to the matching + # then remove neighborhood of u and v from consideration. + u, v = edge + if u not in nodes and v not in nodes and u != v: + matching.add(edge) + nodes.update(edge) + return matching + + +def matching_dict_to_set(matching): + """Converts matching dict format to matching set format + + Converts a dictionary representing a matching (as returned by + :func:`max_weight_matching`) to a set representing a matching (as + returned by :func:`maximal_matching`). + + In the definition of maximal matching adopted by NetworkX, + self-loops are not allowed, so the provided dictionary is expected + to never have any mapping from a key to itself. However, the + dictionary is expected to have mirrored key/value pairs, for + example, key ``u`` with value ``v`` and key ``v`` with value ``u``. + + """ + edges = set() + for edge in matching.items(): + u, v = edge + if (v, u) in edges or edge in edges: + continue + if u == v: + raise nx.NetworkXError(f"Selfloops cannot appear in matchings {edge}") + edges.add(edge) + return edges + + +@nx._dispatchable +def is_matching(G, matching): + """Return True if ``matching`` is a valid matching of ``G`` + + A *matching* in a graph is a set of edges in which no two distinct + edges share a common endpoint. Each node is incident to at most one + edge in the matching. The edges are said to be independent. + + Parameters + ---------- + G : NetworkX graph + + matching : dict or set + A dictionary or set representing a matching. If a dictionary, it + must have ``matching[u] == v`` and ``matching[v] == u`` for each + edge ``(u, v)`` in the matching. If a set, it must have elements + of the form ``(u, v)``, where ``(u, v)`` is an edge in the + matching. + + Returns + ------- + bool + Whether the given set or dictionary represents a valid matching + in the graph. + + Raises + ------ + NetworkXError + If the proposed matching has an edge to a node not in G. + Or if the matching is not a collection of 2-tuple edges. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)]) + >>> nx.is_maximal_matching(G, {1: 3, 2: 4}) # using dict to represent matching + True + + >>> nx.is_matching(G, {(1, 3), (2, 4)}) # using set to represent matching + True + + """ + if isinstance(matching, dict): + matching = matching_dict_to_set(matching) + + nodes = set() + for edge in matching: + if len(edge) != 2: + raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}") + u, v = edge + if u not in G or v not in G: + raise nx.NetworkXError(f"matching contains edge {edge} with node not in G") + if u == v: + return False + if not G.has_edge(u, v): + return False + if u in nodes or v in nodes: + return False + nodes.update(edge) + return True + + +@nx._dispatchable +def is_maximal_matching(G, matching): + """Return True if ``matching`` is a maximal matching of ``G`` + + A *maximal matching* in a graph is a matching in which adding any + edge would cause the set to no longer be a valid matching. + + Parameters + ---------- + G : NetworkX graph + + matching : dict or set + A dictionary or set representing a matching. If a dictionary, it + must have ``matching[u] == v`` and ``matching[v] == u`` for each + edge ``(u, v)`` in the matching. If a set, it must have elements + of the form ``(u, v)``, where ``(u, v)`` is an edge in the + matching. + + Returns + ------- + bool + Whether the given set or dictionary represents a valid maximal + matching in the graph. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (3, 4), (3, 5)]) + >>> nx.is_maximal_matching(G, {(1, 2), (3, 4)}) + True + + """ + if isinstance(matching, dict): + matching = matching_dict_to_set(matching) + # If the given set is not a matching, then it is not a maximal matching. + edges = set() + nodes = set() + for edge in matching: + if len(edge) != 2: + raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}") + u, v = edge + if u not in G or v not in G: + raise nx.NetworkXError(f"matching contains edge {edge} with node not in G") + if u == v: + return False + if not G.has_edge(u, v): + return False + if u in nodes or v in nodes: + return False + nodes.update(edge) + edges.add(edge) + edges.add((v, u)) + # A matching is maximal if adding any new edge from G to it + # causes the resulting set to match some node twice. + # Be careful to check for adding selfloops + for u, v in G.edges: + if (u, v) not in edges: + # could add edge (u, v) to edges and have a bigger matching + if u not in nodes and v not in nodes and u != v: + return False + return True + + +@nx._dispatchable +def is_perfect_matching(G, matching): + """Return True if ``matching`` is a perfect matching for ``G`` + + A *perfect matching* in a graph is a matching in which exactly one edge + is incident upon each vertex. + + Parameters + ---------- + G : NetworkX graph + + matching : dict or set + A dictionary or set representing a matching. If a dictionary, it + must have ``matching[u] == v`` and ``matching[v] == u`` for each + edge ``(u, v)`` in the matching. If a set, it must have elements + of the form ``(u, v)``, where ``(u, v)`` is an edge in the + matching. + + Returns + ------- + bool + Whether the given set or dictionary represents a valid perfect + matching in the graph. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5), (4, 6)]) + >>> my_match = {1: 2, 3: 5, 4: 6} + >>> nx.is_perfect_matching(G, my_match) + True + + """ + if isinstance(matching, dict): + matching = matching_dict_to_set(matching) + + nodes = set() + for edge in matching: + if len(edge) != 2: + raise nx.NetworkXError(f"matching has non-2-tuple edge {edge}") + u, v = edge + if u not in G or v not in G: + raise nx.NetworkXError(f"matching contains edge {edge} with node not in G") + if u == v: + return False + if not G.has_edge(u, v): + return False + if u in nodes or v in nodes: + return False + nodes.update(edge) + return len(nodes) == len(G) + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def min_weight_matching(G, weight="weight"): + """Compute a minimum-weight maximum-cardinality matching of `G`. + + The minimum-weight maximum-cardinality matching is the matching + that has the minimum weight among all maximum-cardinality matchings. + + Use the maximum-weight algorithm with edge weights subtracted + from the maximum weight of all edges. + + A matching is a subset of edges in which no node occurs more than once. + The weight of a matching is the sum of the weights of its edges. + A maximal matching cannot add more edges and still be a matching. + The cardinality of a matching is the number of matched edges. + + This method replaces the edge weights with 1 plus the maximum edge weight + minus the original edge weight. + + new_weight = (max_weight + 1) - edge_weight + + then runs :func:`max_weight_matching` with the new weights. + The max weight matching with these new weights corresponds + to the min weight matching using the original weights. + Adding 1 to the max edge weight keeps all edge weights positive + and as integers if they started as integers. + + Read the documentation of `max_weight_matching` for more information. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + weight: string, optional (default='weight') + Edge data key corresponding to the edge weight. + If key not found, uses 1 as weight. + + Returns + ------- + matching : set + A minimal weight matching of the graph. + + See Also + -------- + max_weight_matching + """ + if len(G.edges) == 0: + return max_weight_matching(G, maxcardinality=True, weight=weight) + G_edges = G.edges(data=weight, default=1) + max_weight = 1 + max(w for _, _, w in G_edges) + InvG = nx.Graph() + edges = ((u, v, max_weight - w) for u, v, w in G_edges) + InvG.add_weighted_edges_from(edges, weight=weight) + return max_weight_matching(InvG, maxcardinality=True, weight=weight) + + +@not_implemented_for("multigraph") +@not_implemented_for("directed") +@nx._dispatchable(edge_attrs="weight") +def max_weight_matching(G, maxcardinality=False, weight="weight"): + """Compute a maximum-weighted matching of G. + + A matching is a subset of edges in which no node occurs more than once. + The weight of a matching is the sum of the weights of its edges. + A maximal matching cannot add more edges and still be a matching. + The cardinality of a matching is the number of matched edges. + + Parameters + ---------- + G : NetworkX graph + Undirected graph + + maxcardinality: bool, optional (default=False) + If maxcardinality is True, compute the maximum-cardinality matching + with maximum weight among all maximum-cardinality matchings. + + weight: string, optional (default='weight') + Edge data key corresponding to the edge weight. + If key not found, uses 1 as weight. + + + Returns + ------- + matching : set + A maximal matching of the graph. + + Examples + -------- + >>> G = nx.Graph() + >>> edges = [(1, 2, 6), (1, 3, 2), (2, 3, 1), (2, 4, 7), (3, 5, 9), (4, 5, 3)] + >>> G.add_weighted_edges_from(edges) + >>> sorted(nx.max_weight_matching(G)) + [(2, 4), (5, 3)] + + Notes + ----- + If G has edges with weight attributes the edge data are used as + weight values else the weights are assumed to be 1. + + This function takes time O(number_of_nodes ** 3). + + If all edge weights are integers, the algorithm uses only integer + computations. If floating point weights are used, the algorithm + could return a slightly suboptimal matching due to numeric + precision errors. + + This method is based on the "blossom" method for finding augmenting + paths and the "primal-dual" method for finding a matching of maximum + weight, both methods invented by Jack Edmonds [1]_. + + Bipartite graphs can also be matched using the functions present in + :mod:`networkx.algorithms.bipartite.matching`. + + References + ---------- + .. [1] "Efficient Algorithms for Finding Maximum Matching in Graphs", + Zvi Galil, ACM Computing Surveys, 1986. + """ + # + # The algorithm is taken from "Efficient Algorithms for Finding Maximum + # Matching in Graphs" by Zvi Galil, ACM Computing Surveys, 1986. + # It is based on the "blossom" method for finding augmenting paths and + # the "primal-dual" method for finding a matching of maximum weight, both + # methods invented by Jack Edmonds. + # + # A C program for maximum weight matching by Ed Rothberg was used + # extensively to validate this new code. + # + # Many terms used in the code comments are explained in the paper + # by Galil. You will probably need the paper to make sense of this code. + # + + class NoNode: + """Dummy value which is different from any node.""" + + class Blossom: + """Representation of a non-trivial blossom or sub-blossom.""" + + __slots__ = ["childs", "edges", "mybestedges"] + + # b.childs is an ordered list of b's sub-blossoms, starting with + # the base and going round the blossom. + + # b.edges is the list of b's connecting edges, such that + # b.edges[i] = (v, w) where v is a vertex in b.childs[i] + # and w is a vertex in b.childs[wrap(i+1)]. + + # If b is a top-level S-blossom, + # b.mybestedges is a list of least-slack edges to neighboring + # S-blossoms, or None if no such list has been computed yet. + # This is used for efficient computation of delta3. + + # Generate the blossom's leaf vertices. + def leaves(self): + stack = [*self.childs] + while stack: + t = stack.pop() + if isinstance(t, Blossom): + stack.extend(t.childs) + else: + yield t + + # Get a list of vertices. + gnodes = list(G) + if not gnodes: + return set() # don't bother with empty graphs + + # Find the maximum edge weight. + maxweight = 0 + allinteger = True + for i, j, d in G.edges(data=True): + wt = d.get(weight, 1) + if i != j and wt > maxweight: + maxweight = wt + allinteger = allinteger and (str(type(wt)).split("'")[1] in ("int", "long")) + + # If v is a matched vertex, mate[v] is its partner vertex. + # If v is a single vertex, v does not occur as a key in mate. + # Initially all vertices are single; updated during augmentation. + mate = {} + + # If b is a top-level blossom, + # label.get(b) is None if b is unlabeled (free), + # 1 if b is an S-blossom, + # 2 if b is a T-blossom. + # The label of a vertex is found by looking at the label of its top-level + # containing blossom. + # If v is a vertex inside a T-blossom, label[v] is 2 iff v is reachable + # from an S-vertex outside the blossom. + # Labels are assigned during a stage and reset after each augmentation. + label = {} + + # If b is a labeled top-level blossom, + # labeledge[b] = (v, w) is the edge through which b obtained its label + # such that w is a vertex in b, or None if b's base vertex is single. + # If w is a vertex inside a T-blossom and label[w] == 2, + # labeledge[w] = (v, w) is an edge through which w is reachable from + # outside the blossom. + labeledge = {} + + # If v is a vertex, inblossom[v] is the top-level blossom to which v + # belongs. + # If v is a top-level vertex, inblossom[v] == v since v is itself + # a (trivial) top-level blossom. + # Initially all vertices are top-level trivial blossoms. + inblossom = dict(zip(gnodes, gnodes)) + + # If b is a sub-blossom, + # blossomparent[b] is its immediate parent (sub-)blossom. + # If b is a top-level blossom, blossomparent[b] is None. + blossomparent = dict(zip(gnodes, repeat(None))) + + # If b is a (sub-)blossom, + # blossombase[b] is its base VERTEX (i.e. recursive sub-blossom). + blossombase = dict(zip(gnodes, gnodes)) + + # If w is a free vertex (or an unreached vertex inside a T-blossom), + # bestedge[w] = (v, w) is the least-slack edge from an S-vertex, + # or None if there is no such edge. + # If b is a (possibly trivial) top-level S-blossom, + # bestedge[b] = (v, w) is the least-slack edge to a different S-blossom + # (v inside b), or None if there is no such edge. + # This is used for efficient computation of delta2 and delta3. + bestedge = {} + + # If v is a vertex, + # dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual + # optimization problem (if all edge weights are integers, multiplication + # by two ensures that all values remain integers throughout the algorithm). + # Initially, u(v) = maxweight / 2. + dualvar = dict(zip(gnodes, repeat(maxweight))) + + # If b is a non-trivial blossom, + # blossomdual[b] = z(b) where z(b) is b's variable in the dual + # optimization problem. + blossomdual = {} + + # If (v, w) in allowedge or (w, v) in allowedg, then the edge + # (v, w) is known to have zero slack in the optimization problem; + # otherwise the edge may or may not have zero slack. + allowedge = {} + + # Queue of newly discovered S-vertices. + queue = [] + + # Return 2 * slack of edge (v, w) (does not work inside blossoms). + def slack(v, w): + return dualvar[v] + dualvar[w] - 2 * G[v][w].get(weight, 1) + + # Assign label t to the top-level blossom containing vertex w, + # coming through an edge from vertex v. + def assignLabel(w, t, v): + b = inblossom[w] + assert label.get(w) is None and label.get(b) is None + label[w] = label[b] = t + if v is not None: + labeledge[w] = labeledge[b] = (v, w) + else: + labeledge[w] = labeledge[b] = None + bestedge[w] = bestedge[b] = None + if t == 1: + # b became an S-vertex/blossom; add it(s vertices) to the queue. + if isinstance(b, Blossom): + queue.extend(b.leaves()) + else: + queue.append(b) + elif t == 2: + # b became a T-vertex/blossom; assign label S to its mate. + # (If b is a non-trivial blossom, its base is the only vertex + # with an external mate.) + base = blossombase[b] + assignLabel(mate[base], 1, base) + + # Trace back from vertices v and w to discover either a new blossom + # or an augmenting path. Return the base vertex of the new blossom, + # or NoNode if an augmenting path was found. + def scanBlossom(v, w): + # Trace back from v and w, placing breadcrumbs as we go. + path = [] + base = NoNode + while v is not NoNode: + # Look for a breadcrumb in v's blossom or put a new breadcrumb. + b = inblossom[v] + if label[b] & 4: + base = blossombase[b] + break + assert label[b] == 1 + path.append(b) + label[b] = 5 + # Trace one step back. + if labeledge[b] is None: + # The base of blossom b is single; stop tracing this path. + assert blossombase[b] not in mate + v = NoNode + else: + assert labeledge[b][0] == mate[blossombase[b]] + v = labeledge[b][0] + b = inblossom[v] + assert label[b] == 2 + # b is a T-blossom; trace one more step back. + v = labeledge[b][0] + # Swap v and w so that we alternate between both paths. + if w is not NoNode: + v, w = w, v + # Remove breadcrumbs. + for b in path: + label[b] = 1 + # Return base vertex, if we found one. + return base + + # Construct a new blossom with given base, through S-vertices v and w. + # Label the new blossom as S; set its dual variable to zero; + # relabel its T-vertices to S and add them to the queue. + def addBlossom(base, v, w): + bb = inblossom[base] + bv = inblossom[v] + bw = inblossom[w] + # Create blossom. + b = Blossom() + blossombase[b] = base + blossomparent[b] = None + blossomparent[bb] = b + # Make list of sub-blossoms and their interconnecting edge endpoints. + b.childs = path = [] + b.edges = edgs = [(v, w)] + # Trace back from v to base. + while bv != bb: + # Add bv to the new blossom. + blossomparent[bv] = b + path.append(bv) + edgs.append(labeledge[bv]) + assert label[bv] == 2 or ( + label[bv] == 1 and labeledge[bv][0] == mate[blossombase[bv]] + ) + # Trace one step back. + v = labeledge[bv][0] + bv = inblossom[v] + # Add base sub-blossom; reverse lists. + path.append(bb) + path.reverse() + edgs.reverse() + # Trace back from w to base. + while bw != bb: + # Add bw to the new blossom. + blossomparent[bw] = b + path.append(bw) + edgs.append((labeledge[bw][1], labeledge[bw][0])) + assert label[bw] == 2 or ( + label[bw] == 1 and labeledge[bw][0] == mate[blossombase[bw]] + ) + # Trace one step back. + w = labeledge[bw][0] + bw = inblossom[w] + # Set label to S. + assert label[bb] == 1 + label[b] = 1 + labeledge[b] = labeledge[bb] + # Set dual variable to zero. + blossomdual[b] = 0 + # Relabel vertices. + for v in b.leaves(): + if label[inblossom[v]] == 2: + # This T-vertex now turns into an S-vertex because it becomes + # part of an S-blossom; add it to the queue. + queue.append(v) + inblossom[v] = b + # Compute b.mybestedges. + bestedgeto = {} + for bv in path: + if isinstance(bv, Blossom): + if bv.mybestedges is not None: + # Walk this subblossom's least-slack edges. + nblist = bv.mybestedges + # The sub-blossom won't need this data again. + bv.mybestedges = None + else: + # This subblossom does not have a list of least-slack + # edges; get the information from the vertices. + nblist = [ + (v, w) for v in bv.leaves() for w in G.neighbors(v) if v != w + ] + else: + nblist = [(bv, w) for w in G.neighbors(bv) if bv != w] + for k in nblist: + (i, j) = k + if inblossom[j] == b: + i, j = j, i + bj = inblossom[j] + if ( + bj != b + and label.get(bj) == 1 + and ((bj not in bestedgeto) or slack(i, j) < slack(*bestedgeto[bj])) + ): + bestedgeto[bj] = k + # Forget about least-slack edge of the subblossom. + bestedge[bv] = None + b.mybestedges = list(bestedgeto.values()) + # Select bestedge[b]. + mybestedge = None + bestedge[b] = None + for k in b.mybestedges: + kslack = slack(*k) + if mybestedge is None or kslack < mybestslack: + mybestedge = k + mybestslack = kslack + bestedge[b] = mybestedge + + # Expand the given top-level blossom. + def expandBlossom(b, endstage): + # This is an obnoxiously complicated recursive function for the sake of + # a stack-transformation. So, we hack around the complexity by using + # a trampoline pattern. By yielding the arguments to each recursive + # call, we keep the actual callstack flat. + + def _recurse(b, endstage): + # Convert sub-blossoms into top-level blossoms. + for s in b.childs: + blossomparent[s] = None + if isinstance(s, Blossom): + if endstage and blossomdual[s] == 0: + # Recursively expand this sub-blossom. + yield s + else: + for v in s.leaves(): + inblossom[v] = s + else: + inblossom[s] = s + # If we expand a T-blossom during a stage, its sub-blossoms must be + # relabeled. + if (not endstage) and label.get(b) == 2: + # Start at the sub-blossom through which the expanding + # blossom obtained its label, and relabel sub-blossoms untili + # we reach the base. + # Figure out through which sub-blossom the expanding blossom + # obtained its label initially. + entrychild = inblossom[labeledge[b][1]] + # Decide in which direction we will go round the blossom. + j = b.childs.index(entrychild) + if j & 1: + # Start index is odd; go forward and wrap. + j -= len(b.childs) + jstep = 1 + else: + # Start index is even; go backward. + jstep = -1 + # Move along the blossom until we get to the base. + v, w = labeledge[b] + while j != 0: + # Relabel the T-sub-blossom. + if jstep == 1: + p, q = b.edges[j] + else: + q, p = b.edges[j - 1] + label[w] = None + label[q] = None + assignLabel(w, 2, v) + # Step to the next S-sub-blossom and note its forward edge. + allowedge[(p, q)] = allowedge[(q, p)] = True + j += jstep + if jstep == 1: + v, w = b.edges[j] + else: + w, v = b.edges[j - 1] + # Step to the next T-sub-blossom. + allowedge[(v, w)] = allowedge[(w, v)] = True + j += jstep + # Relabel the base T-sub-blossom WITHOUT stepping through to + # its mate (so don't call assignLabel). + bw = b.childs[j] + label[w] = label[bw] = 2 + labeledge[w] = labeledge[bw] = (v, w) + bestedge[bw] = None + # Continue along the blossom until we get back to entrychild. + j += jstep + while b.childs[j] != entrychild: + # Examine the vertices of the sub-blossom to see whether + # it is reachable from a neighboring S-vertex outside the + # expanding blossom. + bv = b.childs[j] + if label.get(bv) == 1: + # This sub-blossom just got label S through one of its + # neighbors; leave it be. + j += jstep + continue + if isinstance(bv, Blossom): + for v in bv.leaves(): + if label.get(v): + break + else: + v = bv + # If the sub-blossom contains a reachable vertex, assign + # label T to the sub-blossom. + if label.get(v): + assert label[v] == 2 + assert inblossom[v] == bv + label[v] = None + label[mate[blossombase[bv]]] = None + assignLabel(v, 2, labeledge[v][0]) + j += jstep + # Remove the expanded blossom entirely. + label.pop(b, None) + labeledge.pop(b, None) + bestedge.pop(b, None) + del blossomparent[b] + del blossombase[b] + del blossomdual[b] + + # Now, we apply the trampoline pattern. We simulate a recursive + # callstack by maintaining a stack of generators, each yielding a + # sequence of function arguments. We grow the stack by appending a call + # to _recurse on each argument tuple, and shrink the stack whenever a + # generator is exhausted. + stack = [_recurse(b, endstage)] + while stack: + top = stack[-1] + for s in top: + stack.append(_recurse(s, endstage)) + break + else: + stack.pop() + + # Swap matched/unmatched edges over an alternating path through blossom b + # between vertex v and the base vertex. Keep blossom bookkeeping + # consistent. + def augmentBlossom(b, v): + # This is an obnoxiously complicated recursive function for the sake of + # a stack-transformation. So, we hack around the complexity by using + # a trampoline pattern. By yielding the arguments to each recursive + # call, we keep the actual callstack flat. + + def _recurse(b, v): + # Bubble up through the blossom tree from vertex v to an immediate + # sub-blossom of b. + t = v + while blossomparent[t] != b: + t = blossomparent[t] + # Recursively deal with the first sub-blossom. + if isinstance(t, Blossom): + yield (t, v) + # Decide in which direction we will go round the blossom. + i = j = b.childs.index(t) + if i & 1: + # Start index is odd; go forward and wrap. + j -= len(b.childs) + jstep = 1 + else: + # Start index is even; go backward. + jstep = -1 + # Move along the blossom until we get to the base. + while j != 0: + # Step to the next sub-blossom and augment it recursively. + j += jstep + t = b.childs[j] + if jstep == 1: + w, x = b.edges[j] + else: + x, w = b.edges[j - 1] + if isinstance(t, Blossom): + yield (t, w) + # Step to the next sub-blossom and augment it recursively. + j += jstep + t = b.childs[j] + if isinstance(t, Blossom): + yield (t, x) + # Match the edge connecting those sub-blossoms. + mate[w] = x + mate[x] = w + # Rotate the list of sub-blossoms to put the new base at the front. + b.childs = b.childs[i:] + b.childs[:i] + b.edges = b.edges[i:] + b.edges[:i] + blossombase[b] = blossombase[b.childs[0]] + assert blossombase[b] == v + + # Now, we apply the trampoline pattern. We simulate a recursive + # callstack by maintaining a stack of generators, each yielding a + # sequence of function arguments. We grow the stack by appending a call + # to _recurse on each argument tuple, and shrink the stack whenever a + # generator is exhausted. + stack = [_recurse(b, v)] + while stack: + top = stack[-1] + for args in top: + stack.append(_recurse(*args)) + break + else: + stack.pop() + + # Swap matched/unmatched edges over an alternating path between two + # single vertices. The augmenting path runs through S-vertices v and w. + def augmentMatching(v, w): + for s, j in ((v, w), (w, v)): + # Match vertex s to vertex j. Then trace back from s + # until we find a single vertex, swapping matched and unmatched + # edges as we go. + while 1: + bs = inblossom[s] + assert label[bs] == 1 + assert (labeledge[bs] is None and blossombase[bs] not in mate) or ( + labeledge[bs][0] == mate[blossombase[bs]] + ) + # Augment through the S-blossom from s to base. + if isinstance(bs, Blossom): + augmentBlossom(bs, s) + # Update mate[s] + mate[s] = j + # Trace one step back. + if labeledge[bs] is None: + # Reached single vertex; stop. + break + t = labeledge[bs][0] + bt = inblossom[t] + assert label[bt] == 2 + # Trace one more step back. + s, j = labeledge[bt] + # Augment through the T-blossom from j to base. + assert blossombase[bt] == t + if isinstance(bt, Blossom): + augmentBlossom(bt, j) + # Update mate[j] + mate[j] = s + + # Verify that the optimum solution has been reached. + def verifyOptimum(): + if maxcardinality: + # Vertices may have negative dual; + # find a constant non-negative number to add to all vertex duals. + vdualoffset = max(0, -min(dualvar.values())) + else: + vdualoffset = 0 + # 0. all dual variables are non-negative + assert min(dualvar.values()) + vdualoffset >= 0 + assert len(blossomdual) == 0 or min(blossomdual.values()) >= 0 + # 0. all edges have non-negative slack and + # 1. all matched edges have zero slack; + for i, j, d in G.edges(data=True): + wt = d.get(weight, 1) + if i == j: + continue # ignore self-loops + s = dualvar[i] + dualvar[j] - 2 * wt + iblossoms = [i] + jblossoms = [j] + while blossomparent[iblossoms[-1]] is not None: + iblossoms.append(blossomparent[iblossoms[-1]]) + while blossomparent[jblossoms[-1]] is not None: + jblossoms.append(blossomparent[jblossoms[-1]]) + iblossoms.reverse() + jblossoms.reverse() + for bi, bj in zip(iblossoms, jblossoms): + if bi != bj: + break + s += 2 * blossomdual[bi] + assert s >= 0 + if mate.get(i) == j or mate.get(j) == i: + assert mate[i] == j and mate[j] == i + assert s == 0 + # 2. all single vertices have zero dual value; + for v in gnodes: + assert (v in mate) or dualvar[v] + vdualoffset == 0 + # 3. all blossoms with positive dual value are full. + for b in blossomdual: + if blossomdual[b] > 0: + assert len(b.edges) % 2 == 1 + for i, j in b.edges[1::2]: + assert mate[i] == j and mate[j] == i + # Ok. + + # Main loop: continue until no further improvement is possible. + while 1: + # Each iteration of this loop is a "stage". + # A stage finds an augmenting path and uses that to improve + # the matching. + + # Remove labels from top-level blossoms/vertices. + label.clear() + labeledge.clear() + + # Forget all about least-slack edges. + bestedge.clear() + for b in blossomdual: + b.mybestedges = None + + # Loss of labeling means that we can not be sure that currently + # allowable edges remain allowable throughout this stage. + allowedge.clear() + + # Make queue empty. + queue[:] = [] + + # Label single blossoms/vertices with S and put them in the queue. + for v in gnodes: + if (v not in mate) and label.get(inblossom[v]) is None: + assignLabel(v, 1, None) + + # Loop until we succeed in augmenting the matching. + augmented = 0 + while 1: + # Each iteration of this loop is a "substage". + # A substage tries to find an augmenting path; + # if found, the path is used to improve the matching and + # the stage ends. If there is no augmenting path, the + # primal-dual method is used to pump some slack out of + # the dual variables. + + # Continue labeling until all vertices which are reachable + # through an alternating path have got a label. + while queue and not augmented: + # Take an S vertex from the queue. + v = queue.pop() + assert label[inblossom[v]] == 1 + + # Scan its neighbors: + for w in G.neighbors(v): + if w == v: + continue # ignore self-loops + # w is a neighbor to v + bv = inblossom[v] + bw = inblossom[w] + if bv == bw: + # this edge is internal to a blossom; ignore it + continue + if (v, w) not in allowedge: + kslack = slack(v, w) + if kslack <= 0: + # edge k has zero slack => it is allowable + allowedge[(v, w)] = allowedge[(w, v)] = True + if (v, w) in allowedge: + if label.get(bw) is None: + # (C1) w is a free vertex; + # label w with T and label its mate with S (R12). + assignLabel(w, 2, v) + elif label.get(bw) == 1: + # (C2) w is an S-vertex (not in the same blossom); + # follow back-links to discover either an + # augmenting path or a new blossom. + base = scanBlossom(v, w) + if base is not NoNode: + # Found a new blossom; add it to the blossom + # bookkeeping and turn it into an S-blossom. + addBlossom(base, v, w) + else: + # Found an augmenting path; augment the + # matching and end this stage. + augmentMatching(v, w) + augmented = 1 + break + elif label.get(w) is None: + # w is inside a T-blossom, but w itself has not + # yet been reached from outside the blossom; + # mark it as reached (we need this to relabel + # during T-blossom expansion). + assert label[bw] == 2 + label[w] = 2 + labeledge[w] = (v, w) + elif label.get(bw) == 1: + # keep track of the least-slack non-allowable edge to + # a different S-blossom. + if bestedge.get(bv) is None or kslack < slack(*bestedge[bv]): + bestedge[bv] = (v, w) + elif label.get(w) is None: + # w is a free vertex (or an unreached vertex inside + # a T-blossom) but we can not reach it yet; + # keep track of the least-slack edge that reaches w. + if bestedge.get(w) is None or kslack < slack(*bestedge[w]): + bestedge[w] = (v, w) + + if augmented: + break + + # There is no augmenting path under these constraints; + # compute delta and reduce slack in the optimization problem. + # (Note that our vertex dual variables, edge slacks and delta's + # are pre-multiplied by two.) + deltatype = -1 + delta = deltaedge = deltablossom = None + + # Compute delta1: the minimum value of any vertex dual. + if not maxcardinality: + deltatype = 1 + delta = min(dualvar.values()) + + # Compute delta2: the minimum slack on any edge between + # an S-vertex and a free vertex. + for v in G.nodes(): + if label.get(inblossom[v]) is None and bestedge.get(v) is not None: + d = slack(*bestedge[v]) + if deltatype == -1 or d < delta: + delta = d + deltatype = 2 + deltaedge = bestedge[v] + + # Compute delta3: half the minimum slack on any edge between + # a pair of S-blossoms. + for b in blossomparent: + if ( + blossomparent[b] is None + and label.get(b) == 1 + and bestedge.get(b) is not None + ): + kslack = slack(*bestedge[b]) + if allinteger: + assert (kslack % 2) == 0 + d = kslack // 2 + else: + d = kslack / 2.0 + if deltatype == -1 or d < delta: + delta = d + deltatype = 3 + deltaedge = bestedge[b] + + # Compute delta4: minimum z variable of any T-blossom. + for b in blossomdual: + if ( + blossomparent[b] is None + and label.get(b) == 2 + and (deltatype == -1 or blossomdual[b] < delta) + ): + delta = blossomdual[b] + deltatype = 4 + deltablossom = b + + if deltatype == -1: + # No further improvement possible; max-cardinality optimum + # reached. Do a final delta update to make the optimum + # verifiable. + assert maxcardinality + deltatype = 1 + delta = max(0, min(dualvar.values())) + + # Update dual variables according to delta. + for v in gnodes: + if label.get(inblossom[v]) == 1: + # S-vertex: 2*u = 2*u - 2*delta + dualvar[v] -= delta + elif label.get(inblossom[v]) == 2: + # T-vertex: 2*u = 2*u + 2*delta + dualvar[v] += delta + for b in blossomdual: + if blossomparent[b] is None: + if label.get(b) == 1: + # top-level S-blossom: z = z + 2*delta + blossomdual[b] += delta + elif label.get(b) == 2: + # top-level T-blossom: z = z - 2*delta + blossomdual[b] -= delta + + # Take action at the point where minimum delta occurred. + if deltatype == 1: + # No further improvement possible; optimum reached. + break + elif deltatype == 2: + # Use the least-slack edge to continue the search. + (v, w) = deltaedge + assert label[inblossom[v]] == 1 + allowedge[(v, w)] = allowedge[(w, v)] = True + queue.append(v) + elif deltatype == 3: + # Use the least-slack edge to continue the search. + (v, w) = deltaedge + allowedge[(v, w)] = allowedge[(w, v)] = True + assert label[inblossom[v]] == 1 + queue.append(v) + elif deltatype == 4: + # Expand the least-z blossom. + expandBlossom(deltablossom, False) + + # End of a this substage. + + # Paranoia check that the matching is symmetric. + for v in mate: + assert mate[mate[v]] == v + + # Stop when no more augmenting path can be found. + if not augmented: + break + + # End of a stage; expand all S-blossoms which have zero dual. + for b in list(blossomdual.keys()): + if b not in blossomdual: + continue # already expanded + if blossomparent[b] is None and label.get(b) == 1 and blossomdual[b] == 0: + expandBlossom(b, True) + + # Verify that we reached the optimum solution (only for integer weights). + if allinteger: + verifyOptimum() + + return matching_dict_to_set(mate) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/mis.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/mis.py new file mode 100644 index 0000000000000000000000000000000000000000..0652ac4acec51c86edef8e8ed963d634c40f12ad --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/mis.py @@ -0,0 +1,78 @@ +""" +Algorithm to find a maximal (not maximum) independent set. + +""" + +import networkx as nx +from networkx.utils import not_implemented_for, py_random_state + +__all__ = ["maximal_independent_set"] + + +@not_implemented_for("directed") +@py_random_state(2) +@nx._dispatchable +def maximal_independent_set(G, nodes=None, seed=None): + """Returns a random maximal independent set guaranteed to contain + a given set of nodes. + + An independent set is a set of nodes such that the subgraph + of G induced by these nodes contains no edges. A maximal + independent set is an independent set such that it is not possible + to add a new node and still get an independent set. + + Parameters + ---------- + G : NetworkX graph + + nodes : list or iterable + Nodes that must be part of the independent set. This set of nodes + must be independent. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + indep_nodes : list + List of nodes that are part of a maximal independent set. + + Raises + ------ + NetworkXUnfeasible + If the nodes in the provided list are not part of the graph or + do not form an independent set, an exception is raised. + + NetworkXNotImplemented + If `G` is directed. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.maximal_independent_set(G) # doctest: +SKIP + [4, 0, 2] + >>> nx.maximal_independent_set(G, [1]) # doctest: +SKIP + [1, 3] + + Notes + ----- + This algorithm does not solve the maximum independent set problem. + + """ + if not nodes: + nodes = {seed.choice(list(G))} + else: + nodes = set(nodes) + if not nodes.issubset(G): + raise nx.NetworkXUnfeasible(f"{nodes} is not a subset of the nodes of G") + neighbors = set.union(*[set(G.adj[v]) for v in nodes]) + if set.intersection(neighbors, nodes): + raise nx.NetworkXUnfeasible(f"{nodes} is not an independent set of G") + indep_nodes = list(nodes) + available_nodes = set(G.nodes()).difference(neighbors.union(nodes)) + while available_nodes: + node = seed.choice(list(available_nodes)) + indep_nodes.append(node) + available_nodes.difference_update(list(G.adj[node]) + [node]) + return indep_nodes diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/moral.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/moral.py new file mode 100644 index 0000000000000000000000000000000000000000..e2acf80f6c3715da57dfc92e4c2d2daf986b3c29 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/moral.py @@ -0,0 +1,59 @@ +r"""Function for computing the moral graph of a directed graph.""" + +import itertools + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["moral_graph"] + + +@not_implemented_for("undirected") +@nx._dispatchable(returns_graph=True) +def moral_graph(G): + r"""Return the Moral Graph + + Returns the moralized graph of a given directed graph. + + Parameters + ---------- + G : NetworkX graph + Directed graph + + Returns + ------- + H : NetworkX graph + The undirected moralized graph of G + + Raises + ------ + NetworkXNotImplemented + If `G` is undirected. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (2, 5), (3, 4), (4, 3)]) + >>> G_moral = nx.moral_graph(G) + >>> G_moral.edges() + EdgeView([(1, 2), (2, 3), (2, 5), (2, 4), (3, 4)]) + + Notes + ----- + A moral graph is an undirected graph H = (V, E) generated from a + directed Graph, where if a node has more than one parent node, edges + between these parent nodes are inserted and all directed edges become + undirected. + + https://en.wikipedia.org/wiki/Moral_graph + + References + ---------- + .. [1] Wray L. Buntine. 1995. Chain graphs for learning. + In Proceedings of the Eleventh conference on Uncertainty + in artificial intelligence (UAI'95) + """ + H = G.to_undirected() + for preds in G.pred.values(): + predecessors_combinations = itertools.combinations(preds, r=2) + H.add_edges_from(predecessors_combinations) + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/node_classification.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/node_classification.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5088e10c481b9a19380228dbae57efed4c7a36 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/node_classification.py @@ -0,0 +1,219 @@ +"""This module provides the functions for node classification problem. + +The functions in this module are not imported +into the top level `networkx` namespace. +You can access these functions by importing +the `networkx.algorithms.node_classification` modules, +then accessing the functions as attributes of `node_classification`. +For example: + + >>> from networkx.algorithms import node_classification + >>> G = nx.path_graph(4) + >>> G.edges() + EdgeView([(0, 1), (1, 2), (2, 3)]) + >>> G.nodes[0]["label"] = "A" + >>> G.nodes[3]["label"] = "B" + >>> node_classification.harmonic_function(G) + ['A', 'A', 'B', 'B'] + +References +---------- +Zhu, X., Ghahramani, Z., & Lafferty, J. (2003, August). +Semi-supervised learning using gaussian fields and harmonic functions. +In ICML (Vol. 3, pp. 912-919). +""" + +import networkx as nx + +__all__ = ["harmonic_function", "local_and_global_consistency"] + + +@nx.utils.not_implemented_for("directed") +@nx._dispatchable(node_attrs="label_name") +def harmonic_function(G, max_iter=30, label_name="label"): + """Node classification by Harmonic function + + Function for computing Harmonic function algorithm by Zhu et al. + + Parameters + ---------- + G : NetworkX Graph + max_iter : int + maximum number of iterations allowed + label_name : string + name of target labels to predict + + Returns + ------- + predicted : list + List of length ``len(G)`` with the predicted labels for each node. + + Raises + ------ + NetworkXError + If no nodes in `G` have attribute `label_name`. + + Examples + -------- + >>> from networkx.algorithms import node_classification + >>> G = nx.path_graph(4) + >>> G.nodes[0]["label"] = "A" + >>> G.nodes[3]["label"] = "B" + >>> G.nodes(data=True) + NodeDataView({0: {'label': 'A'}, 1: {}, 2: {}, 3: {'label': 'B'}}) + >>> G.edges() + EdgeView([(0, 1), (1, 2), (2, 3)]) + >>> predicted = node_classification.harmonic_function(G) + >>> predicted + ['A', 'A', 'B', 'B'] + + References + ---------- + Zhu, X., Ghahramani, Z., & Lafferty, J. (2003, August). + Semi-supervised learning using gaussian fields and harmonic functions. + In ICML (Vol. 3, pp. 912-919). + """ + import numpy as np + import scipy as sp + + X = nx.to_scipy_sparse_array(G) # adjacency matrix + labels, label_dict = _get_label_info(G, label_name) + + if labels.shape[0] == 0: + raise nx.NetworkXError( + f"No node on the input graph is labeled by '{label_name}'." + ) + + n_samples = X.shape[0] + n_classes = label_dict.shape[0] + F = np.zeros((n_samples, n_classes)) + + # Build propagation matrix + degrees = X.sum(axis=0) + degrees[degrees == 0] = 1 # Avoid division by 0 + D = sp.sparse.dia_array((1.0 / degrees, 0), shape=(n_samples, n_samples)).tocsr() + P = (D @ X).tolil() + P[labels[:, 0]] = 0 # labels[:, 0] indicates IDs of labeled nodes + # Build base matrix + B = np.zeros((n_samples, n_classes)) + B[labels[:, 0], labels[:, 1]] = 1 + + for _ in range(max_iter): + F = (P @ F) + B + + return label_dict[np.argmax(F, axis=1)].tolist() + + +@nx.utils.not_implemented_for("directed") +@nx._dispatchable(node_attrs="label_name") +def local_and_global_consistency(G, alpha=0.99, max_iter=30, label_name="label"): + """Node classification by Local and Global Consistency + + Function for computing Local and global consistency algorithm by Zhou et al. + + Parameters + ---------- + G : NetworkX Graph + alpha : float + Clamping factor + max_iter : int + Maximum number of iterations allowed + label_name : string + Name of target labels to predict + + Returns + ------- + predicted : list + List of length ``len(G)`` with the predicted labels for each node. + + Raises + ------ + NetworkXError + If no nodes in `G` have attribute `label_name`. + + Examples + -------- + >>> from networkx.algorithms import node_classification + >>> G = nx.path_graph(4) + >>> G.nodes[0]["label"] = "A" + >>> G.nodes[3]["label"] = "B" + >>> G.nodes(data=True) + NodeDataView({0: {'label': 'A'}, 1: {}, 2: {}, 3: {'label': 'B'}}) + >>> G.edges() + EdgeView([(0, 1), (1, 2), (2, 3)]) + >>> predicted = node_classification.local_and_global_consistency(G) + >>> predicted + ['A', 'A', 'B', 'B'] + + References + ---------- + Zhou, D., Bousquet, O., Lal, T. N., Weston, J., & Schölkopf, B. (2004). + Learning with local and global consistency. + Advances in neural information processing systems, 16(16), 321-328. + """ + import numpy as np + import scipy as sp + + X = nx.to_scipy_sparse_array(G) # adjacency matrix + labels, label_dict = _get_label_info(G, label_name) + + if labels.shape[0] == 0: + raise nx.NetworkXError( + f"No node on the input graph is labeled by '{label_name}'." + ) + + n_samples = X.shape[0] + n_classes = label_dict.shape[0] + F = np.zeros((n_samples, n_classes)) + + # Build propagation matrix + degrees = X.sum(axis=0) + degrees[degrees == 0] = 1 # Avoid division by 0 + D2 = sp.sparse.dia_array( + (1.0 / np.sqrt(degrees), 0), shape=(n_samples, n_samples) + ).tocsr() + P = alpha * ((D2 @ X) @ D2) + # Build base matrix + B = np.zeros((n_samples, n_classes)) + B[labels[:, 0], labels[:, 1]] = 1 - alpha + + for _ in range(max_iter): + F = (P @ F) + B + + return label_dict[np.argmax(F, axis=1)].tolist() + + +def _get_label_info(G, label_name): + """Get and return information of labels from the input graph + + Parameters + ---------- + G : Network X graph + label_name : string + Name of the target label + + Returns + ------- + labels : numpy array, shape = [n_labeled_samples, 2] + Array of pairs of labeled node ID and label ID + label_dict : numpy array, shape = [n_classes] + Array of labels + i-th element contains the label corresponding label ID `i` + """ + import numpy as np + + labels = [] + label_to_id = {} + lid = 0 + for i, n in enumerate(G.nodes(data=True)): + if label_name in n[1]: + label = n[1][label_name] + if label not in label_to_id: + label_to_id[label] = lid + lid += 1 + labels.append([i, label_to_id[label]]) + labels = np.array(labels) + label_dict = np.array( + [label for label, _ in sorted(label_to_id.items(), key=lambda x: x[1])] + ) + return (labels, label_dict) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/non_randomness.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/non_randomness.py new file mode 100644 index 0000000000000000000000000000000000000000..3b3a94f0d7855abcfaa2e022720592c3accb69e8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/non_randomness.py @@ -0,0 +1,155 @@ +r"""Computation of graph non-randomness.""" + +import math + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["non_randomness"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def non_randomness(G, k=None, weight="weight"): + """Compute the non-randomness of a graph. + + The first value $R_G$ is the sum of non-randomness values of all + edges within the graph (where the non-randomness of an edge tends to be + small when the two nodes linked by that edge are from two different + communities). + + The second value $R_G^*$ is a relative measure that indicates + to what extent `G` is different from a random graph in terms + of probability. The closer it is to 0, the higher the likelihood + the graph was generated by an Erdős--Rényi model. + + Parameters + ---------- + G : NetworkX graph + Graph must be undirected, connected, and without self-loops. + + k : int or None, optional (default=None) + The number of communities in `G`. + If `k` is not set, the function uses a default community detection + algorithm (:func:`~networkx.algorithms.community.label_propagation_communities`) + to set it. + + weight : string or None, optional (default="weight") + The name of an edge attribute that holds the numerical value used + as a weight. If `None`, then each edge has weight 1, i.e., the graph is + binary. + + Returns + ------- + (float, float) tuple + The first value is $R_G$, the non-randomness of the graph, + the second is $R_G^*$, the relative non-randomness + w.r.t. the Erdős--Rényi model. + + Raises + ------ + NetworkXNotImplemented + If the input graph is directed or a multigraph. + + NetworkXException + If the input graph is not connected. + + NetworkXError + If the input graph contains self-loops or has no edges. + + ValueError + If `k` is not in $\\{1, \\dots, n-1\\}$, where $n$ is the number of nodes, + or if `k` is such that the computed edge probability + $p = \\frac{2km}{n(n-k)}$ does not satisfy $0 < p < 1$. + + Examples + -------- + >>> G = nx.karate_club_graph() + >>> nr, nr_rd = nx.non_randomness(G, 2) + >>> nr, nr_rd = nx.non_randomness(G, 2, "weight") + + When the number of communities `k` is not specified, + :func:`~networkx.algorithms.community.label_propagation_communities` + is used to compute it. + This algorithm can give different results depending on + the order of nodes and edges in the graph. + For example, while the following graphs are identical, + computing the non-randomness of each of them yields different results: + + >>> G1, G2 = nx.Graph(), nx.Graph() + >>> G1.add_edges_from([(0, 1), (1, 2), (1, 3), (3, 4)]) + >>> G2.add_edges_from([(0, 1), (1, 3), (1, 2), (3, 4)]) + >>> [round(r, 6) for r in nx.non_randomness(G1)] + [-1.847759, -5.842437] + >>> [round(r, 6) for r in nx.non_randomness(G2)] + Traceback (most recent call last): + ... + ValueError: invalid number of communities for graph with 5 nodes and 4 edges: 2 + + This is because the community detection algorithm finds + 1 community in `G1` and 2 communities in `G2`. + This can be resolved by specifying the number of communities `k`: + + >>> [round(r, 6) for r in nx.non_randomness(G2, k=1)] + [-1.847759, -5.842437] + + Notes + ----- + If a `weight` argument is passed, this algorithm will use the eigenvalues + of the weighted adjacency matrix instead. + + The output of this function corresponds to (4.4) and (4.5) in [1]_. + A lower value of $R^*_G$ indicates a more random graph; + one can think of $1 - \\Phi(R_G^*)$ as the similarity + between the graph and a random graph, + where $\\Phi(x)$ is the cumulative distribution function + of the standard normal distribution. + + Theorem 2 in [2]_ states that for any graph $G$ + with $n$ nodes, $m$ edges, and $k$ communities, + its non-randomness is bounded below by the non-randomness of an + $r$-regular graph (a graph where each node has degree $r$), + and bounded above by the non-randomness of an $l$-complete graph + (a graph where each community is a clique of $l$ nodes). + + References + ---------- + .. [1] Xiaowei Ying and Xintao Wu, + On Randomness Measures for Social Networks, + SIAM International Conference on Data Mining. 2009 + https://doi.org/10.1137/1.9781611972795.61 + .. [2] Ying, Xiaowei & Wu, Leting & Wu, Xintao. (2012). + A Spectrum-Based Framework for Quantifying Randomness of Social Networks. + IEEE Transactions on Knowledge and Data Engineering 23(12):1842--1856. + https://dl.acm.org/doi/abs/10.1109/TKDE.2010.218 + """ + import numpy as np + + # corner case: graph has no edges + if nx.is_empty(G): + raise nx.NetworkXError("non_randomness not applicable to empty graphs") + if not nx.is_connected(G): + raise nx.NetworkXException("Non connected graph.") + if len(list(nx.selfloop_edges(G))) > 0: + raise nx.NetworkXError("Graph must not contain self-loops") + + n = G.number_of_nodes() + m = G.number_of_edges() + + if k is None: + k = len(tuple(nx.community.label_propagation_communities(G))) + if not 1 <= k < n or not 0 < (p := (2 * k * m) / (n * (n - k))) < 1: + err = ( + f"invalid number of communities for graph with {n} nodes and {m} edges: {k}" + ) + raise ValueError(err) + + # eq. 4.4 + eigenvalues = np.linalg.eigvals(nx.to_numpy_array(G, weight=weight)) + nr = float(np.real(np.sum(eigenvalues[:k]))) + + # eq. 4.5 + nr_rd = (nr - ((n - 2 * k) * p + k)) / math.sqrt(2 * k * p * (1 - p)) + + return nr, nr_rd diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/perfect_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/perfect_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..d84cafb527310173931e8176d5851c2e420a710b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/perfect_graph.py @@ -0,0 +1,73 @@ +import itertools + +import networkx as nx +from networkx.utils.decorators import not_implemented_for + +__all__ = ["is_perfect_graph"] + + +@nx._dispatchable +@not_implemented_for("directed") +@not_implemented_for("multigraph") +def is_perfect_graph(G): + r"""Return True if G is a perfect graph, else False. + + A graph G is perfect if, for every induced subgraph H of G, the chromatic + number of H equals the size of the largest clique in H. + + According to the **Strong Perfect Graph Theorem (SPGT)**: + A graph is perfect if and only if neither the graph G nor its complement + :math:`\overline{G}` contains an **induced odd hole** — an induced cycle of + odd length at least five without chords. + + Parameters + ---------- + G : NetworkX Graph + The graph to check. Must be a finite, simple, undirected graph. + + Returns + ------- + bool + True if G is a perfect graph, else False. + + Notes + ----- + This function uses a direct approach: cycle enumeration to detect + chordless odd cycles in G and :math:`\overline{G}`. This implementation + runs in exponential time in the worst case, since the number of chordless + cycles can grow exponentially. + + The perfect-graph recognition problem is theoretically solvable in + polynomial time. Chudnovsky *et al.* (2006) proved it can be solved in + :math:`O(n^9)` time via a complex structural decomposition [1]_, [2]_. + This implementation opts for a direct, transparent check rather than + implementing that high-degree polynomial-time decomposition algorithm. + + See Also + -------- + is_chordal, is_bipartite : + Related checks for specific categories of perfect graphs, such as chordal + graphs, and bipartite graphs. + chordless_cycles : + Used to detect "holes" in the graph + + References + ---------- + .. [1] M. Chudnovsky, N. Robertson, P. Seymour, and R. Thomas, + *The Strong Perfect Graph Theorem*, + Annals of Mathematics, vol. 164, no. 1, pp. 51–229, 2006. + https://doi.org/10.4007/annals.2006.164.51 + .. [2] M. Chudnovsky, G. Cornuéjols, X. Liu, P. Seymour, and K. Vušković, + *Recognizing Berge Graphs*, + Combinatorica 25(2): 143–186, 2005. + DOI: 10.1007/s00493-005-0003-8 + Preprint available at: + https://web.math.princeton.edu/~pds/papers/algexp/Bergealg.pdf + """ + + return not any( + (len(c) >= 5) and (len(c) % 2 == 1) + for c in itertools.chain( + nx.chordless_cycles(G), nx.chordless_cycles(nx.complement(G)) + ) + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planar_drawing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planar_drawing.py new file mode 100644 index 0000000000000000000000000000000000000000..ea25809b6aeb198b23b44fe9878775d11b7e109c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planar_drawing.py @@ -0,0 +1,464 @@ +from collections import defaultdict + +import networkx as nx + +__all__ = ["combinatorial_embedding_to_pos"] + + +def combinatorial_embedding_to_pos(embedding, fully_triangulate=False): + """Assigns every node a (x, y) position based on the given embedding + + The algorithm iteratively inserts nodes of the input graph in a certain + order and rearranges previously inserted nodes so that the planar drawing + stays valid. This is done efficiently by only maintaining relative + positions during the node placements and calculating the absolute positions + at the end. For more information see [1]_. + + Parameters + ---------- + embedding : nx.PlanarEmbedding + This defines the order of the edges + + fully_triangulate : bool + If set to True the algorithm adds edges to a copy of the input + embedding and makes it chordal. + + Returns + ------- + pos : dict + Maps each node to a tuple that defines the (x, y) position + + References + ---------- + .. [1] M. Chrobak and T.H. Payne: + A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989 + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677 + + """ + if len(embedding.nodes()) < 4: + # Position the node in any triangle + default_positions = [(0, 0), (2, 0), (1, 1)] + pos = {} + for i, v in enumerate(embedding.nodes()): + pos[v] = default_positions[i] + return pos + + embedding, outer_face = triangulate_embedding(embedding, fully_triangulate) + + # The following dicts map a node to another node + # If a node is not in the key set it means that the node is not yet in G_k + # If a node maps to None then the corresponding subtree does not exist + left_t_child = {} + right_t_child = {} + + # The following dicts map a node to an integer + delta_x = {} + y_coordinate = {} + + node_list = get_canonical_ordering(embedding, outer_face) + + # 1. Phase: Compute relative positions + + # Initialization + v1, v2, v3 = node_list[0][0], node_list[1][0], node_list[2][0] + + delta_x[v1] = 0 + y_coordinate[v1] = 0 + right_t_child[v1] = v3 + left_t_child[v1] = None + + delta_x[v2] = 1 + y_coordinate[v2] = 0 + right_t_child[v2] = None + left_t_child[v2] = None + + delta_x[v3] = 1 + y_coordinate[v3] = 1 + right_t_child[v3] = v2 + left_t_child[v3] = None + + for k in range(3, len(node_list)): + vk, contour_nbrs = node_list[k] + wp = contour_nbrs[0] + wp1 = contour_nbrs[1] + wq = contour_nbrs[-1] + wq1 = contour_nbrs[-2] + adds_mult_tri = len(contour_nbrs) > 2 + + # Stretch gaps: + delta_x[wp1] += 1 + delta_x[wq] += 1 + + delta_x_wp_wq = sum(delta_x[x] for x in contour_nbrs[1:]) + + # Adjust offsets + delta_x[vk] = (-y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2 + y_coordinate[vk] = (y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2 + delta_x[wq] = delta_x_wp_wq - delta_x[vk] + if adds_mult_tri: + delta_x[wp1] -= delta_x[vk] + + # Install v_k: + right_t_child[wp] = vk + right_t_child[vk] = wq + if adds_mult_tri: + left_t_child[vk] = wp1 + right_t_child[wq1] = None + else: + left_t_child[vk] = None + + # 2. Phase: Set absolute positions + pos = {} + pos[v1] = (0, y_coordinate[v1]) + remaining_nodes = [v1] + while remaining_nodes: + parent_node = remaining_nodes.pop() + + # Calculate position for left child + set_position( + parent_node, left_t_child, remaining_nodes, delta_x, y_coordinate, pos + ) + # Calculate position for right child + set_position( + parent_node, right_t_child, remaining_nodes, delta_x, y_coordinate, pos + ) + return pos + + +def set_position(parent, tree, remaining_nodes, delta_x, y_coordinate, pos): + """Helper method to calculate the absolute position of nodes.""" + child = tree[parent] + parent_node_x = pos[parent][0] + if child is not None: + # Calculate pos of child + child_x = parent_node_x + delta_x[child] + pos[child] = (child_x, y_coordinate[child]) + # Remember to calculate pos of its children + remaining_nodes.append(child) + + +def get_canonical_ordering(embedding, outer_face): + """Returns a canonical ordering of the nodes + + The canonical ordering of nodes (v1, ..., vn) must fulfill the following + conditions: + (See Lemma 1 in [2]_) + + - For the subgraph G_k of the input graph induced by v1, ..., vk it holds: + - 2-connected + - internally triangulated + - the edge (v1, v2) is part of the outer face + - For a node v(k+1) the following holds: + - The node v(k+1) is part of the outer face of G_k + - It has at least two neighbors in G_k + - All neighbors of v(k+1) in G_k lie consecutively on the outer face of + G_k (excluding the edge (v1, v2)). + + The algorithm used here starts with G_n (containing all nodes). It first + selects the nodes v1 and v2. And then tries to find the order of the other + nodes by checking which node can be removed in order to fulfill the + conditions mentioned above. This is done by calculating the number of + chords of nodes on the outer face. For more information see [1]_. + + Parameters + ---------- + embedding : nx.PlanarEmbedding + The embedding must be triangulated + outer_face : list + The nodes on the outer face of the graph + + Returns + ------- + ordering : list + A list of tuples `(vk, wp_wq)`. Here `vk` is the node at this position + in the canonical ordering. The element `wp_wq` is a list of nodes that + make up the outer face of G_k. + + References + ---------- + .. [1] Steven Chaplick. + Canonical Orders of Planar Graphs and (some of) Their Applications 2015 + https://wuecampus2.uni-wuerzburg.de/moodle/pluginfile.php/545727/mod_resource/content/0/vg-ss15-vl03-canonical-orders-druckversion.pdf + .. [2] M. Chrobak and T.H. Payne: + A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989 + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677 + + """ + v1 = outer_face[0] + v2 = outer_face[1] + chords = defaultdict(int) # Maps nodes to the number of their chords + marked_nodes = set() + ready_to_pick = set(outer_face) + + # Initialize outer_face_ccw_nbr (do not include v1 -> v2) + outer_face_ccw_nbr = {} + prev_nbr = v2 + for idx in range(2, len(outer_face)): + outer_face_ccw_nbr[prev_nbr] = outer_face[idx] + prev_nbr = outer_face[idx] + outer_face_ccw_nbr[prev_nbr] = v1 + + # Initialize outer_face_cw_nbr (do not include v2 -> v1) + outer_face_cw_nbr = {} + prev_nbr = v1 + for idx in range(len(outer_face) - 1, 0, -1): + outer_face_cw_nbr[prev_nbr] = outer_face[idx] + prev_nbr = outer_face[idx] + + def is_outer_face_nbr(x, y): + if x not in outer_face_ccw_nbr: + return outer_face_cw_nbr[x] == y + if x not in outer_face_cw_nbr: + return outer_face_ccw_nbr[x] == y + return outer_face_ccw_nbr[x] == y or outer_face_cw_nbr[x] == y + + def is_on_outer_face(x): + return x not in marked_nodes and (x in outer_face_ccw_nbr or x == v1) + + # Initialize number of chords + for v in outer_face: + for nbr in embedding.neighbors_cw_order(v): + if is_on_outer_face(nbr) and not is_outer_face_nbr(v, nbr): + chords[v] += 1 + ready_to_pick.discard(v) + + # Initialize canonical_ordering + canonical_ordering = [None] * len(embedding.nodes()) + canonical_ordering[0] = (v1, []) + canonical_ordering[1] = (v2, []) + ready_to_pick.discard(v1) + ready_to_pick.discard(v2) + + for k in range(len(embedding.nodes()) - 1, 1, -1): + # 1. Pick v from ready_to_pick + v = ready_to_pick.pop() + marked_nodes.add(v) + + # v has exactly two neighbors on the outer face (wp and wq) + wp = None + wq = None + # Iterate over neighbors of v to find wp and wq + nbr_iterator = iter(embedding.neighbors_cw_order(v)) + while True: + nbr = next(nbr_iterator) + if nbr in marked_nodes: + # Only consider nodes that are not yet removed + continue + if is_on_outer_face(nbr): + # nbr is either wp or wq + if nbr == v1: + wp = v1 + elif nbr == v2: + wq = v2 + else: + if outer_face_cw_nbr[nbr] == v: + # nbr is wp + wp = nbr + else: + # nbr is wq + wq = nbr + if wp is not None and wq is not None: + # We don't need to iterate any further + break + + # Obtain new nodes on outer face (neighbors of v from wp to wq) + wp_wq = [wp] + nbr = wp + while nbr != wq: + # Get next neighbor (clockwise on the outer face) + next_nbr = embedding[v][nbr]["ccw"] + wp_wq.append(next_nbr) + # Update outer face + outer_face_cw_nbr[nbr] = next_nbr + outer_face_ccw_nbr[next_nbr] = nbr + # Move to next neighbor of v + nbr = next_nbr + + if len(wp_wq) == 2: + # There was a chord between wp and wq, decrease number of chords + chords[wp] -= 1 + if chords[wp] == 0: + ready_to_pick.add(wp) + chords[wq] -= 1 + if chords[wq] == 0: + ready_to_pick.add(wq) + else: + # Update all chords involving w_(p+1) to w_(q-1) + new_face_nodes = set(wp_wq[1:-1]) + for w in new_face_nodes: + # If we do not find a chord for w later we can pick it next + ready_to_pick.add(w) + for nbr in embedding.neighbors_cw_order(w): + if is_on_outer_face(nbr) and not is_outer_face_nbr(w, nbr): + # There is a chord involving w + chords[w] += 1 + ready_to_pick.discard(w) + if nbr not in new_face_nodes: + # Also increase chord for the neighbor + # We only iterator over new_face_nodes + chords[nbr] += 1 + ready_to_pick.discard(nbr) + # Set the canonical ordering node and the list of contour neighbors + canonical_ordering[k] = (v, wp_wq) + + return canonical_ordering + + +def triangulate_face(embedding, v1, v2): + """Triangulates the face given by half edge (v, w) + + Parameters + ---------- + embedding : nx.PlanarEmbedding + v1 : node + The half-edge (v1, v2) belongs to the face that gets triangulated + v2 : node + """ + _, v3 = embedding.next_face_half_edge(v1, v2) + _, v4 = embedding.next_face_half_edge(v2, v3) + if v1 in (v2, v3): + # The component has less than 3 nodes + return + while v1 != v4: + # Add edge if not already present on other side + if embedding.has_edge(v1, v3): + # Cannot triangulate at this position + v1, v2, v3 = v2, v3, v4 + else: + # Add edge for triangulation + embedding.add_half_edge(v1, v3, ccw=v2) + embedding.add_half_edge(v3, v1, cw=v2) + v1, v2, v3 = v1, v3, v4 + # Get next node + _, v4 = embedding.next_face_half_edge(v2, v3) + + +def triangulate_embedding(embedding, fully_triangulate=True): + """Triangulates the embedding. + + Traverses faces of the embedding and adds edges to a copy of the + embedding to triangulate it. + The method also ensures that the resulting graph is 2-connected by adding + edges if the same vertex is contained twice on a path around a face. + + Parameters + ---------- + embedding : nx.PlanarEmbedding + The input graph must contain at least 3 nodes. + + fully_triangulate : bool + If set to False the face with the most nodes is chooses as outer face. + This outer face does not get triangulated. + + Returns + ------- + (embedding, outer_face) : (nx.PlanarEmbedding, list) tuple + The element `embedding` is a new embedding containing all edges from + the input embedding and the additional edges to triangulate the graph. + The element `outer_face` is a list of nodes that lie on the outer face. + If the graph is fully triangulated these are three arbitrary connected + nodes. + + """ + if len(embedding.nodes) <= 1: + return embedding, list(embedding.nodes) + embedding = nx.PlanarEmbedding(embedding) + + # Get a list with a node for each connected component + component_nodes = [next(iter(x)) for x in nx.connected_components(embedding)] + + # 1. Make graph a single component (add edge between components) + for i in range(len(component_nodes) - 1): + v1 = component_nodes[i] + v2 = component_nodes[i + 1] + embedding.connect_components(v1, v2) + + # 2. Calculate faces, ensure 2-connectedness and determine outer face + outer_face = [] # A face with the most number of nodes + face_list = [] + edges_visited = set() # Used to keep track of already visited faces + for v in embedding.nodes(): + for w in embedding.neighbors_cw_order(v): + new_face = make_bi_connected(embedding, v, w, edges_visited) + if new_face: + # Found a new face + face_list.append(new_face) + if len(new_face) > len(outer_face): + # The face is a candidate to be the outer face + outer_face = new_face + + # 3. Triangulate (internal) faces + for face in face_list: + if face is not outer_face or fully_triangulate: + # Triangulate this face + triangulate_face(embedding, face[0], face[1]) + + if fully_triangulate: + v1 = outer_face[0] + v2 = outer_face[1] + v3 = embedding[v2][v1]["ccw"] + outer_face = [v1, v2, v3] + + return embedding, outer_face + + +def make_bi_connected(embedding, starting_node, outgoing_node, edges_counted): + """Triangulate a face and make it 2-connected + + This method also adds all edges on the face to `edges_counted`. + + Parameters + ---------- + embedding: nx.PlanarEmbedding + The embedding that defines the faces + starting_node : node + A node on the face + outgoing_node : node + A node such that the half edge (starting_node, outgoing_node) belongs + to the face + edges_counted: set + Set of all half-edges that belong to a face that have been visited + + Returns + ------- + face_nodes: list + A list of all nodes at the border of this face + """ + + # Check if the face has already been calculated + if (starting_node, outgoing_node) in edges_counted: + # This face was already counted + return [] + edges_counted.add((starting_node, outgoing_node)) + + # Add all edges to edges_counted which have this face to their left + v1 = starting_node + v2 = outgoing_node + face_list = [starting_node] # List of nodes around the face + face_set = set(face_list) # Set for faster queries + _, v3 = embedding.next_face_half_edge(v1, v2) + + # Move the nodes v1, v2, v3 around the face: + while v2 != starting_node or v3 != outgoing_node: + if v1 == v2: + raise nx.NetworkXException("Invalid half-edge") + # cycle is not completed yet + if v2 in face_set: + # v2 encountered twice: Add edge to ensure 2-connectedness + embedding.add_half_edge(v1, v3, ccw=v2) + embedding.add_half_edge(v3, v1, cw=v2) + edges_counted.add((v2, v3)) + edges_counted.add((v3, v1)) + v2 = v1 + else: + face_set.add(v2) + face_list.append(v2) + + # set next edge + v1 = v2 + v2, v3 = embedding.next_face_half_edge(v2, v3) + + # remember that this edge has been counted + edges_counted.add((v1, v2)) + + return face_list diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planarity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planarity.py new file mode 100644 index 0000000000000000000000000000000000000000..52f0b576071c969dd0449a941493573b5c807448 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/planarity.py @@ -0,0 +1,1463 @@ +from collections import defaultdict +from copy import deepcopy + +import networkx as nx + +__all__ = ["check_planarity", "is_planar", "PlanarEmbedding"] + + +@nx._dispatchable +def is_planar(G): + """Returns True if and only if `G` is planar. + + A graph is *planar* iff it can be drawn in a plane without + any edge intersections. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + bool + Whether the graph is planar. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2)]) + >>> nx.is_planar(G) + True + >>> nx.is_planar(nx.complete_graph(5)) + False + + See Also + -------- + check_planarity : + Check if graph is planar *and* return a `PlanarEmbedding` instance if True. + """ + + return check_planarity(G, counterexample=False)[0] + + +@nx._dispatchable(returns_graph=True) +def check_planarity(G, counterexample=False): + """Check if a graph is planar and return a counterexample or an embedding. + + A graph is planar iff it can be drawn in a plane without + any edge intersections. + + Parameters + ---------- + G : NetworkX graph + counterexample : bool + A Kuratowski subgraph (to proof non planarity) is only returned if set + to true. + + Returns + ------- + (is_planar, certificate) : (bool, NetworkX graph) tuple + is_planar is true if the graph is planar. + If the graph is planar `certificate` is a PlanarEmbedding + otherwise it is a Kuratowski subgraph. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2)]) + >>> is_planar, P = nx.check_planarity(G) + >>> print(is_planar) + True + + When `G` is planar, a `PlanarEmbedding` instance is returned: + + >>> P.get_data() + {0: [1, 2], 1: [0], 2: [0]} + + Notes + ----- + A (combinatorial) embedding consists of cyclic orderings of the incident + edges at each vertex. Given such an embedding there are multiple approaches + discussed in literature to drawing the graph (subject to various + constraints, e.g. integer coordinates), see e.g. [2]. + + The planarity check algorithm and extraction of the combinatorial embedding + is based on the Left-Right Planarity Test [1]. + + A counterexample is only generated if the corresponding parameter is set, + because the complexity of the counterexample generation is higher. + + See also + -------- + is_planar : + Check for planarity without creating a `PlanarEmbedding` or counterexample. + + References + ---------- + .. [1] Ulrik Brandes: + The Left-Right Planarity Test + 2009 + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.217.9208 + .. [2] Takao Nishizeki, Md Saidur Rahman: + Planar graph drawing + Lecture Notes Series on Computing: Volume 12 + 2004 + """ + + planarity_state = LRPlanarity(G) + embedding = planarity_state.lr_planarity() + if embedding is None: + # graph is not planar + if counterexample: + return False, get_counterexample(G) + else: + return False, None + else: + # graph is planar + return True, embedding + + +@nx._dispatchable(returns_graph=True) +def check_planarity_recursive(G, counterexample=False): + """Recursive version of :meth:`check_planarity`.""" + planarity_state = LRPlanarity(G) + embedding = planarity_state.lr_planarity_recursive() + if embedding is None: + # graph is not planar + if counterexample: + return False, get_counterexample_recursive(G) + else: + return False, None + else: + # graph is planar + return True, embedding + + +@nx._dispatchable(returns_graph=True) +def get_counterexample(G): + """Obtains a Kuratowski subgraph. + + Raises nx.NetworkXException if G is planar. + + The function removes edges such that the graph is still not planar. + At some point the removal of any edge would make the graph planar. + This subgraph must be a Kuratowski subgraph. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + subgraph : NetworkX graph + A Kuratowski subgraph that proves that G is not planar. + + """ + # copy graph + G = nx.Graph(G) + + if check_planarity(G)[0]: + raise nx.NetworkXException("G is planar - no counter example.") + + # find Kuratowski subgraph + subgraph = nx.Graph() + for u in G: + nbrs = list(G[u]) + for v in nbrs: + G.remove_edge(u, v) + if check_planarity(G)[0]: + G.add_edge(u, v) + subgraph.add_edge(u, v) + + return subgraph + + +@nx._dispatchable(returns_graph=True) +def get_counterexample_recursive(G): + """Recursive version of :meth:`get_counterexample`.""" + + # copy graph + G = nx.Graph(G) + + if check_planarity_recursive(G)[0]: + raise nx.NetworkXException("G is planar - no counter example.") + + # find Kuratowski subgraph + subgraph = nx.Graph() + for u in G: + nbrs = list(G[u]) + for v in nbrs: + G.remove_edge(u, v) + if check_planarity_recursive(G)[0]: + G.add_edge(u, v) + subgraph.add_edge(u, v) + + return subgraph + + +class Interval: + """Represents a set of return edges. + + All return edges in an interval induce a same constraint on the contained + edges, which means that all edges must either have a left orientation or + all edges must have a right orientation. + """ + + def __init__(self, low=None, high=None): + self.low = low + self.high = high + + def empty(self): + """Check if the interval is empty""" + return self.low is None and self.high is None + + def copy(self): + """Returns a copy of this interval""" + return Interval(self.low, self.high) + + def conflicting(self, b, planarity_state): + """Returns True if interval I conflicts with edge b""" + return ( + not self.empty() + and planarity_state.lowpt[self.high] > planarity_state.lowpt[b] + ) + + +class ConflictPair: + """Represents a different constraint between two intervals. + + The edges in the left interval must have a different orientation than + the one in the right interval. + """ + + def __init__(self, left=Interval(), right=Interval()): + self.left = left + self.right = right + + def swap(self): + """Swap left and right intervals""" + temp = self.left + self.left = self.right + self.right = temp + + def lowest(self, planarity_state): + """Returns the lowest lowpoint of a conflict pair""" + if self.left.empty(): + return planarity_state.lowpt[self.right.low] + if self.right.empty(): + return planarity_state.lowpt[self.left.low] + return min( + planarity_state.lowpt[self.left.low], planarity_state.lowpt[self.right.low] + ) + + +def top_of_stack(l): + """Returns the element on top of the stack.""" + if not l: + return None + return l[-1] + + +class LRPlanarity: + """A class to maintain the state during planarity check.""" + + __slots__ = [ + "G", + "roots", + "height", + "lowpt", + "lowpt2", + "nesting_depth", + "parent_edge", + "DG", + "adjs", + "ordered_adjs", + "ref", + "side", + "S", + "stack_bottom", + "lowpt_edge", + "left_ref", + "right_ref", + "embedding", + ] + + def __init__(self, G): + # copy G without adding self-loops + self.G = nx.Graph() + self.G.add_nodes_from(G.nodes) + for e in G.edges: + if e[0] != e[1]: + self.G.add_edge(e[0], e[1]) + + self.roots = [] + + # distance from tree root + self.height = defaultdict(lambda: None) + + self.lowpt = {} # height of lowest return point of an edge + self.lowpt2 = {} # height of second lowest return point + self.nesting_depth = {} # for nesting order + + # None -> missing edge + self.parent_edge = defaultdict(lambda: None) + + # oriented DFS graph + self.DG = nx.DiGraph() + self.DG.add_nodes_from(G.nodes) + + self.adjs = {} + self.ordered_adjs = {} + + self.ref = defaultdict(lambda: None) + self.side = defaultdict(lambda: 1) + + # stack of conflict pairs + self.S = [] + self.stack_bottom = {} + self.lowpt_edge = {} + + self.left_ref = {} + self.right_ref = {} + + self.embedding = PlanarEmbedding() + + def lr_planarity(self): + """Execute the LR planarity test. + + Returns + ------- + embedding : dict + If the graph is planar an embedding is returned. Otherwise None. + """ + if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6: + # graph is not planar + return None + + # make adjacency lists for dfs + for v in self.G: + self.adjs[v] = list(self.G[v]) + + # orientation of the graph by depth first search traversal + for v in self.G: + if self.height[v] is None: + self.height[v] = 0 + self.roots.append(v) + self.dfs_orientation(v) + + # Free no longer used variables + self.G = None + self.lowpt2 = None + self.adjs = None + + # testing + for v in self.DG: # sort the adjacency lists by nesting depth + # note: this sorting leads to non linear time + self.ordered_adjs[v] = sorted( + self.DG[v], key=lambda x: self.nesting_depth[(v, x)] + ) + for v in self.roots: + if not self.dfs_testing(v): + return None + + # Free no longer used variables + self.height = None + self.lowpt = None + self.S = None + self.stack_bottom = None + self.lowpt_edge = None + + for e in self.DG.edges: + self.nesting_depth[e] = self.sign(e) * self.nesting_depth[e] + + self.embedding.add_nodes_from(self.DG.nodes) + for v in self.DG: + # sort the adjacency lists again + self.ordered_adjs[v] = sorted( + self.DG[v], key=lambda x: self.nesting_depth[(v, x)] + ) + # initialize the embedding + previous_node = None + for w in self.ordered_adjs[v]: + self.embedding.add_half_edge(v, w, ccw=previous_node) + previous_node = w + + # Free no longer used variables + self.DG = None + self.nesting_depth = None + self.ref = None + + # compute the complete embedding + for v in self.roots: + self.dfs_embedding(v) + + # Free no longer used variables + self.roots = None + self.parent_edge = None + self.ordered_adjs = None + self.left_ref = None + self.right_ref = None + self.side = None + + return self.embedding + + def lr_planarity_recursive(self): + """Recursive version of :meth:`lr_planarity`.""" + if self.G.order() > 2 and self.G.size() > 3 * self.G.order() - 6: + # graph is not planar + return None + + # orientation of the graph by depth first search traversal + for v in self.G: + if self.height[v] is None: + self.height[v] = 0 + self.roots.append(v) + self.dfs_orientation_recursive(v) + + # Free no longer used variable + self.G = None + + # testing + for v in self.DG: # sort the adjacency lists by nesting depth + # note: this sorting leads to non linear time + self.ordered_adjs[v] = sorted( + self.DG[v], key=lambda x: self.nesting_depth[(v, x)] + ) + for v in self.roots: + if not self.dfs_testing_recursive(v): + return None + + for e in self.DG.edges: + self.nesting_depth[e] = self.sign_recursive(e) * self.nesting_depth[e] + + self.embedding.add_nodes_from(self.DG.nodes) + for v in self.DG: + # sort the adjacency lists again + self.ordered_adjs[v] = sorted( + self.DG[v], key=lambda x: self.nesting_depth[(v, x)] + ) + # initialize the embedding + previous_node = None + for w in self.ordered_adjs[v]: + self.embedding.add_half_edge(v, w, ccw=previous_node) + previous_node = w + + # compute the complete embedding + for v in self.roots: + self.dfs_embedding_recursive(v) + + return self.embedding + + def dfs_orientation(self, v): + """Orient the graph by DFS, compute lowpoints and nesting order.""" + # the recursion stack + dfs_stack = [v] + # index of next edge to handle in adjacency list of each node + ind = defaultdict(lambda: 0) + # boolean to indicate whether to skip the initial work for an edge + skip_init = defaultdict(lambda: False) + + while dfs_stack: + v = dfs_stack.pop() + e = self.parent_edge[v] + + for w in self.adjs[v][ind[v] :]: + vw = (v, w) + + if not skip_init[vw]: + if (v, w) in self.DG.edges or (w, v) in self.DG.edges: + ind[v] += 1 + continue # the edge was already oriented + + self.DG.add_edge(v, w) # orient the edge + + self.lowpt[vw] = self.height[v] + self.lowpt2[vw] = self.height[v] + if self.height[w] is None: # (v, w) is a tree edge + self.parent_edge[w] = vw + self.height[w] = self.height[v] + 1 + + dfs_stack.append(v) # revisit v after finishing w + dfs_stack.append(w) # visit w next + skip_init[vw] = True # don't redo this block + break # handle next node in dfs_stack (i.e. w) + else: # (v, w) is a back edge + self.lowpt[vw] = self.height[w] + + # determine nesting graph + self.nesting_depth[vw] = 2 * self.lowpt[vw] + if self.lowpt2[vw] < self.height[v]: # chordal + self.nesting_depth[vw] += 1 + + # update lowpoints of parent edge e + if e is not None: + if self.lowpt[vw] < self.lowpt[e]: + self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw]) + self.lowpt[e] = self.lowpt[vw] + elif self.lowpt[vw] > self.lowpt[e]: + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw]) + else: + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw]) + + ind[v] += 1 + + def dfs_orientation_recursive(self, v): + """Recursive version of :meth:`dfs_orientation`.""" + e = self.parent_edge[v] + for w in self.G[v]: + if (v, w) in self.DG.edges or (w, v) in self.DG.edges: + continue # the edge was already oriented + vw = (v, w) + self.DG.add_edge(v, w) # orient the edge + + self.lowpt[vw] = self.height[v] + self.lowpt2[vw] = self.height[v] + if self.height[w] is None: # (v, w) is a tree edge + self.parent_edge[w] = vw + self.height[w] = self.height[v] + 1 + self.dfs_orientation_recursive(w) + else: # (v, w) is a back edge + self.lowpt[vw] = self.height[w] + + # determine nesting graph + self.nesting_depth[vw] = 2 * self.lowpt[vw] + if self.lowpt2[vw] < self.height[v]: # chordal + self.nesting_depth[vw] += 1 + + # update lowpoints of parent edge e + if e is not None: + if self.lowpt[vw] < self.lowpt[e]: + self.lowpt2[e] = min(self.lowpt[e], self.lowpt2[vw]) + self.lowpt[e] = self.lowpt[vw] + elif self.lowpt[vw] > self.lowpt[e]: + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt[vw]) + else: + self.lowpt2[e] = min(self.lowpt2[e], self.lowpt2[vw]) + + def dfs_testing(self, v): + """Test for LR partition.""" + # the recursion stack + dfs_stack = [v] + # index of next edge to handle in adjacency list of each node + ind = defaultdict(lambda: 0) + # boolean to indicate whether to skip the initial work for an edge + skip_init = defaultdict(lambda: False) + + while dfs_stack: + v = dfs_stack.pop() + e = self.parent_edge[v] + # to indicate whether to skip the final block after the for loop + skip_final = False + + for w in self.ordered_adjs[v][ind[v] :]: + ei = (v, w) + + if not skip_init[ei]: + self.stack_bottom[ei] = top_of_stack(self.S) + + if ei == self.parent_edge[w]: # tree edge + dfs_stack.append(v) # revisit v after finishing w + dfs_stack.append(w) # visit w next + skip_init[ei] = True # don't redo this block + skip_final = True # skip final work after breaking + break # handle next node in dfs_stack (i.e. w) + else: # back edge + self.lowpt_edge[ei] = ei + self.S.append(ConflictPair(right=Interval(ei, ei))) + + # integrate new return edges + if self.lowpt[ei] < self.height[v]: + if w == self.ordered_adjs[v][0]: # e_i has return edge + self.lowpt_edge[e] = self.lowpt_edge[ei] + else: # add constraints of e_i + if not self.add_constraints(ei, e): + # graph is not planar + return False + + ind[v] += 1 + + if not skip_final: + # remove back edges returning to parent + if e is not None: # v isn't root + self.remove_back_edges(e) + + return True + + def dfs_testing_recursive(self, v): + """Recursive version of :meth:`dfs_testing`.""" + e = self.parent_edge[v] + for w in self.ordered_adjs[v]: + ei = (v, w) + self.stack_bottom[ei] = top_of_stack(self.S) + if ei == self.parent_edge[w]: # tree edge + if not self.dfs_testing_recursive(w): + return False + else: # back edge + self.lowpt_edge[ei] = ei + self.S.append(ConflictPair(right=Interval(ei, ei))) + + # integrate new return edges + if self.lowpt[ei] < self.height[v]: + if w == self.ordered_adjs[v][0]: # e_i has return edge + self.lowpt_edge[e] = self.lowpt_edge[ei] + else: # add constraints of e_i + if not self.add_constraints(ei, e): + # graph is not planar + return False + + # remove back edges returning to parent + if e is not None: # v isn't root + self.remove_back_edges(e) + return True + + def add_constraints(self, ei, e): + P = ConflictPair() + # merge return edges of e_i into P.right + while True: + Q = self.S.pop() + if not Q.left.empty(): + Q.swap() + if not Q.left.empty(): # not planar + return False + if self.lowpt[Q.right.low] > self.lowpt[e]: + # merge intervals + if P.right.empty(): # topmost interval + P.right = Q.right.copy() + else: + self.ref[P.right.low] = Q.right.high + P.right.low = Q.right.low + else: # align + self.ref[Q.right.low] = self.lowpt_edge[e] + if top_of_stack(self.S) == self.stack_bottom[ei]: + break + # merge conflicting return edges of e_1,...,e_i-1 into P.L + while top_of_stack(self.S).left.conflicting(ei, self) or top_of_stack( + self.S + ).right.conflicting(ei, self): + Q = self.S.pop() + if Q.right.conflicting(ei, self): + Q.swap() + if Q.right.conflicting(ei, self): # not planar + return False + # merge interval below lowpt(e_i) into P.R + self.ref[P.right.low] = Q.right.high + if Q.right.low is not None: + P.right.low = Q.right.low + + if P.left.empty(): # topmost interval + P.left = Q.left.copy() + else: + self.ref[P.left.low] = Q.left.high + P.left.low = Q.left.low + + if not (P.left.empty() and P.right.empty()): + self.S.append(P) + return True + + def remove_back_edges(self, e): + u = e[0] + # trim back edges ending at parent u + # drop entire conflict pairs + while self.S and top_of_stack(self.S).lowest(self) == self.height[u]: + P = self.S.pop() + if P.left.low is not None: + self.side[P.left.low] = -1 + + if self.S: # one more conflict pair to consider + P = self.S.pop() + # trim left interval + while P.left.high is not None and P.left.high[1] == u: + P.left.high = self.ref[P.left.high] + if P.left.high is None and P.left.low is not None: + # just emptied + self.ref[P.left.low] = P.right.low + self.side[P.left.low] = -1 + P.left.low = None + # trim right interval + while P.right.high is not None and P.right.high[1] == u: + P.right.high = self.ref[P.right.high] + if P.right.high is None and P.right.low is not None: + # just emptied + self.ref[P.right.low] = P.left.low + self.side[P.right.low] = -1 + P.right.low = None + self.S.append(P) + + # side of e is side of a highest return edge + if self.lowpt[e] < self.height[u]: # e has return edge + hl = top_of_stack(self.S).left.high + hr = top_of_stack(self.S).right.high + + if hl is not None and (hr is None or self.lowpt[hl] > self.lowpt[hr]): + self.ref[e] = hl + else: + self.ref[e] = hr + + def dfs_embedding(self, v): + """Completes the embedding.""" + # the recursion stack + dfs_stack = [v] + # index of next edge to handle in adjacency list of each node + ind = defaultdict(lambda: 0) + + while dfs_stack: + v = dfs_stack.pop() + + for w in self.ordered_adjs[v][ind[v] :]: + ind[v] += 1 + ei = (v, w) + + if ei == self.parent_edge[w]: # tree edge + self.embedding.add_half_edge_first(w, v) + self.left_ref[v] = w + self.right_ref[v] = w + + dfs_stack.append(v) # revisit v after finishing w + dfs_stack.append(w) # visit w next + break # handle next node in dfs_stack (i.e. w) + else: # back edge + if self.side[ei] == 1: + self.embedding.add_half_edge(w, v, ccw=self.right_ref[w]) + else: + self.embedding.add_half_edge(w, v, cw=self.left_ref[w]) + self.left_ref[w] = v + + def dfs_embedding_recursive(self, v): + """Recursive version of :meth:`dfs_embedding`.""" + for w in self.ordered_adjs[v]: + ei = (v, w) + if ei == self.parent_edge[w]: # tree edge + self.embedding.add_half_edge_first(w, v) + self.left_ref[v] = w + self.right_ref[v] = w + self.dfs_embedding_recursive(w) + else: # back edge + if self.side[ei] == 1: + # place v directly after right_ref[w] in embed. list of w + self.embedding.add_half_edge(w, v, ccw=self.right_ref[w]) + else: + # place v directly before left_ref[w] in embed. list of w + self.embedding.add_half_edge(w, v, cw=self.left_ref[w]) + self.left_ref[w] = v + + def sign(self, e): + """Resolve the relative side of an edge to the absolute side.""" + # the recursion stack + dfs_stack = [e] + # dict to remember reference edges + old_ref = defaultdict(lambda: None) + + while dfs_stack: + e = dfs_stack.pop() + + if self.ref[e] is not None: + dfs_stack.append(e) # revisit e after finishing self.ref[e] + dfs_stack.append(self.ref[e]) # visit self.ref[e] next + old_ref[e] = self.ref[e] # remember value of self.ref[e] + self.ref[e] = None + else: + self.side[e] *= self.side[old_ref[e]] + + return self.side[e] + + def sign_recursive(self, e): + """Recursive version of :meth:`sign`.""" + if self.ref[e] is not None: + self.side[e] = self.side[e] * self.sign_recursive(self.ref[e]) + self.ref[e] = None + return self.side[e] + + +class PlanarEmbedding(nx.DiGraph): + """Represents a planar graph with its planar embedding. + + The planar embedding is given by a `combinatorial embedding + `_. + + .. note:: `check_planarity` is the preferred way to check if a graph is planar. + + **Neighbor ordering:** + + In comparison to a usual graph structure, the embedding also stores the + order of all neighbors for every vertex. + The order of the neighbors can be given in clockwise (cw) direction or + counterclockwise (ccw) direction. This order is stored as edge attributes + in the underlying directed graph. For the edge (u, v) the edge attribute + 'cw' is set to the neighbor of u that follows immediately after v in + clockwise direction. + + In order for a PlanarEmbedding to be valid it must fulfill multiple + conditions. It is possible to check if these conditions are fulfilled with + the method :meth:`check_structure`. + The conditions are: + + * Edges must go in both directions (because the edge attributes differ) + * Every edge must have a 'cw' and 'ccw' attribute which corresponds to a + correct planar embedding. + + As long as a PlanarEmbedding is invalid only the following methods should + be called: + + * :meth:`add_half_edge` + * :meth:`connect_components` + + Even though the graph is a subclass of nx.DiGraph, it can still be used + for algorithms that require undirected graphs, because the method + :meth:`is_directed` is overridden. This is possible, because a valid + PlanarGraph must have edges in both directions. + + **Half edges:** + + In methods like `add_half_edge` the term "half-edge" is used, which is + a term that is used in `doubly connected edge lists + `_. It is used + to emphasize that the edge is only in one direction and there exists + another half-edge in the opposite direction. + While conventional edges always have two faces (including outer face) next + to them, it is possible to assign each half-edge *exactly one* face. + For a half-edge (u, v) that is oriented such that u is below v then the + face that belongs to (u, v) is to the right of this half-edge. + + See Also + -------- + is_planar : + Preferred way to check if an existing graph is planar. + + check_planarity : + A convenient way to create a `PlanarEmbedding`. If not planar, + it returns a subgraph that shows this. + + Examples + -------- + + Create an embedding of a star graph (compare `nx.star_graph(3)`): + + >>> G = nx.PlanarEmbedding() + >>> G.add_half_edge(0, 1) + >>> G.add_half_edge(0, 2, ccw=1) + >>> G.add_half_edge(0, 3, ccw=2) + >>> G.add_half_edge(1, 0) + >>> G.add_half_edge(2, 0) + >>> G.add_half_edge(3, 0) + + Alternatively the same embedding can also be defined in counterclockwise + orientation. The following results in exactly the same PlanarEmbedding: + + >>> G = nx.PlanarEmbedding() + >>> G.add_half_edge(0, 1) + >>> G.add_half_edge(0, 3, cw=1) + >>> G.add_half_edge(0, 2, cw=3) + >>> G.add_half_edge(1, 0) + >>> G.add_half_edge(2, 0) + >>> G.add_half_edge(3, 0) + + After creating a graph, it is possible to validate that the PlanarEmbedding + object is correct: + + >>> G.check_structure() + + """ + + def __init__(self, incoming_graph_data=None, **attr): + super().__init__(incoming_graph_data=incoming_graph_data, **attr) + self.add_edge = self._forbidden + self.add_edges_from = self._forbidden + self.add_weighted_edges_from = self._forbidden + + def _forbidden(self, *args, **kwargs): + """Forbidden operation + + Any edge additions to a PlanarEmbedding should be done using + method `add_half_edge`. + """ + raise NotImplementedError( + "Use `add_half_edge` method to add edges to a PlanarEmbedding." + ) + + def get_data(self): + """Converts the adjacency structure into a better readable structure. + + Returns + ------- + embedding : dict + A dict mapping all nodes to a list of neighbors sorted in + clockwise order. + + See Also + -------- + set_data + + """ + embedding = {} + for v in self: + embedding[v] = list(self.neighbors_cw_order(v)) + return embedding + + def set_data(self, data): + """Inserts edges according to given sorted neighbor list. + + The input format is the same as the output format of get_data(). + + Parameters + ---------- + data : dict + A dict mapping all nodes to a list of neighbors sorted in + clockwise order. + + See Also + -------- + get_data + + """ + for v in data: + ref = None + for w in reversed(data[v]): + self.add_half_edge(v, w, cw=ref) + ref = w + + def remove_node(self, n): + """Remove node n. + + Removes the node n and all adjacent edges, updating the + PlanarEmbedding to account for any resulting edge removal. + Attempting to remove a non-existent node will raise an exception. + + Parameters + ---------- + n : node + A node in the graph + + Raises + ------ + NetworkXError + If n is not in the graph. + + See Also + -------- + remove_nodes_from + + """ + try: + for u in self._pred[n]: + succs_u = self._succ[u] + un_cw = succs_u[n]["cw"] + un_ccw = succs_u[n]["ccw"] + del succs_u[n] + del self._pred[u][n] + if n != un_cw: + succs_u[un_cw]["ccw"] = un_ccw + succs_u[un_ccw]["cw"] = un_cw + del self._node[n] + del self._succ[n] + del self._pred[n] + except KeyError as err: # NetworkXError if n not in self + raise nx.NetworkXError( + f"The node {n} is not in the planar embedding." + ) from err + nx._clear_cache(self) + + def remove_nodes_from(self, nodes): + """Remove multiple nodes. + + Parameters + ---------- + nodes : iterable container + A container of nodes (list, dict, set, etc.). If a node + in the container is not in the graph it is silently ignored. + + See Also + -------- + remove_node + + Notes + ----- + When removing nodes from an iterator over the graph you are changing, + a `RuntimeError` will be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_nodes)`, and pass this + object to `G.remove_nodes_from`. + + """ + for n in nodes: + if n in self._node: + self.remove_node(n) + # silently skip non-existing nodes + + def neighbors_cw_order(self, v): + """Generator for the neighbors of v in clockwise order. + + Parameters + ---------- + v : node + + Yields + ------ + node + + """ + succs = self._succ[v] + if not succs: + # v has no neighbors + return + start_node = next(reversed(succs)) + yield start_node + current_node = succs[start_node]["cw"] + while start_node != current_node: + yield current_node + current_node = succs[current_node]["cw"] + + def add_half_edge(self, start_node, end_node, *, cw=None, ccw=None): + """Adds a half-edge from `start_node` to `end_node`. + + If the half-edge is not the first one out of `start_node`, a reference + node must be provided either in the clockwise (parameter `cw`) or in + the counterclockwise (parameter `ccw`) direction. Only one of `cw`/`ccw` + can be specified (or neither in the case of the first edge). + Note that specifying a reference in the clockwise (`cw`) direction means + inserting the new edge in the first counterclockwise position with + respect to the reference (and vice-versa). + + Parameters + ---------- + start_node : node + Start node of inserted edge. + end_node : node + End node of inserted edge. + cw, ccw: node + End node of reference edge. + Omit or pass `None` if adding the first out-half-edge of `start_node`. + + + Raises + ------ + NetworkXException + If the `cw` or `ccw` node is not a successor of `start_node`. + If `start_node` has successors, but neither `cw` or `ccw` is provided. + If both `cw` and `ccw` are specified. + + See Also + -------- + connect_components + """ + + succs = self._succ.get(start_node) + if succs: + # there is already some edge out of start_node + leftmost_nbr = next(reversed(self._succ[start_node])) + if cw is not None: + if cw not in succs: + raise nx.NetworkXError("Invalid clockwise reference node.") + if ccw is not None: + raise nx.NetworkXError("Only one of cw/ccw can be specified.") + ref_ccw = succs[cw]["ccw"] + super().add_edge(start_node, end_node, cw=cw, ccw=ref_ccw) + succs[ref_ccw]["cw"] = end_node + succs[cw]["ccw"] = end_node + # when (cw == leftmost_nbr), the newly added neighbor is + # already at the end of dict self._succ[start_node] and + # takes the place of the former leftmost_nbr + move_leftmost_nbr_to_end = cw != leftmost_nbr + elif ccw is not None: + if ccw not in succs: + raise nx.NetworkXError("Invalid counterclockwise reference node.") + ref_cw = succs[ccw]["cw"] + super().add_edge(start_node, end_node, cw=ref_cw, ccw=ccw) + succs[ref_cw]["ccw"] = end_node + succs[ccw]["cw"] = end_node + move_leftmost_nbr_to_end = True + else: + raise nx.NetworkXError( + "Node already has out-half-edge(s), either cw or ccw reference node required." + ) + if move_leftmost_nbr_to_end: + # LRPlanarity (via self.add_half_edge_first()) requires that + # we keep track of the leftmost neighbor, which we accomplish + # by keeping it as the last key in dict self._succ[start_node] + succs[leftmost_nbr] = succs.pop(leftmost_nbr) + + else: + if cw is not None or ccw is not None: + raise nx.NetworkXError("Invalid reference node.") + # adding the first edge out of start_node + super().add_edge(start_node, end_node, ccw=end_node, cw=end_node) + + def check_structure(self): + """Runs without exceptions if this object is valid. + + Checks that the following properties are fulfilled: + + * Edges go in both directions (because the edge attributes differ). + * Every edge has a 'cw' and 'ccw' attribute which corresponds to a + correct planar embedding. + + Running this method verifies that the underlying Graph must be planar. + + Raises + ------ + NetworkXException + This exception is raised with a short explanation if the + PlanarEmbedding is invalid. + """ + # Check fundamental structure + for v in self: + try: + sorted_nbrs = set(self.neighbors_cw_order(v)) + except KeyError as err: + msg = f"Bad embedding. Missing orientation for a neighbor of {v}" + raise nx.NetworkXException(msg) from err + + unsorted_nbrs = set(self[v]) + if sorted_nbrs != unsorted_nbrs: + msg = "Bad embedding. Edge orientations not set correctly." + raise nx.NetworkXException(msg) + for w in self[v]: + # Check if opposite half-edge exists + if not self.has_edge(w, v): + msg = "Bad embedding. Opposite half-edge is missing." + raise nx.NetworkXException(msg) + + # Check planarity + counted_half_edges = set() + for component in nx.connected_components(self): + if len(component) == 1: + # Don't need to check single node component + continue + num_nodes = len(component) + num_half_edges = 0 + num_faces = 0 + for v in component: + for w in self.neighbors_cw_order(v): + num_half_edges += 1 + if (v, w) not in counted_half_edges: + # We encountered a new face + num_faces += 1 + # Mark all half-edges belonging to this face + self.traverse_face(v, w, counted_half_edges) + num_edges = num_half_edges // 2 # num_half_edges is even + if num_nodes - num_edges + num_faces != 2: + # The result does not match Euler's formula + msg = "Bad embedding. The graph does not match Euler's formula" + raise nx.NetworkXException(msg) + + def add_half_edge_ccw(self, start_node, end_node, reference_neighbor): + """Adds a half-edge from start_node to end_node. + + The half-edge is added counter clockwise next to the existing half-edge + (start_node, reference_neighbor). + + Parameters + ---------- + start_node : node + Start node of inserted edge. + end_node : node + End node of inserted edge. + reference_neighbor: node + End node of reference edge. + + Raises + ------ + NetworkXException + If the reference_neighbor does not exist. + + See Also + -------- + add_half_edge + add_half_edge_cw + connect_components + + """ + self.add_half_edge(start_node, end_node, cw=reference_neighbor) + + def add_half_edge_cw(self, start_node, end_node, reference_neighbor): + """Adds a half-edge from start_node to end_node. + + The half-edge is added clockwise next to the existing half-edge + (start_node, reference_neighbor). + + Parameters + ---------- + start_node : node + Start node of inserted edge. + end_node : node + End node of inserted edge. + reference_neighbor: node + End node of reference edge. + + Raises + ------ + NetworkXException + If the reference_neighbor does not exist. + + See Also + -------- + add_half_edge + add_half_edge_ccw + connect_components + """ + self.add_half_edge(start_node, end_node, ccw=reference_neighbor) + + def remove_edge(self, u, v): + """Remove the edge between u and v. + + Parameters + ---------- + u, v : nodes + Remove the half-edges (u, v) and (v, u) and update the + edge ordering around the removed edge. + + Raises + ------ + NetworkXError + If there is not an edge between u and v. + + See Also + -------- + remove_edges_from : remove a collection of edges + """ + try: + succs_u = self._succ[u] + succs_v = self._succ[v] + uv_cw = succs_u[v]["cw"] + uv_ccw = succs_u[v]["ccw"] + vu_cw = succs_v[u]["cw"] + vu_ccw = succs_v[u]["ccw"] + del succs_u[v] + del self._pred[v][u] + del succs_v[u] + del self._pred[u][v] + if v != uv_cw: + succs_u[uv_cw]["ccw"] = uv_ccw + succs_u[uv_ccw]["cw"] = uv_cw + if u != vu_cw: + succs_v[vu_cw]["ccw"] = vu_ccw + succs_v[vu_ccw]["cw"] = vu_cw + except KeyError as err: + raise nx.NetworkXError( + f"The edge {u}-{v} is not in the planar embedding." + ) from err + nx._clear_cache(self) + + def remove_edges_from(self, ebunch): + """Remove all edges specified in ebunch. + + Parameters + ---------- + ebunch: list or container of edge tuples + Each pair of half-edges between the nodes given in the tuples + will be removed from the graph. The nodes can be passed as: + + - 2-tuples (u, v) half-edges (u, v) and (v, u). + - 3-tuples (u, v, k) where k is ignored. + + See Also + -------- + remove_edge : remove a single edge + + Notes + ----- + Will fail silently if an edge in ebunch is not in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> ebunch = [(1, 2), (2, 3)] + >>> G.remove_edges_from(ebunch) + """ + for e in ebunch: + u, v = e[:2] # ignore edge data + # assuming that the PlanarEmbedding is valid, if the half_edge + # (u, v) is in the graph, then so is half_edge (v, u) + if u in self._succ and v in self._succ[u]: + self.remove_edge(u, v) + + def connect_components(self, v, w): + """Adds half-edges for (v, w) and (w, v) at some position. + + This method should only be called if v and w are in different + components, or it might break the embedding. + This especially means that if `connect_components(v, w)` + is called it is not allowed to call `connect_components(w, v)` + afterwards. The neighbor orientations in both directions are + all set correctly after the first call. + + Parameters + ---------- + v : node + w : node + + See Also + -------- + add_half_edge + """ + if v in self._succ and self._succ[v]: + ref = next(reversed(self._succ[v])) + else: + ref = None + self.add_half_edge(v, w, cw=ref) + if w in self._succ and self._succ[w]: + ref = next(reversed(self._succ[w])) + else: + ref = None + self.add_half_edge(w, v, cw=ref) + + def add_half_edge_first(self, start_node, end_node): + """Add a half-edge and set end_node as start_node's leftmost neighbor. + + The new edge is inserted counterclockwise with respect to the current + leftmost neighbor, if there is one. + + Parameters + ---------- + start_node : node + end_node : node + + See Also + -------- + add_half_edge + connect_components + """ + succs = self._succ.get(start_node) + # the leftmost neighbor is the last entry in the + # self._succ[start_node] dict + leftmost_nbr = next(reversed(succs)) if succs else None + self.add_half_edge(start_node, end_node, cw=leftmost_nbr) + + def next_face_half_edge(self, v, w): + """Returns the following half-edge left of a face. + + Parameters + ---------- + v : node + w : node + + Returns + ------- + half-edge : tuple + """ + new_node = self[w][v]["ccw"] + return w, new_node + + def traverse_face(self, v, w, mark_half_edges=None): + """Returns nodes on the face that belong to the half-edge (v, w). + + The face that is traversed lies to the right of the half-edge (in an + orientation where v is below w). + + Optionally it is possible to pass a set to which all encountered half + edges are added. Before calling this method, this set must not include + any half-edges that belong to the face. + + Parameters + ---------- + v : node + Start node of half-edge. + w : node + End node of half-edge. + mark_half_edges: set, optional + Set to which all encountered half-edges are added. + + Returns + ------- + face : list + A list of nodes that lie on this face. + """ + if mark_half_edges is None: + mark_half_edges = set() + + face_nodes = [v] + mark_half_edges.add((v, w)) + prev_node = v + cur_node = w + # Last half-edge is (incoming_node, v) + incoming_node = self[v][w]["cw"] + + while cur_node != v or prev_node != incoming_node: + face_nodes.append(cur_node) + prev_node, cur_node = self.next_face_half_edge(prev_node, cur_node) + if (prev_node, cur_node) in mark_half_edges: + raise nx.NetworkXException("Bad planar embedding. Impossible face.") + mark_half_edges.add((prev_node, cur_node)) + + return face_nodes + + def is_directed(self): + """A valid PlanarEmbedding is undirected. + + All reverse edges are contained, i.e. for every existing + half-edge (v, w) the half-edge in the opposite direction (w, v) is also + contained. + """ + return False + + def copy(self, as_view=False): + if as_view is True: + return nx.graphviews.generic_graph_view(self) + G = self.__class__() + G.graph.update(self.graph) + G.add_nodes_from((n, d.copy()) for n, d in self._node.items()) + super(self.__class__, G).add_edges_from( + (u, v, datadict.copy()) + for u, nbrs in self._adj.items() + for v, datadict in nbrs.items() + ) + return G + + def to_undirected(self, reciprocal=False, as_view=False): + """ + Returns a non-embedding undirected representation of the graph. + + This method strips the planar embedding information and provides + a simple undirected graph representation. While creating the undirected graph, + all edge attributes are retained except the ``"cw"`` and ``"ccw"`` attributes + which are removed from the edge data. Those attributes are specific to + the requirements of planar embeddings. + + Parameters + ---------- + reciprocal : bool (optional) + Not supported for PlanarEmbedding. This parameter raises an exception + if used. All valid embeddings include reciprocal half-edges by definition, + making this parameter unnecessary. + as_view : bool (optional, default=False) + Not supported for PlanarEmbedding. This parameter raises an exception + if used. + + Returns + ------- + G : Graph + An undirected graph with the same name and nodes as the PlanarEmbedding. + Edges are included with their data, except for the ``"cw"`` and ``"ccw"`` + attributes, which are omitted. + + + Notes + ----- + - If edges exist in both directions ``(u, v)`` and ``(v, u)`` in the PlanarEmbedding, + attributes for the resulting undirected edge will be combined, excluding ``"cw"`` + and ``"ccw"``. + - A deep copy is made of the other edge attributes as well as the + node and graph attributes, ensuring independence of the resulting graph. + - Subclass-specific data structures used in the original graph may not transfer + to the undirected graph. The resulting graph will be of type ``nx.Graph``. + """ + + if reciprocal: + raise ValueError( + "'reciprocal=True' is not supported for PlanarEmbedding.\n" + "All valid embeddings include reciprocal half-edges by definition,\n" + "making this parameter unnecessary." + ) + + if as_view: + raise ValueError("'as_view=True' is not supported for PlanarEmbedding.") + + graph_class = self.to_undirected_class() + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + G.add_edges_from( + (u, v, {k: deepcopy(v) for k, v in d.items() if k not in {"cw", "ccw"}}) + for u, nbrs in self._adj.items() + for v, d in nbrs.items() + ) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/polynomials.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/polynomials.py new file mode 100644 index 0000000000000000000000000000000000000000..7ebc7554a7654c8961c9d8a8024d17210ccf44ca --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/polynomials.py @@ -0,0 +1,306 @@ +"""Provides algorithms supporting the computation of graph polynomials. + +Graph polynomials are polynomial-valued graph invariants that encode a wide +variety of structural information. Examples include the Tutte polynomial, +chromatic polynomial, characteristic polynomial, and matching polynomial. An +extensive treatment is provided in [1]_. + +For a simple example, the `~sympy.matrices.matrices.MatrixDeterminant.charpoly` +method can be used to compute the characteristic polynomial from the adjacency +matrix of a graph. Consider the complete graph ``K_4``: + +>>> import sympy +>>> x = sympy.Symbol("x") +>>> G = nx.complete_graph(4) +>>> A = nx.to_numpy_array(G, dtype=int) +>>> M = sympy.SparseMatrix(A) +>>> M.charpoly(x).as_expr() +x**4 - 6*x**2 - 8*x - 3 + + +.. [1] Y. Shi, M. Dehmer, X. Li, I. Gutman, + "Graph Polynomials" +""" + +from collections import deque + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["tutte_polynomial", "chromatic_polynomial"] + + +@not_implemented_for("directed") +@nx._dispatchable +def tutte_polynomial(G): + r"""Returns the Tutte polynomial of `G` + + This function computes the Tutte polynomial via an iterative version of + the deletion-contraction algorithm. + + The Tutte polynomial `T_G(x, y)` is a fundamental graph polynomial invariant in + two variables. It encodes a wide array of information related to the + edge-connectivity of a graph; "Many problems about graphs can be reduced to + problems of finding and evaluating the Tutte polynomial at certain values" [1]_. + In fact, every deletion-contraction-expressible feature of a graph is a + specialization of the Tutte polynomial [2]_ (see Notes for examples). + + There are several equivalent definitions; here are three: + + Def 1 (rank-nullity expansion): For `G` an undirected graph, `n(G)` the + number of vertices of `G`, `E` the edge set of `G`, `V` the vertex set of + `G`, and `c(A)` the number of connected components of the graph with vertex + set `V` and edge set `A` [3]_: + + .. math:: + + T_G(x, y) = \sum_{A \in E} (x-1)^{c(A) - c(E)} (y-1)^{c(A) + |A| - n(G)} + + Def 2 (spanning tree expansion): Let `G` be an undirected graph, `T` a spanning + tree of `G`, and `E` the edge set of `G`. Let `E` have an arbitrary strict + linear order `L`. Let `B_e` be the unique minimal nonempty edge cut of + $E \setminus T \cup {e}$. An edge `e` is internally active with respect to + `T` and `L` if `e` is the least edge in `B_e` according to the linear order + `L`. The internal activity of `T` (denoted `i(T)`) is the number of edges + in $E \setminus T$ that are internally active with respect to `T` and `L`. + Let `P_e` be the unique path in $T \cup {e}$ whose source and target vertex + are the same. An edge `e` is externally active with respect to `T` and `L` + if `e` is the least edge in `P_e` according to the linear order `L`. The + external activity of `T` (denoted `e(T)`) is the number of edges in + $E \setminus T$ that are externally active with respect to `T` and `L`. + Then [4]_ [5]_: + + .. math:: + + T_G(x, y) = \sum_{T \text{ a spanning tree of } G} x^{i(T)} y^{e(T)} + + Def 3 (deletion-contraction recurrence): For `G` an undirected graph, `G-e` + the graph obtained from `G` by deleting edge `e`, `G/e` the graph obtained + from `G` by contracting edge `e`, `k(G)` the number of cut-edges of `G`, + and `l(G)` the number of self-loops of `G`: + + .. math:: + T_G(x, y) = \begin{cases} + x^{k(G)} y^{l(G)}, & \text{if all edges are cut-edges or self-loops} \\ + T_{G-e}(x, y) + T_{G/e}(x, y), & \text{otherwise, for an arbitrary edge $e$ not a cut-edge or loop} + \end{cases} + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + instance of `sympy.core.add.Add` + A Sympy expression representing the Tutte polynomial for `G`. + + Examples + -------- + >>> C = nx.cycle_graph(5) + >>> nx.tutte_polynomial(C) + x**4 + x**3 + x**2 + x + y + + >>> D = nx.diamond_graph() + >>> nx.tutte_polynomial(D) + x**3 + 2*x**2 + 2*x*y + x + y**2 + y + + Notes + ----- + Some specializations of the Tutte polynomial: + + - `T_G(1, 1)` counts the number of spanning trees of `G` + - `T_G(1, 2)` counts the number of connected spanning subgraphs of `G` + - `T_G(2, 1)` counts the number of spanning forests in `G` + - `T_G(0, 2)` counts the number of strong orientations of `G` + - `T_G(2, 0)` counts the number of acyclic orientations of `G` + + Edge contraction is defined and deletion-contraction is introduced in [6]_. + Combinatorial meaning of the coefficients is introduced in [7]_. + Universality, properties, and applications are discussed in [8]_. + + Practically, up-front computation of the Tutte polynomial may be useful when + users wish to repeatedly calculate edge-connectivity-related information + about one or more graphs. + + References + ---------- + .. [1] M. Brandt, + "The Tutte Polynomial." + Talking About Combinatorial Objects Seminar, 2015 + https://math.berkeley.edu/~brandtm/talks/tutte.pdf + .. [2] A. Björklund, T. Husfeldt, P. Kaski, M. Koivisto, + "Computing the Tutte polynomial in vertex-exponential time" + 49th Annual IEEE Symposium on Foundations of Computer Science, 2008 + https://ieeexplore.ieee.org/abstract/document/4691000 + .. [3] Y. Shi, M. Dehmer, X. Li, I. Gutman, + "Graph Polynomials," p. 14 + .. [4] Y. Shi, M. Dehmer, X. Li, I. Gutman, + "Graph Polynomials," p. 46 + .. [5] A. Nešetril, J. Goodall, + "Graph invariants, homomorphisms, and the Tutte polynomial" + https://iuuk.mff.cuni.cz/~andrew/Tutte.pdf + .. [6] D. B. West, + "Introduction to Graph Theory," p. 84 + .. [7] G. Coutinho, + "A brief introduction to the Tutte polynomial" + Structural Analysis of Complex Networks, 2011 + https://homepages.dcc.ufmg.br/~gabriel/seminars/coutinho_tuttepolynomial_seminar.pdf + .. [8] J. A. Ellis-Monaghan, C. Merino, + "Graph polynomials and their applications I: The Tutte polynomial" + Structural Analysis of Complex Networks, 2011 + https://arxiv.org/pdf/0803.3079.pdf + """ + import sympy + + x = sympy.Symbol("x") + y = sympy.Symbol("y") + stack = deque() + stack.append(nx.MultiGraph(G)) + + polynomial = 0 + while stack: + G = stack.pop() + bridges = set(nx.bridges(G)) + + e = None + for i in G.edges: + if (i[0], i[1]) not in bridges and i[0] != i[1]: + e = i + break + if not e: + loops = list(nx.selfloop_edges(G, keys=True)) + polynomial += x ** len(bridges) * y ** len(loops) + else: + # deletion-contraction + C = nx.contracted_edge(G, e, self_loops=True) + C.remove_edge(e[0], e[0]) + G.remove_edge(*e) + stack.append(G) + stack.append(C) + return sympy.simplify(polynomial) + + +@not_implemented_for("directed") +@nx._dispatchable +def chromatic_polynomial(G): + r"""Returns the chromatic polynomial of `G` + + This function computes the chromatic polynomial via an iterative version of + the deletion-contraction algorithm. + + The chromatic polynomial `X_G(x)` is a fundamental graph polynomial + invariant in one variable. Evaluating `X_G(k)` for an natural number `k` + enumerates the proper k-colorings of `G`. + + There are several equivalent definitions; here are three: + + Def 1 (explicit formula): + For `G` an undirected graph, `c(G)` the number of connected components of + `G`, `E` the edge set of `G`, and `G(S)` the spanning subgraph of `G` with + edge set `S` [1]_: + + .. math:: + + X_G(x) = \sum_{S \subseteq E} (-1)^{|S|} x^{c(G(S))} + + + Def 2 (interpolating polynomial): + For `G` an undirected graph, `n(G)` the number of vertices of `G`, `k_0 = 0`, + and `k_i` the number of distinct ways to color the vertices of `G` with `i` + unique colors (for `i` a natural number at most `n(G)`), `X_G(x)` is the + unique Lagrange interpolating polynomial of degree `n(G)` through the points + `(0, k_0), (1, k_1), \dots, (n(G), k_{n(G)})` [2]_. + + + Def 3 (chromatic recurrence): + For `G` an undirected graph, `G-e` the graph obtained from `G` by deleting + edge `e`, `G/e` the graph obtained from `G` by contracting edge `e`, `n(G)` + the number of vertices of `G`, and `e(G)` the number of edges of `G` [3]_: + + .. math:: + X_G(x) = \begin{cases} + x^{n(G)}, & \text{if $e(G)=0$} \\ + X_{G-e}(x) - X_{G/e}(x), & \text{otherwise, for an arbitrary edge $e$} + \end{cases} + + This formulation is also known as the Fundamental Reduction Theorem [4]_. + + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + instance of `sympy.core.add.Add` + A Sympy expression representing the chromatic polynomial for `G`. + + Examples + -------- + >>> C = nx.cycle_graph(5) + >>> nx.chromatic_polynomial(C) + x**5 - 5*x**4 + 10*x**3 - 10*x**2 + 4*x + + >>> G = nx.complete_graph(4) + >>> nx.chromatic_polynomial(G) + x**4 - 6*x**3 + 11*x**2 - 6*x + + Notes + ----- + Interpretation of the coefficients is discussed in [5]_. Several special + cases are listed in [2]_. + + The chromatic polynomial is a specialization of the Tutte polynomial; in + particular, ``X_G(x) = T_G(x, 0)`` [6]_. + + The chromatic polynomial may take negative arguments, though evaluations + may not have chromatic interpretations. For instance, ``X_G(-1)`` enumerates + the acyclic orientations of `G` [7]_. + + References + ---------- + .. [1] D. B. West, + "Introduction to Graph Theory," p. 222 + .. [2] E. W. Weisstein + "Chromatic Polynomial" + MathWorld--A Wolfram Web Resource + https://mathworld.wolfram.com/ChromaticPolynomial.html + .. [3] D. B. West, + "Introduction to Graph Theory," p. 221 + .. [4] J. Zhang, J. Goodall, + "An Introduction to Chromatic Polynomials" + https://math.mit.edu/~apost/courses/18.204_2018/Julie_Zhang_paper.pdf + .. [5] R. C. Read, + "An Introduction to Chromatic Polynomials" + Journal of Combinatorial Theory, 1968 + https://math.berkeley.edu/~mrklug/ReadChromatic.pdf + .. [6] W. T. Tutte, + "Graph-polynomials" + Advances in Applied Mathematics, 2004 + https://www.sciencedirect.com/science/article/pii/S0196885803000411 + .. [7] R. P. Stanley, + "Acyclic orientations of graphs" + Discrete Mathematics, 2006 + https://math.mit.edu/~rstan/pubs/pubfiles/18.pdf + """ + import sympy + + x = sympy.Symbol("x") + stack = deque() + stack.append(nx.MultiGraph(G, contraction_idx=0)) + + polynomial = 0 + while stack: + G = stack.pop() + edges = list(G.edges) + if not edges: + polynomial += (-1) ** G.graph["contraction_idx"] * x ** len(G) + else: + e = edges[0] + C = nx.contracted_edge(G, e, self_loops=True) + C.graph["contraction_idx"] = G.graph["contraction_idx"] + 1 + C.remove_edge(e[0], e[0]) + G.remove_edge(*e) + stack.append(G) + stack.append(C) + return polynomial diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/reciprocity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/reciprocity.py new file mode 100644 index 0000000000000000000000000000000000000000..5ea7ed2ce26ab973e07bcc6ec0d92aa4799d9a6a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/reciprocity.py @@ -0,0 +1,98 @@ +"""Algorithms to calculate reciprocity in a directed graph.""" + +import networkx as nx +from networkx import NetworkXError + +from ..utils import not_implemented_for + +__all__ = ["reciprocity", "overall_reciprocity"] + + +@not_implemented_for("undirected", "multigraph") +@nx._dispatchable +def reciprocity(G, nodes=None): + r"""Compute the reciprocity in a directed graph. + + The reciprocity of a directed graph is defined as the ratio + of the number of edges pointing in both directions to the total + number of edges in the graph. + Formally, $r = |{(u,v) \in G|(v,u) \in G}| / |{(u,v) \in G}|$. + + The reciprocity of a single node u is defined similarly, + it is the ratio of the number of edges in both directions to + the total number of edges attached to node u. + + Parameters + ---------- + G : graph + A networkx directed graph + nodes : container of nodes, optional (default=whole graph) + Compute reciprocity for nodes in this container. + + Returns + ------- + out : dictionary + Reciprocity keyed by node label. + + Notes + ----- + The reciprocity is not defined for isolated nodes. + In such cases this function will return None. + + """ + # If `nodes` is not specified, calculate the reciprocity of the graph. + if nodes is None: + return overall_reciprocity(G) + + # If `nodes` represents a single node in the graph, return only its + # reciprocity. + if nodes in G: + reciprocity = next(_reciprocity_iter(G, nodes))[1] + if reciprocity is None: + raise NetworkXError("Not defined for isolated nodes.") + else: + return reciprocity + + # Otherwise, `nodes` represents an iterable of nodes, so return a + # dictionary mapping node to its reciprocity. + return dict(_reciprocity_iter(G, nodes)) + + +def _reciprocity_iter(G, nodes): + """Return an iterator of (node, reciprocity).""" + n = G.nbunch_iter(nodes) + for node in n: + pred = set(G.predecessors(node)) + succ = set(G.successors(node)) + overlap = pred & succ + n_total = len(pred) + len(succ) + + # Reciprocity is not defined for isolated nodes. + # Return None. + if n_total == 0: + yield (node, None) + else: + reciprocity = 2 * len(overlap) / n_total + yield (node, reciprocity) + + +@not_implemented_for("undirected", "multigraph") +@nx._dispatchable +def overall_reciprocity(G): + """Compute the reciprocity for the whole graph. + + See the doc of reciprocity for the definition. + + Parameters + ---------- + G : graph + A networkx graph + + """ + n_all_edge = G.number_of_edges() + n_overlap_edge = (n_all_edge - G.to_undirected().number_of_edges()) * 2 + + if n_all_edge == 0: + raise NetworkXError("Not defined for empty graphs") + + return n_overlap_edge / n_all_edge diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/regular.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/regular.py new file mode 100644 index 0000000000000000000000000000000000000000..a0032e2d4fb94df69110b99c38ba4c688565399d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/regular.py @@ -0,0 +1,167 @@ +"""Functions for computing and verifying regular graphs.""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["is_regular", "is_k_regular", "k_factor"] + + +@nx._dispatchable +def is_regular(G): + """Determines whether a graph is regular. + + A regular graph is a graph where all nodes have the same degree. A regular + digraph is a graph where all nodes have the same indegree and all nodes + have the same outdegree. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + bool + Whether the given graph or digraph is regular. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 1)]) + >>> nx.is_regular(G) + True + + """ + if len(G) == 0: + raise nx.NetworkXPointlessConcept("Graph has no nodes.") + n1 = nx.utils.arbitrary_element(G) + if not G.is_directed(): + d1 = G.degree(n1) + return all(d1 == d for _, d in G.degree) + else: + d_in = G.in_degree(n1) + in_regular = (d_in == d for _, d in G.in_degree) + d_out = G.out_degree(n1) + out_regular = (d_out == d for _, d in G.out_degree) + return all(in_regular) and all(out_regular) + + +@not_implemented_for("directed") +@nx._dispatchable +def is_k_regular(G, k): + """Determines whether the graph ``G`` is a k-regular graph. + + A k-regular graph is a graph where each vertex has degree k. + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + bool + Whether the given graph is k-regular. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)]) + >>> nx.is_k_regular(G, k=3) + False + + """ + return all(d == k for n, d in G.degree) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(preserve_edge_attrs=True, returns_graph=True) +def k_factor(G, k, matching_weight="weight"): + """Compute a `k`-factor of a graph. + + A `k`-factor of a graph is a spanning `k`-regular subgraph. + A spanning `k`-regular subgraph of `G` is a subgraph that contains + each node of `G` and a subset of the edges of `G` such that each + node has degree `k`. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + k : int + The degree of the `k`-factor. + + matching_weight: string, optional (default="weight") + Edge attribute name corresponding to the edge weight. + If not present, the edge is assumed to have weight 1. + Used for finding the max-weighted perfect matching. + + Returns + ------- + NetworkX graph + A `k`-factor of `G`. + + Examples + -------- + >>> G = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 1)]) + >>> KF = nx.k_factor(G, k=1) + >>> KF.edges() + EdgeView([(1, 2), (3, 4)]) + + References + ---------- + .. [1] "An algorithm for computing simple k-factors.", + Meijer, Henk, Yurai Núñez-Rodríguez, and David Rappaport, + Information processing letters, 2009. + """ + # Validate minimum degree requirement. + if any(d < k for _, d in G.degree): + raise nx.NetworkXUnfeasible("Graph contains a vertex with degree less than k") + + g = G.copy() + gadgets = [] + + # Replace each node with a gadget. + for node, degree in G.degree: + is_large = k >= degree / 2.0 + + # Create gadget nodes. + outer = [(node, i) for i in range(degree)] + if is_large: + core = [(node, i) for i in range(degree, 2 * degree - k)] + inner = [] + else: + core = [(node, i) for i in range(2 * degree, 2 * degree + k)] + inner = [(node, i) for i in range(degree, 2 * degree)] + + # Connect gadget nodes to neighbors. + g.add_edges_from(zip(outer, inner)) + for outer_n, (neighbor, attrs) in zip(outer, g[node].items()): + g.add_edge(outer_n, neighbor, **attrs) + + # Add internal edges. + g.add_edges_from((u, v) for u in core for v in (outer if is_large else inner)) + + g.remove_node(node) + gadgets.append((node, outer, core, inner)) + + # Find perfect matching. + m = nx.max_weight_matching(g, maxcardinality=True, weight=matching_weight) + if not nx.is_perfect_matching(g, m): + raise nx.NetworkXUnfeasible( + "Cannot find k-factor because no perfect matching exists" + ) + + # Keep only edges in matching. + g.remove_edges_from(e for e in g.edges if e not in m and e[::-1] not in m) + + # Restore original nodes and remove gadgets. + for node, outer, core, inner in gadgets: + g.add_node(node) + core_set = set(core) + for outer_n in outer: + for neighbor, attrs in g._adj[outer_n].items(): + if neighbor not in core_set: + g.add_edge(node, neighbor, **attrs) + break + g.remove_nodes_from(outer + core + inner) + + return g diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/richclub.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/richclub.py new file mode 100644 index 0000000000000000000000000000000000000000..445b27d142547e5cad04e00abc9ca33d45edbee6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/richclub.py @@ -0,0 +1,138 @@ +"""Functions for computing rich-club coefficients.""" + +from itertools import accumulate + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["rich_club_coefficient"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def rich_club_coefficient(G, normalized=True, Q=100, seed=None): + r"""Returns the rich-club coefficient of the graph `G`. + + For each degree *k*, the *rich-club coefficient* is the ratio of the + number of actual to the number of potential edges for nodes with + degree greater than *k*: + + .. math:: + + \phi(k) = \frac{2 E_k}{N_k (N_k - 1)} + + where `N_k` is the number of nodes with degree larger than *k*, and + `E_k` is the number of edges among those nodes. + + Parameters + ---------- + G : NetworkX graph + Undirected graph with neither parallel edges nor self-loops. + normalized : bool (optional) + Normalize using randomized network as in [1]_ + Q : float (optional, default=100) + If `normalized` is True, perform `Q * m` double-edge + swaps, where `m` is the number of edges in `G`, to use as a + null-model for normalization. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + rc : dictionary + A dictionary, keyed by degree, with rich-club coefficient values. + + Raises + ------ + NetworkXError + If `G` has fewer than four nodes and ``normalized=True``. + A randomly sampled graph for normalization cannot be generated in this case. + + Examples + -------- + >>> G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)]) + >>> rc = nx.rich_club_coefficient(G, normalized=False, seed=42) + >>> rc[0] + 0.4 + + Notes + ----- + The rich club definition and algorithm are found in [1]_. This + algorithm ignores any edge weights and is not defined for directed + graphs or graphs with parallel edges or self loops. + + Normalization is done by computing the rich club coefficient for a randomly + sampled graph with the same degree distribution as `G` by + repeatedly swapping the endpoints of existing edges. For graphs with fewer than 4 + nodes, it is not possible to generate a random graph with a prescribed + degree distribution, as the degree distribution fully determines the graph + (hence making the coefficients trivially normalized to 1). + This function raises an exception in this case. + + Estimates for appropriate values of `Q` are found in [2]_. + + References + ---------- + .. [1] Julian J. McAuley, Luciano da Fontoura Costa, + and Tibério S. Caetano, + "The rich-club phenomenon across complex network hierarchies", + Applied Physics Letters Vol 91 Issue 8, August 2007. + https://arxiv.org/abs/physics/0701290 + .. [2] R. Milo, N. Kashtan, S. Itzkovitz, M. E. J. Newman, U. Alon, + "Uniform generation of random graphs with arbitrary degree + sequences", 2006. https://arxiv.org/abs/cond-mat/0312028 + """ + if nx.number_of_selfloops(G) > 0: + raise Exception( + "rich_club_coefficient is not implemented for graphs with self loops." + ) + rc = _compute_rc(G) + if normalized: + # make R a copy of G, randomize with Q*|E| double edge swaps + # and use rich_club coefficient of R to normalize + R = G.copy() + E = R.number_of_edges() + nx.double_edge_swap(R, Q * E, max_tries=Q * E * 10, seed=seed) + rcran = _compute_rc(R) + rc = {k: v / rcran[k] for k, v in rc.items()} + return rc + + +def _compute_rc(G): + """Returns the rich-club coefficient for each degree in the graph + `G`. + + `G` is an undirected graph without multiedges. + + Returns a dictionary mapping degree to rich-club coefficient for + that degree. + + """ + deghist = nx.degree_histogram(G) + total = sum(deghist) + # Compute the number of nodes with degree greater than `k`, for each + # degree `k` (omitting the last entry, which is zero). + nks = (total - cs for cs in accumulate(deghist) if total - cs > 1) + # Create a sorted list of pairs of edge endpoint degrees. + # + # The list is sorted in reverse order so that we can pop from the + # right side of the list later, instead of popping from the left + # side of the list, which would have a linear time cost. + edge_degrees = sorted((sorted(map(G.degree, e)) for e in G.edges()), reverse=True) + ek = G.number_of_edges() + if ek == 0: + return {} + + k1, k2 = edge_degrees.pop() + rc = {} + for d, nk in enumerate(nks): + while k1 <= d: + if len(edge_degrees) == 0: + ek = 0 + break + k1, k2 = edge_degrees.pop() + ek -= 1 + rc[d] = 2 * ek / (nk * (nk - 1)) + return rc diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eb0d91cecc902f6390cb8309c017cb1558f7753f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/__init__.py @@ -0,0 +1,5 @@ +from networkx.algorithms.shortest_paths.generic import * +from networkx.algorithms.shortest_paths.unweighted import * +from networkx.algorithms.shortest_paths.weighted import * +from networkx.algorithms.shortest_paths.astar import * +from networkx.algorithms.shortest_paths.dense import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/astar.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/astar.py new file mode 100644 index 0000000000000000000000000000000000000000..118229716cdfd5fe3a45619010437ec0df502d3d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/astar.py @@ -0,0 +1,239 @@ +"""Shortest paths and path lengths using the A* ("A star") algorithm.""" + +from heapq import heappop, heappush +from itertools import count + +import networkx as nx +from networkx.algorithms.shortest_paths.weighted import _weight_function + +__all__ = ["astar_path", "astar_path_length"] + + +@nx._dispatchable(edge_attrs="weight", preserve_node_attrs="heuristic") +def astar_path(G, source, target, heuristic=None, weight="weight", *, cutoff=None): + """Returns a list of nodes in a shortest path between source and target + using the A* ("A-star") algorithm. + + There may be more than one shortest path. This returns only one. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : node + Ending node for path + + heuristic : function + A function to evaluate the estimate of the distance + from the a node to the target. The function takes + two nodes arguments and must return a number. + If the heuristic is inadmissible (if it might + overestimate the cost of reaching the goal from a node), + the result may not be a shortest path. + The algorithm does not support updating heuristic + values for the same node due to caching the first + heuristic calculation per node. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + cutoff : float, optional + If this is provided, the search will be bounded to this value. I.e. if + the evaluation function surpasses this value for a node n, the node will not + be expanded further and will be ignored. More formally, let h'(n) be the + heuristic function, and g(n) be the cost of reaching n from the source node. Then, + if g(n) + h'(n) > cutoff, the node will not be explored further. + Note that if the heuristic is inadmissible, it is possible that paths + are ignored even though they satisfy the cutoff. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> print(nx.astar_path(G, 0, 4)) + [0, 1, 2, 3, 4] + >>> G = nx.grid_graph(dim=[3, 3]) # nodes are two-tuples (x,y) + >>> nx.set_edge_attributes(G, {e: e[1][0] * 2 for e in G.edges()}, "cost") + >>> def dist(a, b): + ... (x1, y1) = a + ... (x2, y2) = b + ... return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5 + >>> print(nx.astar_path(G, (0, 0), (2, 2), heuristic=dist, weight="cost")) + [(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + See Also + -------- + shortest_path, dijkstra_path + + """ + if source not in G: + raise nx.NodeNotFound(f"Source {source} is not in G") + + if target not in G: + raise nx.NodeNotFound(f"Target {target} is not in G") + + if heuristic is None: + # The default heuristic is h=0 - same as Dijkstra's algorithm + def heuristic(u, v): + return 0 + + weight = _weight_function(G, weight) + + G_succ = G._adj # For speed-up (and works for both directed and undirected graphs) + + # The queue stores priority, node, cost to reach, and parent. + # Uses Python heapq to keep in priority order. + # Add a counter to the queue to prevent the underlying heap from + # attempting to compare the nodes themselves. The hash breaks ties in the + # priority and is guaranteed unique for all nodes in the graph. + c = count() + queue = [(0, next(c), source, 0, None)] + + # Maps enqueued nodes to distance of discovered paths and the + # computed heuristics to target. We avoid computing the heuristics + # more than once and inserting the node into the queue too many times. + enqueued = {} + # Maps explored nodes to parent closest to the source. + explored = {} + + while queue: + # Pop the smallest item from queue. + _, __, curnode, dist, parent = heappop(queue) + + if curnode == target: + path = [curnode] + node = parent + while node is not None: + path.append(node) + node = explored[node] + path.reverse() + return path + + if curnode in explored: + # Do not override the parent of starting node + if explored[curnode] is None: + continue + + # Skip bad paths that were enqueued before finding a better one + qcost, h = enqueued[curnode] + if qcost < dist: + continue + + explored[curnode] = parent + + for neighbor, w in G_succ[curnode].items(): + cost = weight(curnode, neighbor, w) + if cost is None: + continue + ncost = dist + cost + if neighbor in enqueued: + qcost, h = enqueued[neighbor] + # if qcost <= ncost, a less costly path from the + # neighbor to the source was already determined. + # Therefore, we won't attempt to push this neighbor + # to the queue + if qcost <= ncost: + continue + else: + h = heuristic(neighbor, target) + + if cutoff and ncost + h > cutoff: + continue + + enqueued[neighbor] = ncost, h + heappush(queue, (ncost + h, next(c), neighbor, ncost, curnode)) + + raise nx.NetworkXNoPath(f"Node {target} not reachable from {source}") + + +@nx._dispatchable(edge_attrs="weight", preserve_node_attrs="heuristic") +def astar_path_length( + G, source, target, heuristic=None, weight="weight", *, cutoff=None +): + """Returns the length of the shortest path between source and target using + the A* ("A-star") algorithm. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : node + Ending node for path + + heuristic : function + A function to evaluate the estimate of the distance + from the a node to the target. The function takes + two nodes arguments and must return a number. + If the heuristic is inadmissible (if it might + overestimate the cost of reaching the goal from a node), + the result may not be a shortest path. + The algorithm does not support updating heuristic + values for the same node due to caching the first + heuristic calculation per node. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + cutoff : float, optional + If this is provided, the search will be bounded to this value. I.e. if + the evaluation function surpasses this value for a node n, the node will not + be expanded further and will be ignored. More formally, let h'(n) be the + heuristic function, and g(n) be the cost of reaching n from the source node. Then, + if g(n) + h'(n) > cutoff, the node will not be explored further. + Note that if the heuristic is inadmissible, it is possible that paths + are ignored even though they satisfy the cutoff. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + See Also + -------- + astar_path + + """ + if source not in G or target not in G: + msg = f"Either source {source} or target {target} is not in G" + raise nx.NodeNotFound(msg) + + weight = _weight_function(G, weight) + path = astar_path(G, source, target, heuristic, weight, cutoff=cutoff) + return sum(weight(u, v, G[u][v]) for u, v in zip(path[:-1], path[1:])) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/dense.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/dense.py new file mode 100644 index 0000000000000000000000000000000000000000..d259d8c2710dfeb0c299d8ec8e46677b1f27606d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/dense.py @@ -0,0 +1,264 @@ +"""Floyd-Warshall algorithm for shortest paths.""" + +import networkx as nx + +__all__ = [ + "floyd_warshall", + "floyd_warshall_predecessor_and_distance", + "reconstruct_path", + "floyd_warshall_numpy", +] + + +@nx._dispatchable(edge_attrs="weight") +def floyd_warshall_numpy(G, nodelist=None, weight="weight"): + """Find all-pairs shortest path lengths using Floyd's algorithm. + + This algorithm for finding shortest paths takes advantage of + matrix representations of a graph and works well for dense + graphs where all-pairs shortest path lengths are desired. + The results are returned as a NumPy array, distance[i, j], + where i and j are the indexes of two nodes in nodelist. + The entry distance[i, j] is the distance along a shortest + path from i to j. If no path exists the distance is Inf. + + Parameters + ---------- + G : NetworkX graph + + nodelist : list, optional (default=G.nodes) + The rows and columns are ordered by the nodes in nodelist. + If nodelist is None then the ordering is produced by G.nodes. + Nodelist should include all nodes in G. + + weight: string, optional (default='weight') + Edge data key corresponding to the edge weight. + + Returns + ------- + distance : 2D numpy.ndarray + A numpy array of shortest path distances between nodes. + If there is no path between two nodes the value is Inf. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_weighted_edges_from( + ... [(0, 1, 5), (1, 2, 2), (2, 3, -3), (1, 3, 10), (3, 2, 8)] + ... ) + >>> nx.floyd_warshall_numpy(G) + array([[ 0., 5., 7., 4.], + [inf, 0., 2., -1.], + [inf, inf, 0., -3.], + [inf, inf, 8., 0.]]) + + Notes + ----- + Floyd's algorithm is appropriate for finding shortest paths in + dense graphs or graphs with negative weights when Dijkstra's + algorithm fails. This algorithm can still fail if there are negative + cycles. It has running time $O(n^3)$ with running space of $O(n^2)$. + + Raises + ------ + NetworkXError + If nodelist is not a list of the nodes in G. + """ + import numpy as np + + if nodelist is not None: + if not (len(nodelist) == len(G) == len(set(nodelist))): + raise nx.NetworkXError( + "nodelist must contain every node in G with no repeats." + "If you wanted a subgraph of G use G.subgraph(nodelist)" + ) + + # To handle cases when an edge has weight=0, we must make sure that + # nonedges are not given the value 0 as well. + A = nx.to_numpy_array( + G, nodelist, multigraph_weight=min, weight=weight, nonedge=np.inf + ) + n, m = A.shape + np.fill_diagonal(A, 0) # diagonal elements should be zero + for i in range(n): + # The second term has the same shape as A due to broadcasting + A = np.minimum(A, A[i, :][np.newaxis, :] + A[:, i][:, np.newaxis]) + return A + + +@nx._dispatchable(edge_attrs="weight") +def floyd_warshall_predecessor_and_distance(G, weight="weight"): + """Find all-pairs shortest path lengths using Floyd's algorithm. + + Parameters + ---------- + G : NetworkX graph + + weight: string, optional (default= 'weight') + Edge data key corresponding to the edge weight. + + Returns + ------- + predecessor,distance : dictionaries + Dictionaries, keyed by source and target, of predecessors and distances + in the shortest path. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_weighted_edges_from( + ... [ + ... ("s", "u", 10), + ... ("s", "x", 5), + ... ("u", "v", 1), + ... ("u", "x", 2), + ... ("v", "y", 1), + ... ("x", "u", 3), + ... ("x", "v", 5), + ... ("x", "y", 2), + ... ("y", "s", 7), + ... ("y", "v", 6), + ... ] + ... ) + >>> predecessors, _ = nx.floyd_warshall_predecessor_and_distance(G) + >>> print(nx.reconstruct_path("s", "v", predecessors)) + ['s', 'x', 'u', 'v'] + + Notes + ----- + Floyd's algorithm is appropriate for finding shortest paths + in dense graphs or graphs with negative weights when Dijkstra's algorithm + fails. This algorithm can still fail if there are negative cycles. + It has running time $O(n^3)$ with running space of $O(n^2)$. + + See Also + -------- + floyd_warshall + floyd_warshall_numpy + all_pairs_shortest_path + all_pairs_shortest_path_length + """ + from collections import defaultdict + + # dictionary-of-dictionaries representation for dist and pred + # use some defaultdict magick here + # for dist the default is the floating point inf value + dist = defaultdict(lambda: defaultdict(lambda: float("inf"))) + for u in G: + dist[u][u] = 0 + pred = defaultdict(dict) + # initialize path distance dictionary to be the adjacency matrix + # also set the distance to self to 0 (zero diagonal) + undirected = not G.is_directed() + for u, v, d in G.edges(data=True): + e_weight = d.get(weight, 1.0) + dist[u][v] = min(e_weight, dist[u][v]) + pred[u][v] = u + if undirected: + dist[v][u] = min(e_weight, dist[v][u]) + pred[v][u] = v + for w in G: + dist_w = dist[w] # save recomputation + for u in G: + dist_u = dist[u] # save recomputation + for v in G: + d = dist_u[w] + dist_w[v] + if dist_u[v] > d: + dist_u[v] = d + pred[u][v] = pred[w][v] + return dict(pred), dict(dist) + + +@nx._dispatchable(graphs=None) +def reconstruct_path(source, target, predecessors): + """Reconstruct a path from source to target using the predecessors + dict as returned by floyd_warshall_predecessor_and_distance + + Parameters + ---------- + source : node + Starting node for path + + target : node + Ending node for path + + predecessors: dictionary + Dictionary, keyed by source and target, of predecessors in the + shortest path, as returned by floyd_warshall_predecessor_and_distance + + Returns + ------- + path : list + A list of nodes containing the shortest path from source to target + + If source and target are the same, an empty list is returned + + Notes + ----- + This function is meant to give more applicability to the + floyd_warshall_predecessor_and_distance function + + See Also + -------- + floyd_warshall_predecessor_and_distance + """ + if source == target: + return [] + prev = predecessors[source] + curr = prev[target] + path = [target, curr] + while curr != source: + curr = prev[curr] + path.append(curr) + return list(reversed(path)) + + +@nx._dispatchable(edge_attrs="weight") +def floyd_warshall(G, weight="weight"): + """Find all-pairs shortest path lengths using Floyd's algorithm. + + Parameters + ---------- + G : NetworkX graph + + weight: string, optional (default= 'weight') + Edge data key corresponding to the edge weight. + + + Returns + ------- + distance : dict + A dictionary, keyed by source and target, of shortest paths distances + between nodes. + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.DiGraph() + >>> G.add_weighted_edges_from( + ... [(0, 1, 5), (1, 2, 2), (2, 3, -3), (1, 3, 10), (3, 2, 8)] + ... ) + >>> fw = nx.floyd_warshall(G, weight="weight") + >>> results = {a: dict(b) for a, b in fw.items()} + >>> pprint(results) + {0: {0: 0, 1: 5, 2: 7, 3: 4}, + 1: {0: inf, 1: 0, 2: 2, 3: -1}, + 2: {0: inf, 1: inf, 2: 0, 3: -3}, + 3: {0: inf, 1: inf, 2: 8, 3: 0}} + + Notes + ----- + Floyd's algorithm is appropriate for finding shortest paths + in dense graphs or graphs with negative weights when Dijkstra's algorithm + fails. This algorithm can still fail if there are negative cycles. + It has running time $O(n^3)$ with running space of $O(n^2)$. + + See Also + -------- + floyd_warshall_predecessor_and_distance + floyd_warshall_numpy + all_pairs_shortest_path + all_pairs_shortest_path_length + """ + # could make this its own function to reduce memory costs + return floyd_warshall_predecessor_and_distance(G, weight=weight)[1] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/generic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/generic.py new file mode 100644 index 0000000000000000000000000000000000000000..1fd0eafdb1b30de7cc501144807f4c77ac8a7a08 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/generic.py @@ -0,0 +1,716 @@ +""" +Compute the shortest paths and path lengths between nodes in the graph. + +These algorithms work with undirected and directed graphs. + +""" + +import networkx as nx + +__all__ = [ + "shortest_path", + "all_shortest_paths", + "single_source_all_shortest_paths", + "all_pairs_all_shortest_paths", + "shortest_path_length", + "average_shortest_path_length", + "has_path", +] + + +@nx._dispatchable +def has_path(G, source, target): + """Returns *True* if *G* has a path from *source* to *target*. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : node + Ending node for path + """ + try: + nx.shortest_path(G, source, target) + except nx.NetworkXNoPath: + return False + return True + + +@nx._dispatchable(edge_attrs="weight") +def shortest_path(G, source=None, target=None, weight=None, method="dijkstra"): + """Compute shortest paths in the graph. + + Parameters + ---------- + G : NetworkX graph + + source : node, optional + Starting node for path. If not specified, compute shortest + paths for each possible starting node. + + target : node, optional + Ending node for path. If not specified, compute shortest + paths to all possible nodes. + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'dijkstra') + The algorithm to use to compute the path. + Supported options: 'dijkstra', 'bellman-ford'. + Other inputs produce a ValueError. + If `weight` is None, unweighted graph methods are used, and this + suggestion is ignored. + + Returns + ------- + path: list or dictionary or iterator + All returned paths include both the source and target in the path. + + If the source and target are both specified, return a single list + of nodes in a shortest path from the source to the target. + + If only the source is specified, return a dictionary keyed by + targets with a list of nodes in a shortest path from the source + to one of the targets. + + If only the target is specified, return a dictionary keyed by + sources with a list of nodes in a shortest path from one of the + sources to the target. + + If neither the source nor target are specified, return an iterator + over (source, dictionary) where dictionary is keyed by target to + list of nodes in a shortest path from the source to the target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + ValueError + If `method` is not among the supported options. + + NetworkXNoPath + If `source` and `target` are specified but no path exists between them. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> print(nx.shortest_path(G, source=0, target=4)) + [0, 1, 2, 3, 4] + >>> p = nx.shortest_path(G, source=0) # target not specified + >>> p[3] # shortest path from source=0 to target=3 + [0, 1, 2, 3] + >>> p = nx.shortest_path(G, target=4) # source not specified + >>> p[1] # shortest path from source=1 to target=4 + [1, 2, 3, 4] + >>> p = dict(nx.shortest_path(G)) # source, target not specified + >>> p[2][4] # shortest path from source=2 to target=4 + [2, 3, 4] + + Notes + ----- + There may be more than one shortest path between a source and target. + This returns only one of them. + + See Also + -------- + all_pairs_shortest_path + all_pairs_dijkstra_path + all_pairs_bellman_ford_path + single_source_shortest_path + single_source_dijkstra_path + single_source_bellman_ford_path + """ + if method not in ("dijkstra", "bellman-ford"): + # so we don't need to check in each branch later + raise ValueError(f"method not supported: {method}") + method = "unweighted" if weight is None else method + if source is None: + if target is None: + # Find paths between all pairs. Iterator of dicts. + if method == "unweighted": + paths = nx.all_pairs_shortest_path(G) + elif method == "dijkstra": + paths = nx.all_pairs_dijkstra_path(G, weight=weight) + else: # method == 'bellman-ford': + paths = nx.all_pairs_bellman_ford_path(G, weight=weight) + else: + # Find paths from all nodes co-accessible to the target. + if G.is_directed(): + G = G.reverse(copy=False) + if method == "unweighted": + paths = nx.single_source_shortest_path(G, target) + elif method == "dijkstra": + paths = nx.single_source_dijkstra_path(G, target, weight=weight) + else: # method == 'bellman-ford': + paths = nx.single_source_bellman_ford_path(G, target, weight=weight) + # Now flip the paths so they go from a source to the target. + for target in paths: + paths[target] = list(reversed(paths[target])) + else: + if target is None: + # Find paths to all nodes accessible from the source. + if method == "unweighted": + paths = nx.single_source_shortest_path(G, source) + elif method == "dijkstra": + paths = nx.single_source_dijkstra_path(G, source, weight=weight) + else: # method == 'bellman-ford': + paths = nx.single_source_bellman_ford_path(G, source, weight=weight) + else: + # Find shortest source-target path. + if method == "unweighted": + paths = nx.bidirectional_shortest_path(G, source, target) + elif method == "dijkstra": + _, paths = nx.bidirectional_dijkstra(G, source, target, weight) + else: # method == 'bellman-ford': + paths = nx.bellman_ford_path(G, source, target, weight) + return paths + + +@nx._dispatchable(edge_attrs="weight") +def shortest_path_length(G, source=None, target=None, weight=None, method="dijkstra"): + """Compute shortest path lengths in the graph. + + Parameters + ---------- + G : NetworkX graph + + source : node, optional + Starting node for path. + If not specified, compute shortest path lengths using all nodes as + source nodes. + + target : node, optional + Ending node for path. + If not specified, compute shortest path lengths using all nodes as + target nodes. + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'dijkstra') + The algorithm to use to compute the path length. + Supported options: 'dijkstra', 'bellman-ford'. + Other inputs produce a ValueError. + If `weight` is None, unweighted graph methods are used, and this + suggestion is ignored. + + Returns + ------- + length: number or iterator + If the source and target are both specified, return the length of + the shortest path from the source to the target. + + If only the source is specified, return a dict keyed by target + to the shortest path length from the source to that target. + + If only the target is specified, return a dict keyed by source + to the shortest path length from that source to the target. + + If neither the source nor target are specified, return an iterator + over (source, dictionary) where dictionary is keyed by target to + shortest path length from source to that target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + ValueError + If `method` is not among the supported options. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.shortest_path_length(G, source=0, target=4) + 4 + >>> p = nx.shortest_path_length(G, source=0) # target not specified + >>> p[4] + 4 + >>> p = nx.shortest_path_length(G, target=4) # source not specified + >>> p[0] + 4 + >>> p = dict(nx.shortest_path_length(G)) # source,target not specified + >>> p[0][4] + 4 + + Notes + ----- + The length of the path is always 1 less than the number of nodes involved + in the path since the length measures the number of edges followed. + + For digraphs this returns the shortest directed path length. To find path + lengths in the reverse direction use G.reverse(copy=False) first to flip + the edge orientation. + + See Also + -------- + all_pairs_shortest_path_length + all_pairs_dijkstra_path_length + all_pairs_bellman_ford_path_length + single_source_shortest_path_length + single_source_dijkstra_path_length + single_source_bellman_ford_path_length + """ + if method not in ("dijkstra", "bellman-ford"): + # so we don't need to check in each branch later + raise ValueError(f"method not supported: {method}") + method = "unweighted" if weight is None else method + if source is None: + if target is None: + # Find paths between all pairs. + if method == "unweighted": + paths = nx.all_pairs_shortest_path_length(G) + elif method == "dijkstra": + paths = nx.all_pairs_dijkstra_path_length(G, weight=weight) + else: # method == 'bellman-ford': + paths = nx.all_pairs_bellman_ford_path_length(G, weight=weight) + else: + # Find paths from all nodes co-accessible to the target. + if G.is_directed(): + G = G.reverse(copy=False) + if method == "unweighted": + path_length = nx.single_source_shortest_path_length + paths = path_length(G, target) + elif method == "dijkstra": + path_length = nx.single_source_dijkstra_path_length + paths = path_length(G, target, weight=weight) + else: # method == 'bellman-ford': + path_length = nx.single_source_bellman_ford_path_length + paths = path_length(G, target, weight=weight) + else: + if target is None: + # Find paths to all nodes accessible from the source. + if method == "unweighted": + paths = nx.single_source_shortest_path_length(G, source) + elif method == "dijkstra": + path_length = nx.single_source_dijkstra_path_length + paths = path_length(G, source, weight=weight) + else: # method == 'bellman-ford': + path_length = nx.single_source_bellman_ford_path_length + paths = path_length(G, source, weight=weight) + else: + # Find shortest source-target path. + if method == "unweighted": + p = nx.bidirectional_shortest_path(G, source, target) + paths = len(p) - 1 + elif method == "dijkstra": + paths = nx.dijkstra_path_length(G, source, target, weight) + else: # method == 'bellman-ford': + paths = nx.bellman_ford_path_length(G, source, target, weight) + return paths + + +@nx._dispatchable(edge_attrs="weight") +def average_shortest_path_length(G, weight=None, method=None): + r"""Returns the average shortest path length. + + The average shortest path length is + + .. math:: + + a =\sum_{\substack{s,t \in V \\ s\neq t}} \frac{d(s, t)}{n(n-1)} + + where `V` is the set of nodes in `G`, + `d(s, t)` is the shortest path from `s` to `t`, + and `n` is the number of nodes in `G`. + + .. versionchanged:: 3.0 + An exception is raised for directed graphs that are not strongly + connected. + + Parameters + ---------- + G : NetworkX graph + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'unweighted' or 'dijkstra') + The algorithm to use to compute the path lengths. + Supported options are 'unweighted', 'dijkstra', 'bellman-ford', + 'floyd-warshall' and 'floyd-warshall-numpy'. + Other method values produce a ValueError. + The default method is 'unweighted' if `weight` is None, + otherwise the default method is 'dijkstra'. + + Raises + ------ + NetworkXPointlessConcept + If `G` is the null graph (that is, the graph on zero nodes). + + NetworkXError + If `G` is not connected (or not strongly connected, in the case + of a directed graph). + + ValueError + If `method` is not among the supported options. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.average_shortest_path_length(G) + 2.0 + + For disconnected graphs, you can compute the average shortest path + length for each component + + >>> G = nx.Graph([(1, 2), (3, 4)]) + >>> for C in (G.subgraph(c).copy() for c in nx.connected_components(G)): + ... print(nx.average_shortest_path_length(C)) + 1.0 + 1.0 + + """ + single_source_methods = ["unweighted", "dijkstra", "bellman-ford"] + all_pairs_methods = ["floyd-warshall", "floyd-warshall-numpy"] + supported_methods = single_source_methods + all_pairs_methods + + if method is None: + method = "unweighted" if weight is None else "dijkstra" + if method not in supported_methods: + raise ValueError(f"method not supported: {method}") + + n = len(G) + # For the special case of the null graph, raise an exception, since + # there are no paths in the null graph. + if n == 0: + msg = ( + "the null graph has no paths, thus there is no average shortest path length" + ) + raise nx.NetworkXPointlessConcept(msg) + # For the special case of the trivial graph, return zero immediately. + if n == 1: + return 0 + # Shortest path length is undefined if the graph is not strongly connected. + if G.is_directed() and not nx.is_strongly_connected(G): + raise nx.NetworkXError("Graph is not strongly connected.") + # Shortest path length is undefined if the graph is not connected. + if not G.is_directed() and not nx.is_connected(G): + raise nx.NetworkXError("Graph is not connected.") + + # Compute all-pairs shortest paths. + def path_length(v): + if method == "unweighted": + return nx.single_source_shortest_path_length(G, v) + elif method == "dijkstra": + return nx.single_source_dijkstra_path_length(G, v, weight=weight) + elif method == "bellman-ford": + return nx.single_source_bellman_ford_path_length(G, v, weight=weight) + + if method in single_source_methods: + # Sum the distances for each (ordered) pair of source and target node. + s = sum(l for u in G for l in path_length(u).values()) + else: + if method == "floyd-warshall": + all_pairs = nx.floyd_warshall(G, weight=weight) + s = sum(sum(t.values()) for t in all_pairs.values()) + elif method == "floyd-warshall-numpy": + s = float(nx.floyd_warshall_numpy(G, weight=weight).sum()) + return s / (n * (n - 1)) + + +@nx._dispatchable(edge_attrs="weight") +def all_shortest_paths(G, source, target, weight=None, method="dijkstra"): + """Compute all shortest simple paths in the graph. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path. + + target : node + Ending node for path. + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'dijkstra') + The algorithm to use to compute the path lengths. + Supported options: 'dijkstra', 'bellman-ford'. + Other inputs produce a ValueError. + If `weight` is None, unweighted graph methods are used, and this + suggestion is ignored. + + Returns + ------- + paths : generator of lists + A generator of all paths between source and target. + + Raises + ------ + ValueError + If `method` is not among the supported options. + + NetworkXNoPath + If `target` cannot be reached from `source`. + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_path(G, [0, 1, 2]) + >>> nx.add_path(G, [0, 10, 2]) + >>> print([p for p in nx.all_shortest_paths(G, source=0, target=2)]) + [[0, 1, 2], [0, 10, 2]] + + Notes + ----- + There may be many shortest paths between the source and target. If G + contains zero-weight cycles, this function will not produce all shortest + paths because doing so would produce infinitely many paths of unbounded + length -- instead, we only produce the shortest simple paths. + + See Also + -------- + shortest_path + single_source_shortest_path + all_pairs_shortest_path + """ + method = "unweighted" if weight is None else method + if method == "unweighted": + pred = nx.predecessor(G, source) + elif method == "dijkstra": + pred, dist = nx.dijkstra_predecessor_and_distance(G, source, weight=weight) + elif method == "bellman-ford": + pred, dist = nx.bellman_ford_predecessor_and_distance(G, source, weight=weight) + else: + raise ValueError(f"method not supported: {method}") + + return _build_paths_from_predecessors({source}, target, pred) + + +@nx._dispatchable(edge_attrs="weight") +def single_source_all_shortest_paths(G, source, weight=None, method="dijkstra"): + """Compute all shortest simple paths from the given source in the graph. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path. + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'dijkstra') + The algorithm to use to compute the path lengths. + Supported options: 'dijkstra', 'bellman-ford'. + Other inputs produce a ValueError. + If `weight` is None, unweighted graph methods are used, and this + suggestion is ignored. + + Returns + ------- + paths : generator of dictionary + A generator of all paths between source and all nodes in the graph. + + Raises + ------ + ValueError + If `method` is not among the supported options. + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_path(G, [0, 1, 2, 3, 0]) + >>> dict(nx.single_source_all_shortest_paths(G, source=0)) + {0: [[0]], 1: [[0, 1]], 3: [[0, 3]], 2: [[0, 1, 2], [0, 3, 2]]} + + Notes + ----- + There may be many shortest paths between the source and target. If G + contains zero-weight cycles, this function will not produce all shortest + paths because doing so would produce infinitely many paths of unbounded + length -- instead, we only produce the shortest simple paths. + + See Also + -------- + shortest_path + all_shortest_paths + single_source_shortest_path + all_pairs_shortest_path + all_pairs_all_shortest_paths + """ + method = "unweighted" if weight is None else method + if method == "unweighted": + pred = nx.predecessor(G, source) + elif method == "dijkstra": + pred, dist = nx.dijkstra_predecessor_and_distance(G, source, weight=weight) + elif method == "bellman-ford": + pred, dist = nx.bellman_ford_predecessor_and_distance(G, source, weight=weight) + else: + raise ValueError(f"method not supported: {method}") + for n in pred: + yield n, list(_build_paths_from_predecessors({source}, n, pred)) + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_all_shortest_paths(G, weight=None, method="dijkstra"): + """Compute all shortest paths between all nodes. + + Parameters + ---------- + G : NetworkX graph + + weight : None, string or function, optional (default = None) + If None, every edge has weight/distance/cost 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly + three positional arguments: the two endpoints of an edge and + the dictionary of edge attributes for that edge. + The function must return a number. + + method : string, optional (default = 'dijkstra') + The algorithm to use to compute the path lengths. + Supported options: 'dijkstra', 'bellman-ford'. + Other inputs produce a ValueError. + If `weight` is None, unweighted graph methods are used, and this + suggestion is ignored. + + Returns + ------- + paths : generator of dictionary + Dictionary of arrays, keyed by source and target, of all shortest paths. + + Raises + ------ + ValueError + If `method` is not among the supported options. + + Examples + -------- + >>> G = nx.cycle_graph(4) + >>> dict(nx.all_pairs_all_shortest_paths(G))[0][2] + [[0, 1, 2], [0, 3, 2]] + >>> dict(nx.all_pairs_all_shortest_paths(G))[0][3] + [[0, 3]] + + Notes + ----- + There may be multiple shortest paths with equal lengths. Unlike + all_pairs_shortest_path, this method returns all shortest paths. + + See Also + -------- + all_pairs_shortest_path + single_source_all_shortest_paths + """ + for n in G: + yield ( + n, + dict(single_source_all_shortest_paths(G, n, weight=weight, method=method)), + ) + + +def _build_paths_from_predecessors(sources, target, pred): + """Compute all simple paths to target, given the predecessors found in + pred, terminating when any source in sources is found. + + Parameters + ---------- + sources : set + Starting nodes for path. + + target : node + Ending node for path. + + pred : dict + A dictionary of predecessor lists, keyed by node + + Returns + ------- + paths : generator of lists + A generator of all paths between source and target. + + Raises + ------ + NetworkXNoPath + If `target` cannot be reached from `source`. + + Notes + ----- + There may be many paths between the sources and target. If there are + cycles among the predecessors, this function will not produce all + possible paths because doing so would produce infinitely many paths + of unbounded length -- instead, we only produce simple paths. + + See Also + -------- + shortest_path + single_source_shortest_path + all_pairs_shortest_path + all_shortest_paths + bellman_ford_path + """ + if target not in pred: + raise nx.NetworkXNoPath(f"Target {target} cannot be reached from given sources") + + seen = {target} + stack = [[target, 0]] + top = 0 + while top >= 0: + node, i = stack[top] + if node in sources: + yield [p for p, n in reversed(stack[: top + 1])] + if len(pred[node]) > i: + stack[top][1] = i + 1 + next = pred[node][i] + if next in seen: + continue + else: + seen.add(next) + top += 1 + if top == len(stack): + stack.append([next, 0]) + else: + stack[top][:] = [next, 0] + else: + seen.discard(node) + top -= 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/unweighted.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/unweighted.py new file mode 100644 index 0000000000000000000000000000000000000000..4dffeb51825dca4c04ed09fffc48fc93a3d7e63d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/unweighted.py @@ -0,0 +1,625 @@ +""" +Shortest path algorithms for unweighted graphs. +""" + +import operator + +import networkx as nx + +__all__ = [ + "bidirectional_shortest_path", + "single_source_shortest_path", + "single_source_shortest_path_length", + "single_target_shortest_path", + "single_target_shortest_path_length", + "all_pairs_shortest_path", + "all_pairs_shortest_path_length", + "predecessor", +] + + +@nx._dispatchable +def single_source_shortest_path_length(G, source, cutoff=None): + """Compute the shortest path lengths from `source` to all reachable nodes in `G`. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + cutoff : integer, optional + Depth to stop the search. Only target nodes where the shortest path to + this node from the source node contains <= ``cutoff + 1`` nodes will be + included in the returned results. + + Returns + ------- + lengths : dict + Dict keyed by node to shortest path length from source node. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = nx.single_source_shortest_path_length(G, 0) + >>> length[4] + 4 + >>> for node in sorted(length): + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 3 + 4: 4 + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> length = nx.single_source_shortest_path_length(G, 0, cutoff=2) + >>> for node in sorted(length): + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + + See Also + -------- + :any:`shortest_path_length` : + Shortest path length with specifiable source, target, and weight. + :any:`single_source_dijkstra_path_length` : + Shortest weighted path length from source with Dijkstra algorithm. + :any:`single_source_bellman_ford_path_length` : + Shortest weighted path length from source with Bellman-Ford algorithm. + """ + if source not in G: + raise nx.NodeNotFound(f"Source {source} is not in G") + if cutoff is None: + cutoff = float("inf") + nextlevel = [source] + return dict(_single_shortest_path_length(G._adj, nextlevel, cutoff)) + + +def _single_shortest_path_length(adj, firstlevel, cutoff): + """Yields (node, level) in a breadth first search + + Shortest Path Length helper function + Parameters + ---------- + adj : dict + Adjacency dict or view + firstlevel : list + starting nodes, e.g. [source] or [target] + cutoff : int or float + level at which we stop the process + """ + seen = set(firstlevel) + nextlevel = firstlevel + level = 0 + n = len(adj) + for v in nextlevel: + yield (v, level) + while nextlevel and cutoff > level: + level += 1 + thislevel = nextlevel + nextlevel = [] + for v in thislevel: + for w in adj[v]: + if w not in seen: + seen.add(w) + nextlevel.append(w) + yield (w, level) + if len(seen) == n: + return + + +@nx._dispatchable +def single_target_shortest_path_length(G, target, cutoff=None): + """Compute the shortest path lengths to target from all reachable nodes. + + Parameters + ---------- + G : NetworkX graph + + target : node + Target node for path + + cutoff : integer, optional + Depth to stop the search. Only source nodes where the shortest path + from this node to the target node contains <= ``cutoff + 1`` nodes will + be included in the returned results. + + Returns + ------- + lengths : dictionary + Dictionary, keyed by source, of shortest path lengths. + + Examples + -------- + >>> G = nx.path_graph(5, create_using=nx.DiGraph()) + >>> length = nx.single_target_shortest_path_length(G, 4) + >>> length[0] + 4 + >>> for node in sorted(length): + ... print(f"{node}: {length[node]}") + 0: 4 + 1: 3 + 2: 2 + 3: 1 + 4: 0 + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> length = nx.single_target_shortest_path_length(G, 4, cutoff=2) + >>> for node in sorted(length): + ... print(f"{node}: {length[node]}") + 2: 2 + 3: 1 + 4: 0 + + See Also + -------- + single_source_shortest_path_length, shortest_path_length + """ + if target not in G: + raise nx.NodeNotFound(f"Target {target} is not in G") + if cutoff is None: + cutoff = float("inf") + # handle either directed or undirected + adj = G._pred if G.is_directed() else G._adj + nextlevel = [target] + return dict(_single_shortest_path_length(adj, nextlevel, cutoff)) + + +@nx._dispatchable +def all_pairs_shortest_path_length(G, cutoff=None): + """Computes the shortest path lengths between all nodes in `G`. + + Parameters + ---------- + G : NetworkX graph + + cutoff : integer, optional + Depth at which to stop the search. Only paths of length at most + `cutoff` (i.e. paths containing <= ``cutoff + 1`` nodes) are returned. + + Returns + ------- + lengths : iterator + (source, dictionary) iterator with dictionary keyed by target and + shortest path length as the key value. + + Notes + ----- + The iterator returned only has reachable node pairs. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = dict(nx.all_pairs_shortest_path_length(G)) + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"1 - {node}: {length[1][node]}") + 1 - 0: 1 + 1 - 1: 0 + 1 - 2: 1 + 1 - 3: 2 + 1 - 4: 3 + >>> length[3][2] + 1 + >>> length[2][2] + 0 + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> path_lengths = dict(nx.all_pairs_shortest_path_length(G, cutoff=2)) + >>> path_lengths[1] # node 4 is too far away to appear + {1: 0, 0: 1, 2: 1, 3: 2} + """ + length = single_source_shortest_path_length + # TODO This can be trivially parallelized. + for n in G: + yield (n, length(G, n, cutoff=cutoff)) + + +@nx._dispatchable +def bidirectional_shortest_path(G, source, target): + """Returns a list of nodes in a shortest path between source and target. + + Parameters + ---------- + G : NetworkX graph + + source : node label + starting node for path + + target : node label + ending node for path + + Returns + ------- + path: list + List of nodes in a path from source to target. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_path(G, [0, 1, 2, 3, 0, 4, 5, 6, 7, 4]) + >>> nx.bidirectional_shortest_path(G, 2, 6) + [2, 1, 0, 4, 5, 6] + + See Also + -------- + shortest_path + + Notes + ----- + This algorithm is used by shortest_path(G, source, target). + """ + + if source not in G: + raise nx.NodeNotFound(f"Source {source} is not in G") + + if target not in G: + raise nx.NodeNotFound(f"Target {target} is not in G") + + # call helper to do the real work + results = _bidirectional_pred_succ(G, source, target) + pred, succ, w = results + + # build path from pred+w+succ + path = [] + # from source to w + while w is not None: + path.append(w) + w = pred[w] + path.reverse() + # from w to target + w = succ[path[-1]] + while w is not None: + path.append(w) + w = succ[w] + + return path + + +def _bidirectional_pred_succ(G, source, target): + """Bidirectional shortest path helper. + + Returns (pred, succ, w) where + pred is a dictionary of predecessors from w to the source, and + succ is a dictionary of successors from w to the target. + """ + # does BFS from both source and target and meets in the middle + if target == source: + return ({target: None}, {source: None}, source) + + # handle either directed or undirected + if G.is_directed(): + Gpred = G.pred + Gsucc = G.succ + else: + Gpred = G.adj + Gsucc = G.adj + + # predecessor and successors in search + pred = {source: None} + succ = {target: None} + + # initialize fringes, start with forward + forward_fringe = [source] + reverse_fringe = [target] + + while forward_fringe and reverse_fringe: + if len(forward_fringe) <= len(reverse_fringe): + this_level = forward_fringe + forward_fringe = [] + for v in this_level: + for w in Gsucc[v]: + if w not in pred: + forward_fringe.append(w) + pred[w] = v + if w in succ: # path found + return pred, succ, w + else: + this_level = reverse_fringe + reverse_fringe = [] + for v in this_level: + for w in Gpred[v]: + if w not in succ: + succ[w] = v + reverse_fringe.append(w) + if w in pred: # found path + return pred, succ, w + + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") + + +@nx._dispatchable +def single_source_shortest_path(G, source, cutoff=None): + """Compute shortest path between source + and all other nodes reachable from source. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + cutoff : integer, optional + Depth to stop the search. Only target nodes where the shortest path to + this node from the source node contains <= ``cutoff + 1`` nodes will be + included in the returned results. + + Returns + ------- + paths : dictionary + Dictionary, keyed by target, of shortest paths. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.single_source_shortest_path(G, 0) + {0: [0], 1: [0, 1], 2: [0, 1, 2], 3: [0, 1, 2, 3], 4: [0, 1, 2, 3, 4]} + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> nx.single_source_shortest_path(G, 0, cutoff=2) + {0: [0], 1: [0, 1], 2: [0, 1, 2]} + + Notes + ----- + The shortest path is not necessarily unique. So there can be multiple + paths between the source and each target node, all of which have the + same 'shortest' length. For each target node, this function returns + only one of those paths. + + See Also + -------- + shortest_path + """ + if source not in G: + raise nx.NodeNotFound(f"Source {source} not in G") + if cutoff is None: + cutoff = float("inf") + nextlevel = [source] # list of nodes to check at next level + paths = {source: [source]} # paths dictionary (paths to key from source) + return dict(_single_shortest_path(G._adj, nextlevel, paths, cutoff, operator.add)) + + +def _single_shortest_path(adj, firstlevel, paths, cutoff, join): + """Returns shortest paths + + Shortest Path helper function + Parameters + ---------- + adj : dict + Adjacency dict or view + firstlevel : list + starting nodes, e.g. [source] or [target] + paths : dict + paths for starting nodes, e.g. {source: [source]} + cutoff : int or float + level at which we stop the process + join : function + function to construct a path from two partial paths. Requires two + list inputs `p1` and `p2`, and returns a list. Usually returns + `p1 + p2` (forward from source) or `p2 + p1` (backward from target) + """ + level = 0 + nextlevel = firstlevel + n = len(adj) + while nextlevel and cutoff > level: + thislevel = nextlevel + nextlevel = [] + for v in thislevel: + for w in adj[v]: + if w not in paths: + paths[w] = join(paths[v], [w]) + nextlevel.append(w) + if len(paths) == n: + return paths + level += 1 + return paths + + +@nx._dispatchable +def single_target_shortest_path(G, target, cutoff=None): + """Compute shortest path to target from all nodes that reach target. + + Parameters + ---------- + G : NetworkX graph + + target : node label + Target node for path + + cutoff : integer, optional + Depth to stop the search. Only source nodes where the shortest path + from this node to the target node contains <= ``cutoff + 1`` nodes will + be included in the returned results. + + Returns + ------- + paths : dictionary + Dictionary, keyed by source, of shortest paths. + + Examples + -------- + >>> G = nx.path_graph(5, create_using=nx.DiGraph()) + >>> nx.single_target_shortest_path(G, 4) + {4: [4], 3: [3, 4], 2: [2, 3, 4], 1: [1, 2, 3, 4], 0: [0, 1, 2, 3, 4]} + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> nx.single_target_shortest_path(G, 4, cutoff=2) + {4: [4], 3: [3, 4], 2: [2, 3, 4]} + + Notes + ----- + The shortest path is not necessarily unique. So there can be multiple + paths between the source and each target node, all of which have the + same 'shortest' length. For each target node, this function returns + only one of those paths. + + See Also + -------- + shortest_path, single_source_shortest_path + """ + if target not in G: + raise nx.NodeNotFound(f"Target {target} not in G") + + def join(p1, p2): + return p2 + p1 + + # handle undirected graphs + adj = G._pred if G.is_directed() else G._adj + if cutoff is None: + cutoff = float("inf") + nextlevel = [target] # list of nodes to check at next level + paths = {target: [target]} # paths dictionary (paths to key from source) + return dict(_single_shortest_path(adj, nextlevel, paths, cutoff, join)) + + +@nx._dispatchable +def all_pairs_shortest_path(G, cutoff=None): + """Compute shortest paths between all nodes. + + Parameters + ---------- + G : NetworkX graph + + cutoff : integer, optional + Depth at which to stop the search. Only paths containing at most + ``cutoff + 1`` nodes are returned. + + Returns + ------- + paths : iterator + Dictionary, keyed by source and target, of shortest paths. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = dict(nx.all_pairs_shortest_path(G)) + >>> print(path[0]) + {0: [0], 1: [0, 1], 2: [0, 1, 2], 3: [0, 1, 2, 3], 4: [0, 1, 2, 3, 4]} + + Only include paths with length less than or equal to the `cutoff` keyword + argument: + + >>> path = dict(nx.all_pairs_shortest_path(G, cutoff=2)) + >>> print(path[0]) + {0: [0], 1: [0, 1], 2: [0, 1, 2]} + + Notes + ----- + There may be multiple shortest paths with the same length between + two nodes. For each pair, this function returns only one of those paths. + + See Also + -------- + floyd_warshall + all_pairs_all_shortest_paths + + """ + # TODO This can be trivially parallelized. + for n in G: + yield (n, single_source_shortest_path(G, n, cutoff=cutoff)) + + +@nx._dispatchable +def predecessor(G, source, target=None, cutoff=None, return_seen=None): + """Returns dict of predecessors for the path from source to all nodes in G. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + target : node label, optional + Ending node for path. If provided only predecessors between + source and target are returned + + cutoff : integer, optional + Depth to stop the search. Only paths of length <= `cutoff` are + returned. + + return_seen : bool, optional (default=None) + Whether to return a dictionary, keyed by node, of the level (number of + hops) to reach the node (as seen during breadth-first-search). + + Returns + ------- + pred : dictionary + Dictionary, keyed by node, of predecessors in the shortest path. + + + (pred, seen): tuple of dictionaries + If `return_seen` argument is set to `True`, then a tuple of dictionaries + is returned. The first element is the dictionary, keyed by node, of + predecessors in the shortest path. The second element is the dictionary, + keyed by node, of the level (number of hops) to reach the node (as seen + during breadth-first-search). + + Examples + -------- + >>> G = nx.path_graph(4) + >>> list(G) + [0, 1, 2, 3] + >>> nx.predecessor(G, 0) + {0: [], 1: [0], 2: [1], 3: [2]} + >>> nx.predecessor(G, 0, cutoff=2) + {0: [], 1: [0], 2: [1]} + >>> nx.predecessor(G, 0, return_seen=True) + ({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3}) + + + """ + if source not in G: + raise nx.NodeNotFound(f"Source {source} not in G") + + level = 0 # the current level + nextlevel = [source] # list of nodes to check at next level + seen = {source: level} # level (number of hops) when seen in BFS + pred = {source: []} # predecessor dictionary + while nextlevel: + level = level + 1 + thislevel = nextlevel + nextlevel = [] + for v in thislevel: + for w in G[v]: + if w not in seen: + pred[w] = [v] + seen[w] = level + nextlevel.append(w) + elif seen[w] == level: # add v to predecessor list if it + pred[w].append(v) # is at the correct level + if cutoff and cutoff <= level: + break + + if target is not None: + if return_seen: + if target not in pred: + return ([], -1) # No predecessor + return (pred[target], seen[target]) + else: + if target not in pred: + return [] # No predecessor + return pred[target] + else: + if return_seen: + return (pred, seen) + else: + return pred diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/weighted.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/weighted.py new file mode 100644 index 0000000000000000000000000000000000000000..a75398be79a83bee9bb1ecc6f6ce8fd62bca5ee4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/shortest_paths/weighted.py @@ -0,0 +1,2542 @@ +""" +Shortest path algorithms for weighted graphs. +""" + +from collections import deque +from heapq import heappop, heappush +from itertools import count, islice + +import networkx as nx +from networkx.algorithms.shortest_paths.generic import _build_paths_from_predecessors + +__all__ = [ + "dijkstra_path", + "dijkstra_path_length", + "bidirectional_dijkstra", + "single_source_dijkstra", + "single_source_dijkstra_path", + "single_source_dijkstra_path_length", + "multi_source_dijkstra", + "multi_source_dijkstra_path", + "multi_source_dijkstra_path_length", + "all_pairs_dijkstra", + "all_pairs_dijkstra_path", + "all_pairs_dijkstra_path_length", + "dijkstra_predecessor_and_distance", + "bellman_ford_path", + "bellman_ford_path_length", + "single_source_bellman_ford", + "single_source_bellman_ford_path", + "single_source_bellman_ford_path_length", + "all_pairs_bellman_ford_path", + "all_pairs_bellman_ford_path_length", + "bellman_ford_predecessor_and_distance", + "negative_edge_cycle", + "find_negative_cycle", + "goldberg_radzik", + "johnson", +] + + +def _weight_function(G, weight): + """Returns a function that returns the weight of an edge. + + The returned function is specifically suitable for input to + functions :func:`_dijkstra` and :func:`_bellman_ford_relaxation`. + + Parameters + ---------- + G : NetworkX graph. + + weight : string or function + If it is callable, `weight` itself is returned. If it is a string, + it is assumed to be the name of the edge attribute that represents + the weight of an edge. In that case, a function is returned that + gets the edge weight according to the specified edge attribute. + + Returns + ------- + function + This function returns a callable that accepts exactly three inputs: + a node, an node adjacent to the first one, and the edge attribute + dictionary for the eedge joining those nodes. That function returns + a number representing the weight of an edge. + + If `G` is a multigraph, and `weight` is not callable, the + minimum edge weight over all parallel edges is returned. If any edge + does not have an attribute with key `weight`, it is assumed to + have weight one. + + """ + if callable(weight): + return weight + # If the weight keyword argument is not callable, we assume it is a + # string representing the edge attribute containing the weight of + # the edge. + if G.is_multigraph(): + return lambda u, v, d: min(attr.get(weight, 1) for attr in d.values()) + return lambda u, v, data: data.get(weight, 1) + + +@nx._dispatchable(edge_attrs="weight") +def dijkstra_path(G, source, target, weight="weight"): + """Returns the shortest weighted path from source to target in G. + + Uses Dijkstra's Method to compute the shortest weighted path + between two nodes in a graph. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node + + target : node + Ending node + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + path : list + List of nodes in a shortest path. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> print(nx.dijkstra_path(G, 0, 4)) + [0, 1, 2, 3, 4] + + Find edges of shortest path in Multigraph + + >>> G = nx.MultiDiGraph() + >>> G.add_weighted_edges_from([(1, 2, 0.75), (1, 2, 0.5), (2, 3, 0.5), (1, 3, 1.5)]) + >>> nodes = nx.dijkstra_path(G, 1, 3) + >>> edges = nx.utils.pairwise(nodes) + >>> list( + ... (u, v, min(G[u][v], key=lambda k: G[u][v][k].get("weight", 1))) + ... for u, v in edges + ... ) + [(1, 2, 1), (2, 3, 0)] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + The weight function can be used to include node weights. + + >>> def func(u, v, d): + ... node_u_wt = G.nodes[u].get("node_weight", 1) + ... node_v_wt = G.nodes[v].get("node_weight", 1) + ... edge_wt = d.get("weight", 1) + ... return node_u_wt / 2 + node_v_wt / 2 + edge_wt + + In this example we take the average of start and end node + weights of an edge and add it to the weight of the edge. + + The function :func:`single_source_dijkstra` computes both + path and length-of-path if you need both, use that. + + See Also + -------- + bidirectional_dijkstra + bellman_ford_path + single_source_dijkstra + """ + (length, path) = single_source_dijkstra(G, source, target=target, weight=weight) + return path + + +@nx._dispatchable(edge_attrs="weight") +def dijkstra_path_length(G, source, target, weight="weight"): + """Returns the shortest weighted path length in G from source to target. + + Uses Dijkstra's Method to compute the shortest weighted path length + between two nodes in a graph. + + Parameters + ---------- + G : NetworkX graph + + source : node label + starting node for path + + target : node label + ending node for path + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + length : number + Shortest path length. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.dijkstra_path_length(G, 0, 4) + 4 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + The function :func:`single_source_dijkstra` computes both + path and length-of-path if you need both, use that. + + See Also + -------- + bidirectional_dijkstra + bellman_ford_path_length + single_source_dijkstra + + """ + if source not in G: + raise nx.NodeNotFound(f"Node {source} not found in graph") + if source == target: + return 0 + weight = _weight_function(G, weight) + length = _dijkstra(G, source, weight, target=target) + try: + return length[target] + except KeyError as err: + raise nx.NetworkXNoPath(f"Node {target} not reachable from {source}") from err + + +@nx._dispatchable(edge_attrs="weight") +def single_source_dijkstra_path(G, source, cutoff=None, weight="weight"): + """Find shortest weighted paths in G from a source node. + + Compute shortest path between source and all other reachable + nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path. + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + paths : dictionary + Dictionary of shortest path lengths keyed by target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = nx.single_source_dijkstra_path(G, 0) + >>> path[4] + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + See Also + -------- + single_source_dijkstra, single_source_bellman_ford + + """ + return multi_source_dijkstra_path(G, {source}, cutoff=cutoff, weight=weight) + + +@nx._dispatchable(edge_attrs="weight") +def single_source_dijkstra_path_length(G, source, cutoff=None, weight="weight"): + """Find shortest weighted path lengths in G from a source node. + + Compute the shortest path length between source and all other + reachable nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + length : dict + Dict keyed by node to shortest path length from source. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = nx.single_source_dijkstra_path_length(G, 0) + >>> length[4] + 4 + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 3 + 4: 4 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + See Also + -------- + single_source_dijkstra, single_source_bellman_ford_path_length + + """ + return multi_source_dijkstra_path_length(G, {source}, cutoff=cutoff, weight=weight) + + +@nx._dispatchable(edge_attrs="weight") +def single_source_dijkstra(G, source, target=None, cutoff=None, weight="weight"): + """Find shortest weighted paths and lengths from a source node. + + Compute the shortest path length between source and all other + reachable nodes for a weighted graph. + + Uses Dijkstra's algorithm to compute shortest paths and lengths + between a source and all other reachable nodes in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + target : node label, optional + Ending node for path + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + distance, path : pair of dictionaries, or numeric and list. + If target is None, paths and lengths to all nodes are computed. + The return value is a tuple of two dictionaries keyed by target nodes. + The first dictionary stores distance to each target node. + The second stores the path to each target node. + If target is not None, returns a tuple (distance, path), where + distance is the distance from source to target and path is a list + representing the path from source to target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length, path = nx.single_source_dijkstra(G, 0) + >>> length[4] + 4 + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 3 + 4: 4 + >>> path[4] + [0, 1, 2, 3, 4] + >>> length, path = nx.single_source_dijkstra(G, 0, 1) + >>> length + 1 + >>> path + [0, 1] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + Based on the Python cookbook recipe (119466) at + https://code.activestate.com/recipes/119466/ + + This algorithm is not guaranteed to work if edge weights + are negative or are floating point numbers + (overflows and roundoff errors can cause problems). + + See Also + -------- + single_source_dijkstra_path + single_source_dijkstra_path_length + single_source_bellman_ford + """ + return multi_source_dijkstra( + G, {source}, cutoff=cutoff, target=target, weight=weight + ) + + +@nx._dispatchable(edge_attrs="weight") +def multi_source_dijkstra_path(G, sources, cutoff=None, weight="weight"): + """Find shortest weighted paths in G from a given set of source + nodes. + + Compute shortest path between any of the source nodes and all other + reachable nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + sources : non-empty set of nodes + Starting nodes for paths. If this is just a set containing a + single node, then all paths computed by this function will start + from that node. If there are two or more nodes in the set, the + computed paths may begin from any one of the start nodes. + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + paths : dictionary + Dictionary of shortest paths keyed by target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = nx.multi_source_dijkstra_path(G, {0, 4}) + >>> path[1] + [0, 1] + >>> path[3] + [4, 3] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + Raises + ------ + ValueError + If `sources` is empty. + NodeNotFound + If any of `sources` is not in `G`. + + See Also + -------- + multi_source_dijkstra, multi_source_bellman_ford + + """ + length, path = multi_source_dijkstra(G, sources, cutoff=cutoff, weight=weight) + return path + + +@nx._dispatchable(edge_attrs="weight") +def multi_source_dijkstra_path_length(G, sources, cutoff=None, weight="weight"): + """Find shortest weighted path lengths in G from a given set of + source nodes. + + Compute the shortest path length between any of the source nodes and + all other reachable nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + sources : non-empty set of nodes + Starting nodes for paths. If this is just a set containing a + single node, then all paths computed by this function will start + from that node. If there are two or more nodes in the set, the + computed paths may begin from any one of the start nodes. + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + length : dict + Dict keyed by node to shortest path length to nearest source. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = nx.multi_source_dijkstra_path_length(G, {0, 4}) + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 1 + 4: 0 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + Raises + ------ + ValueError + If `sources` is empty. + NodeNotFound + If any of `sources` is not in `G`. + + See Also + -------- + multi_source_dijkstra + + """ + if not sources: + raise ValueError("sources must not be empty") + for s in sources: + if s not in G: + raise nx.NodeNotFound(f"Node {s} not found in graph") + weight = _weight_function(G, weight) + return _dijkstra_multisource(G, sources, weight, cutoff=cutoff) + + +@nx._dispatchable(edge_attrs="weight") +def multi_source_dijkstra(G, sources, target=None, cutoff=None, weight="weight"): + """Find shortest weighted paths and lengths from a given set of + source nodes. + + Uses Dijkstra's algorithm to compute the shortest paths and lengths + between one of the source nodes and the given `target`, or all other + reachable nodes if not specified, for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + sources : non-empty set of nodes + Starting nodes for paths. If this is just a set containing a + single node, then all paths computed by this function will start + from that node. If there are two or more nodes in the set, the + computed paths may begin from any one of the start nodes. + + target : node label, optional + Ending node for path + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + distance, path : pair of dictionaries, or numeric and list + If target is None, returns a tuple of two dictionaries keyed by node. + The first dictionary stores distance from one of the source nodes. + The second stores the path from one of the sources to that node. + If target is not None, returns a tuple of (distance, path) where + distance is the distance from source to target and path is a list + representing the path from source to target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length, path = nx.multi_source_dijkstra(G, {0, 4}) + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 1 + 4: 0 + >>> path[1] + [0, 1] + >>> path[3] + [4, 3] + + >>> length, path = nx.multi_source_dijkstra(G, {0, 4}, 1) + >>> length + 1 + >>> path + [0, 1] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + Based on the Python cookbook recipe (119466) at + https://code.activestate.com/recipes/119466/ + + This algorithm is not guaranteed to work if edge weights + are negative or are floating point numbers + (overflows and roundoff errors can cause problems). + + Raises + ------ + ValueError + If `sources` is empty. + NodeNotFound + If any of `sources` is not in `G`. + + See Also + -------- + multi_source_dijkstra_path + multi_source_dijkstra_path_length + + """ + if not sources: + raise ValueError("sources must not be empty") + for s in sources: + if s not in G: + raise nx.NodeNotFound(f"Node {s} not found in graph") + if target in sources: + return (0, [target]) + weight = _weight_function(G, weight) + paths = {source: [source] for source in sources} # dictionary of paths + dist = _dijkstra_multisource( + G, sources, weight, paths=paths, cutoff=cutoff, target=target + ) + if target is None: + return (dist, paths) + try: + return (dist[target], paths[target]) + except KeyError as err: + raise nx.NetworkXNoPath(f"No path to {target}.") from err + + +def _dijkstra(G, source, weight, pred=None, paths=None, cutoff=None, target=None): + """Uses Dijkstra's algorithm to find shortest weighted paths from a + single source. + + This is a convenience function for :func:`_dijkstra_multisource` + with all the arguments the same, except the keyword argument + `sources` set to ``[source]``. + + """ + return _dijkstra_multisource( + G, [source], weight, pred=pred, paths=paths, cutoff=cutoff, target=target + ) + + +def _dijkstra_multisource( + G, sources, weight, pred=None, paths=None, cutoff=None, target=None +): + """Uses Dijkstra's algorithm to find shortest weighted paths + + Parameters + ---------- + G : NetworkX graph + + sources : non-empty iterable of nodes + Starting nodes for paths. If this is just an iterable containing + a single node, then all paths computed by this function will + start from that node. If there are two or more nodes in this + iterable, the computed paths may begin from any one of the start + nodes. + + weight: function + Function with (u, v, data) input that returns that edge's weight + or None to indicate a hidden edge + + pred: dict of lists, optional(default=None) + dict to store a list of predecessors keyed by that node + If None, predecessors are not stored. + + paths: dict, optional (default=None) + dict to store the path list from source to each node, keyed by node. + If None, paths are not stored. + + target : node label, optional + Ending node for path. Search is halted when target is found. + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + Returns + ------- + distance : dictionary + A mapping from node to shortest distance to that node from one + of the source nodes. + + Raises + ------ + NodeNotFound + If any of `sources` is not in `G`. + + Notes + ----- + The optional predecessor and path dictionaries can be accessed by + the caller through the original pred and paths objects passed + as arguments. No need to explicitly return pred or paths. + + """ + # If `paths` is specified, we use a temporary internal dictionary (`pred_dict`) to + # store predecessors, used to reconstruct paths. However, if the caller + # passed in a `pred` dictionary, we must compute *all* predecessors, since the caller + # expects the full predecessor structure. + pred_dict = pred if paths is None or pred is not None else {} + + G_succ = G._adj # For speed-up (and works for both directed and undirected graphs) + + dist = {} # dictionary of final distances + seen = {} + # fringe is heapq with 3-tuples (distance,c,node) + # use the count c to avoid comparing nodes (may not be able to) + c = count() + fringe = [] + for source in sources: + seen[source] = 0 + heappush(fringe, (0, next(c), source)) + number_of_sources = len(seen) + while fringe: + (dist_v, _, v) = heappop(fringe) + if v in dist: + continue # already searched this node. + dist[v] = dist_v + if v == target: + break + for u, e in G_succ[v].items(): + cost = weight(v, u, e) + if cost is None: + continue + vu_dist = dist_v + cost + if cutoff is not None and vu_dist > cutoff: + continue + if u in dist: + u_dist = dist[u] + if vu_dist < u_dist: + raise ValueError("Contradictory paths found:", "negative weights?") + elif pred is not None and vu_dist == u_dist: + # Found another shortest path to u with equal distance (including zero-weight edges). + # We must store *all* predecessors because `pred` was provided by the caller. + pred_dict[u].append(v) + elif u not in seen or vu_dist < seen[u]: + seen[u] = vu_dist + heappush(fringe, (vu_dist, next(c), u)) + if pred_dict is not None: + pred_dict[u] = [v] + elif pred is not None and vu_dist == seen[u]: + # Found another shortest path to u + # We must store *all* predecessors because `pred` was provided by the caller. + pred_dict[u].append(v) + + if paths is not None: + # Reconstruct the path from source to target using the predecessor dictionary. + if target is None: + # Since `dist` is in increasing distance order, each predecessor's path is + # already computed by the time we process `v`. We skip the first + # `number_of_sources` entries because sources already have their paths defined. + for v in islice(dist, number_of_sources, None): + # `v` must be in `pred_dict`: any node with a distance (and not a source) + # has a predecessor. + paths[v] = paths[pred_dict[v][0]] + [v] + else: + # Caller requested the path to a specific target node. + path = paths[target] = [target] + while (current_preds := pred_dict.get(path[-1])) is not None: + path.append(current_preds[0]) + # The path was built in reverse order, so reverse it at the end. + path.reverse() + + # The optional predecessor and path dictionaries can be accessed + # by the caller via the pred and paths objects passed as arguments. + return dist + + +@nx._dispatchable(edge_attrs="weight") +def dijkstra_predecessor_and_distance(G, source, cutoff=None, weight="weight"): + """Compute weighted shortest path length and predecessors. + + Uses Dijkstra's Method to obtain the shortest weighted paths + and return dictionaries of predecessors for each node and + distance for each node from the `source`. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + pred, distance : dictionaries + Returns two dictionaries representing a list of predecessors + of a node and the distance to each node. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The list of predecessors contains more than one element only when + there are more than one shortest paths to the key node. + + Examples + -------- + >>> G = nx.path_graph(5, create_using=nx.DiGraph()) + >>> pred, dist = nx.dijkstra_predecessor_and_distance(G, 0) + >>> sorted(pred.items()) + [(0, []), (1, [0]), (2, [1]), (3, [2]), (4, [3])] + >>> sorted(dist.items()) + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] + + >>> pred, dist = nx.dijkstra_predecessor_and_distance(G, 0, 1) + >>> sorted(pred.items()) + [(0, []), (1, [0])] + >>> sorted(dist.items()) + [(0, 0), (1, 1)] + """ + if source not in G: + raise nx.NodeNotFound(f"Node {source} is not found in the graph") + weight = _weight_function(G, weight) + pred = {source: []} # dictionary of predecessors + return (pred, _dijkstra(G, source, weight, pred=pred, cutoff=cutoff)) + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_dijkstra(G, cutoff=None, weight="weight"): + """Find shortest weighted paths and lengths between all nodes. + + Parameters + ---------- + G : NetworkX graph + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edge[u][v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Yields + ------ + (node, (distance, path)) : (node obj, (dict, dict)) + Each source node has two associated dicts. The first holds distance + keyed by target and the second holds paths keyed by target. + (See single_source_dijkstra for the source/target node terminology.) + If desired you can apply `dict()` to this function to create a dict + keyed by source node to the two dicts. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> len_path = dict(nx.all_pairs_dijkstra(G)) + >>> len_path[3][0][1] + 2 + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"3 - {node}: {len_path[3][0][node]}") + 3 - 0: 3 + 3 - 1: 2 + 3 - 2: 1 + 3 - 3: 0 + 3 - 4: 1 + >>> len_path[3][1][1] + [3, 2, 1] + >>> for n, (dist, path) in nx.all_pairs_dijkstra(G): + ... print(path[1]) + [0, 1] + [1] + [2, 1] + [3, 2, 1] + [4, 3, 2, 1] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The yielded dicts only have keys for reachable nodes. + """ + for n in G: + dist, path = single_source_dijkstra(G, n, cutoff=cutoff, weight=weight) + yield (n, (dist, path)) + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_dijkstra_path_length(G, cutoff=None, weight="weight"): + """Compute shortest path lengths between all nodes in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + distance : iterator + (source, dictionary) iterator with dictionary keyed by target and + shortest path length as the key value. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = dict(nx.all_pairs_dijkstra_path_length(G)) + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"1 - {node}: {length[1][node]}") + 1 - 0: 1 + 1 - 1: 0 + 1 - 2: 1 + 1 - 3: 2 + 1 - 4: 3 + >>> length[3][2] + 1 + >>> length[2][2] + 0 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The dictionary returned only has keys for reachable node pairs. + """ + length = single_source_dijkstra_path_length + for n in G: + yield (n, length(G, n, cutoff=cutoff, weight=weight)) + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_dijkstra_path(G, cutoff=None, weight="weight"): + """Compute shortest paths between all nodes in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + cutoff : integer or float, optional + Length (sum of edge weights) at which the search is stopped. + If cutoff is provided, only return paths with summed weight <= cutoff. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + paths : iterator + (source, dictionary) iterator with dictionary keyed by target and + shortest path as the key value. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = dict(nx.all_pairs_dijkstra_path(G)) + >>> path[0][4] + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + floyd_warshall, all_pairs_bellman_ford_path + + """ + path = single_source_dijkstra_path + # TODO This can be trivially parallelized. + for n in G: + yield (n, path(G, n, cutoff=cutoff, weight=weight)) + + +@nx._dispatchable(edge_attrs="weight") +def bellman_ford_predecessor_and_distance( + G, source, target=None, weight="weight", heuristic=False +): + """Compute shortest path lengths and predecessors on shortest paths + in weighted graphs. + + The algorithm has a running time of $O(mn)$ where $n$ is the number of + nodes and $m$ is the number of edges. It is slower than Dijkstra but + can handle negative edge weights. + + If a negative cycle is detected, you can use :func:`find_negative_cycle` + to return the cycle and examine it. Shortest paths are not defined when + a negative cycle exists because once reached, the path can cycle forever + to build up arbitrarily low weights. + + Parameters + ---------- + G : NetworkX graph + The algorithm works for all types of graphs, including directed + graphs and multigraphs. + + source: node label + Starting node for path + + target : node label, optional + Ending node for path + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + heuristic : bool + Determines whether to use a heuristic to early detect negative + cycles at a hopefully negligible cost. + + Returns + ------- + pred, dist : dictionaries + Returns two dictionaries keyed by node to predecessor in the + path and to the distance from the source respectively. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXUnbounded + If the (di)graph contains a negative (di)cycle, the + algorithm raises an exception to indicate the presence of the + negative (di)cycle. Note: any negative weight edge in an + undirected graph is a negative cycle. + + Examples + -------- + >>> G = nx.path_graph(5, create_using=nx.DiGraph()) + >>> pred, dist = nx.bellman_ford_predecessor_and_distance(G, 0) + >>> sorted(pred.items()) + [(0, []), (1, [0]), (2, [1]), (3, [2]), (4, [3])] + >>> sorted(dist.items()) + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] + + >>> pred, dist = nx.bellman_ford_predecessor_and_distance(G, 0, 1) + >>> sorted(pred.items()) + [(0, []), (1, [0]), (2, [1]), (3, [2]), (4, [3])] + >>> sorted(dist.items()) + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] + + >>> G = nx.cycle_graph(5, create_using=nx.DiGraph()) + >>> G[1][2]["weight"] = -7 + >>> nx.bellman_ford_predecessor_and_distance(G, 0) + Traceback (most recent call last): + ... + networkx.exception.NetworkXUnbounded: Negative cycle detected. + + See Also + -------- + find_negative_cycle + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The dictionaries returned only have keys for nodes reachable from + the source. + + In the case where the (di)graph is not connected, if a component + not containing the source contains a negative (di)cycle, it + will not be detected. + + In NetworkX v2.1 and prior, the source node had predecessor `[None]`. + In NetworkX v2.2 this changed to the source node having predecessor `[]` + """ + if source not in G: + raise nx.NodeNotFound(f"Node {source} is not found in the graph") + weight = _weight_function(G, weight) + if G.is_multigraph(): + if any( + weight(u, v, {k: d}) < 0 + for u, v, k, d in nx.selfloop_edges(G, keys=True, data=True) + ): + raise nx.NetworkXUnbounded("Negative cycle detected.") + else: + if any(weight(u, v, d) < 0 for u, v, d in nx.selfloop_edges(G, data=True)): + raise nx.NetworkXUnbounded("Negative cycle detected.") + + dist = {source: 0} + pred = {source: []} + + if len(G) == 1: + return pred, dist + + weight = _weight_function(G, weight) + + dist = _bellman_ford( + G, [source], weight, pred=pred, dist=dist, target=target, heuristic=heuristic + ) + return (pred, dist) + + +def _bellman_ford( + G, + source, + weight, + pred=None, + paths=None, + dist=None, + target=None, + heuristic=True, +): + """Calls relaxation loop for Bellman–Ford algorithm and builds paths + + This is an implementation of the SPFA variant. + See https://en.wikipedia.org/wiki/Shortest_Path_Faster_Algorithm + + Parameters + ---------- + G : NetworkX graph + + source: list + List of source nodes. The shortest path from any of the source + nodes will be found if multiple sources are provided. + + weight : function + The weight of an edge is the value returned by the function. The + function must accept exactly three positional arguments: the two + endpoints of an edge and the dictionary of edge attributes for + that edge. The function must return a number. + + pred: dict of lists, optional (default=None) + dict to store a list of predecessors keyed by that node + If None, predecessors are not stored + + paths: dict, optional (default=None) + dict to store the path list from source to each node, keyed by node + If None, paths are not stored + + dist: dict, optional (default=None) + dict to store distance from source to the keyed node + If None, returned dist dict contents default to 0 for every node in the + source list + + target: node label, optional + Ending node for path. Path lengths to other destinations may (and + probably will) be incorrect. + + heuristic : bool + Determines whether to use a heuristic to early detect negative + cycles at a hopefully negligible cost. + + Returns + ------- + dist : dict + Returns a dict keyed by node to the distance from the source. + Dicts for paths and pred are in the mutated input dicts by those names. + + Raises + ------ + NodeNotFound + If any of `source` is not in `G`. + + NetworkXUnbounded + If the (di)graph contains a negative (di)cycle, the + algorithm raises an exception to indicate the presence of the + negative (di)cycle. Note: any negative weight edge in an + undirected graph is a negative cycle + """ + if pred is None: + pred = {v: [] for v in source} + + if dist is None: + dist = {v: 0 for v in source} + + negative_cycle_found = _inner_bellman_ford( + G, + source, + weight, + pred, + dist, + heuristic, + ) + if negative_cycle_found is not None: + raise nx.NetworkXUnbounded("Negative cycle detected.") + + if paths is not None: + sources = set(source) + dsts = [target] if target is not None else pred + for dst in dsts: + gen = _build_paths_from_predecessors(sources, dst, pred) + paths[dst] = next(gen) + + return dist + + +def _inner_bellman_ford( + G, + sources, + weight, + pred, + dist=None, + heuristic=True, +): + """Inner Relaxation loop for Bellman–Ford algorithm. + + This is an implementation of the SPFA variant. + See https://en.wikipedia.org/wiki/Shortest_Path_Faster_Algorithm + + Parameters + ---------- + G : NetworkX graph + + source: list + List of source nodes. The shortest path from any of the source + nodes will be found if multiple sources are provided. + + weight : function + The weight of an edge is the value returned by the function. The + function must accept exactly three positional arguments: the two + endpoints of an edge and the dictionary of edge attributes for + that edge. The function must return a number. + + pred: dict of lists + dict to store a list of predecessors keyed by that node + + dist: dict, optional (default=None) + dict to store distance from source to the keyed node + If None, returned dist dict contents default to 0 for every node in the + source list + + heuristic : bool + Determines whether to use a heuristic to early detect negative + cycles at a hopefully negligible cost. + + Returns + ------- + node or None + Return a node `v` where processing discovered a negative cycle. + If no negative cycle found, return None. + + Raises + ------ + NodeNotFound + If any of `source` is not in `G`. + """ + for s in sources: + if s not in G: + raise nx.NodeNotFound(f"Source {s} not in G") + + if pred is None: + pred = {v: [] for v in sources} + + if dist is None: + dist = {v: 0 for v in sources} + + # Heuristic Storage setup. Note: use None because nodes cannot be None + nonexistent_edge = (None, None) + pred_edge = {v: None for v in sources} + recent_update = {v: nonexistent_edge for v in sources} + + G_succ = G._adj # For speed-up (and works for both directed and undirected graphs) + inf = float("inf") + n = len(G) + + count = {} + q = deque(sources) + in_q = set(sources) + while q: + u = q.popleft() + in_q.remove(u) + + # Skip relaxations if any of the predecessors of u is in the queue. + if all(pred_u not in in_q for pred_u in pred[u]): + dist_u = dist[u] + for v, e in G_succ[u].items(): + dist_v = dist_u + weight(u, v, e) + + if dist_v < dist.get(v, inf): + # In this conditional branch we are updating the path with v. + # If it happens that some earlier update also added node v + # that implies the existence of a negative cycle since + # after the update node v would lie on the update path twice. + # The update path is stored up to one of the source nodes, + # therefore u is always in the dict recent_update + if heuristic: + if v in recent_update[u]: + # Negative cycle found! + pred[v].append(u) + return v + + # Transfer the recent update info from u to v if the + # same source node is the head of the update path. + # If the source node is responsible for the cost update, + # then clear the history and use it instead. + if v in pred_edge and pred_edge[v] == u: + recent_update[v] = recent_update[u] + else: + recent_update[v] = (u, v) + + if v not in in_q: + q.append(v) + in_q.add(v) + count_v = count.get(v, 0) + 1 + if count_v == n: + # Negative cycle found! + return v + + count[v] = count_v + dist[v] = dist_v + pred[v] = [u] + pred_edge[v] = u + + elif dist.get(v) is not None and dist_v == dist.get(v): + pred[v].append(u) + + # successfully found shortest_path. No negative cycles found. + return None + + +@nx._dispatchable(edge_attrs="weight") +def bellman_ford_path(G, source, target, weight="weight"): + """Returns the shortest path from source to target in a weighted graph G. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node + + target : node + Ending node + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + path : list + List of nodes in a shortest path. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.bellman_ford_path(G, 0, 4) + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + dijkstra_path, bellman_ford_path_length + """ + length, path = single_source_bellman_ford(G, source, target=target, weight=weight) + return path + + +@nx._dispatchable(edge_attrs="weight") +def bellman_ford_path_length(G, source, target, weight="weight"): + """Returns the shortest path length from source to target + in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node label + starting node for path + + target : node label + ending node for path + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + length : number + Shortest path length. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.bellman_ford_path_length(G, 0, 4) + 4 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + dijkstra_path_length, bellman_ford_path + """ + if source == target: + if source not in G: + raise nx.NodeNotFound(f"Node {source} not found in graph") + return 0 + + weight = _weight_function(G, weight) + + length = _bellman_ford(G, [source], weight, target=target) + + try: + return length[target] + except KeyError as err: + raise nx.NetworkXNoPath(f"node {target} not reachable from {source}") from err + + +@nx._dispatchable(edge_attrs="weight") +def single_source_bellman_ford_path(G, source, weight="weight"): + """Compute shortest path between source and all other reachable + nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path. + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + paths : dictionary + Dictionary of shortest path lengths keyed by target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = nx.single_source_bellman_ford_path(G, 0) + >>> path[4] + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + single_source_dijkstra, single_source_bellman_ford + + """ + (length, path) = single_source_bellman_ford(G, source, weight=weight) + return path + + +@nx._dispatchable(edge_attrs="weight") +def single_source_bellman_ford_path_length(G, source, weight="weight"): + """Compute the shortest path length between source and all other + reachable nodes for a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + length : dictionary + Dictionary of shortest path length keyed by target + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = nx.single_source_bellman_ford_path_length(G, 0) + >>> length[4] + 4 + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 3 + 4: 4 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + single_source_dijkstra, single_source_bellman_ford + + """ + weight = _weight_function(G, weight) + return _bellman_ford(G, [source], weight) + + +@nx._dispatchable(edge_attrs="weight") +def single_source_bellman_ford(G, source, target=None, weight="weight"): + """Compute shortest paths and lengths in a weighted graph G. + + Uses Bellman-Ford algorithm for shortest paths. + + Parameters + ---------- + G : NetworkX graph + + source : node label + Starting node for path + + target : node label, optional + Ending node for path + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + distance, path : pair of dictionaries, or numeric and list + If target is None, returns a tuple of two dictionaries keyed by node. + The first dictionary stores distance from one of the source nodes. + The second stores the path from one of the sources to that node. + If target is not None, returns a tuple of (distance, path) where + distance is the distance from source to target and path is a list + representing the path from source to target. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length, path = nx.single_source_bellman_ford(G, 0) + >>> length[4] + 4 + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"{node}: {length[node]}") + 0: 0 + 1: 1 + 2: 2 + 3: 3 + 4: 4 + >>> path[4] + [0, 1, 2, 3, 4] + >>> length, path = nx.single_source_bellman_ford(G, 0, 1) + >>> length + 1 + >>> path + [0, 1] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + single_source_dijkstra + single_source_bellman_ford_path + single_source_bellman_ford_path_length + """ + if source == target: + if source not in G: + raise nx.NodeNotFound(f"Node {source} is not found in the graph") + return (0, [source]) + + weight = _weight_function(G, weight) + + paths = {source: [source]} # dictionary of paths + dist = _bellman_ford(G, [source], weight, paths=paths, target=target) + if target is None: + return (dist, paths) + try: + return (dist[target], paths[target]) + except KeyError as err: + msg = f"Node {target} not reachable from {source}" + raise nx.NetworkXNoPath(msg) from err + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_bellman_ford_path_length(G, weight="weight"): + """Compute shortest path lengths between all nodes in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + distance : iterator + (source, dictionary) iterator with dictionary keyed by target and + shortest path length as the key value. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length = dict(nx.all_pairs_bellman_ford_path_length(G)) + >>> for node in [0, 1, 2, 3, 4]: + ... print(f"1 - {node}: {length[1][node]}") + 1 - 0: 1 + 1 - 1: 0 + 1 - 2: 1 + 1 - 3: 2 + 1 - 4: 3 + >>> length[3][2] + 1 + >>> length[2][2] + 0 + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The dictionary returned only has keys for reachable node pairs. + """ + length = single_source_bellman_ford_path_length + for n in G: + yield (n, dict(length(G, n, weight=weight))) + + +@nx._dispatchable(edge_attrs="weight") +def all_pairs_bellman_ford_path(G, weight="weight"): + """Compute shortest paths between all nodes in a weighted graph. + + Parameters + ---------- + G : NetworkX graph + + weight : string or function (default="weight") + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + paths : iterator + (source, dictionary) iterator with dictionary keyed by target and + shortest path as the key value. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> path = dict(nx.all_pairs_bellman_ford_path(G)) + >>> path[0][4] + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + See Also + -------- + floyd_warshall, all_pairs_dijkstra_path + + """ + path = single_source_bellman_ford_path + for n in G: + yield (n, path(G, n, weight=weight)) + + +@nx._dispatchable(edge_attrs="weight") +def goldberg_radzik(G, source, weight="weight"): + """Compute shortest path lengths and predecessors on shortest paths + in weighted graphs. + + The algorithm has a running time of $O(mn)$ where $n$ is the number of + nodes and $m$ is the number of edges. It is slower than Dijkstra but + can handle negative edge weights. + + Parameters + ---------- + G : NetworkX graph + The algorithm works for all types of graphs, including directed + graphs and multigraphs. + + source: node label + Starting node for path + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + pred, dist : dictionaries + Returns two dictionaries keyed by node to predecessor in the + path and to the distance from the source respectively. + + Raises + ------ + NodeNotFound + If `source` is not in `G`. + + NetworkXUnbounded + If the (di)graph contains a negative (di)cycle, the + algorithm raises an exception to indicate the presence of the + negative (di)cycle. Note: any negative weight edge in an + undirected graph is a negative cycle. + + As of NetworkX v3.2, a zero weight cycle is no longer + incorrectly reported as a negative weight cycle. + + + Examples + -------- + >>> G = nx.path_graph(5, create_using=nx.DiGraph()) + >>> pred, dist = nx.goldberg_radzik(G, 0) + >>> sorted(pred.items()) + [(0, None), (1, 0), (2, 1), (3, 2), (4, 3)] + >>> sorted(dist.items()) + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)] + + >>> G = nx.cycle_graph(5, create_using=nx.DiGraph()) + >>> G[1][2]["weight"] = -7 + >>> nx.goldberg_radzik(G, 0) + Traceback (most recent call last): + ... + networkx.exception.NetworkXUnbounded: Negative cycle detected. + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The dictionaries returned only have keys for nodes reachable from + the source. + + In the case where the (di)graph is not connected, if a component + not containing the source contains a negative (di)cycle, it + will not be detected. + + """ + if source not in G: + raise nx.NodeNotFound(f"Node {source} is not found in the graph") + weight = _weight_function(G, weight) + if G.is_multigraph(): + if any( + weight(u, v, {k: d}) < 0 + for u, v, k, d in nx.selfloop_edges(G, keys=True, data=True) + ): + raise nx.NetworkXUnbounded("Negative cycle detected.") + else: + if any(weight(u, v, d) < 0 for u, v, d in nx.selfloop_edges(G, data=True)): + raise nx.NetworkXUnbounded("Negative cycle detected.") + + if len(G) == 1: + return {source: None}, {source: 0} + + G_succ = G._adj # For speed-up (and works for both directed and undirected graphs) + + inf = float("inf") + d = {u: inf for u in G} + d[source] = 0 + pred = {source: None} + + def topo_sort(relabeled): + """Topologically sort nodes relabeled in the previous round and detect + negative cycles. + """ + # List of nodes to scan in this round. Denoted by A in Goldberg and + # Radzik's paper. + to_scan = [] + # In the DFS in the loop below, neg_count records for each node the + # number of edges of negative reduced costs on the path from a DFS root + # to the node in the DFS forest. The reduced cost of an edge (u, v) is + # defined as d[u] + weight[u][v] - d[v]. + # + # neg_count also doubles as the DFS visit marker array. + neg_count = {} + for u in relabeled - neg_count.keys(): + d_u = d[u] + # Skip nodes without out-edges of negative reduced costs. + if all(d_u + weight(u, v, e) >= d[v] for v, e in G_succ[u].items()): + continue + # Nonrecursive DFS that inserts nodes reachable from u via edges of + # nonpositive reduced costs into to_scan in (reverse) topological + # order. + stack = [(u, iter(G_succ[u].items()))] + in_stack = {u} + neg_count[u] = 0 + while stack: + u, it = stack[-1] + try: + v, e = next(it) + except StopIteration: + to_scan.append(u) + stack.pop() + in_stack.remove(u) + continue + t = d[u] + weight(u, v, e) + d_v = d[v] + if t < d_v: + d[v] = t + pred[v] = u + if v not in neg_count: + neg_count[v] = neg_count[u] + 1 + stack.append((v, iter(G_succ[v].items()))) + in_stack.add(v) + elif v in in_stack and neg_count[u] + 1 > neg_count[v]: + # (u, v) is a back edge, and the cycle formed by the + # path v to u and (u, v) contains at least one edge of + # negative reduced cost. The cycle must be of negative + # cost. + raise nx.NetworkXUnbounded("Negative cycle detected.") + to_scan.reverse() + return to_scan + + def relax(to_scan): + """Relax out-edges of relabeled nodes.""" + relabeled = set() + # Scan nodes in to_scan in topological order and relax incident + # out-edges. Add the relabeled nodes to labeled. + for u in to_scan: + d_u = d[u] + for v, e in G_succ[u].items(): + w_e = weight(u, v, e) + if d_u + w_e < d[v]: + d[v] = d_u + w_e + pred[v] = u + relabeled.add(v) + return relabeled + + # Set of nodes relabeled in the last round of scan operations. Denoted by B + # in Goldberg and Radzik's paper. + relabeled = {source} + + while relabeled: + to_scan = topo_sort(relabeled) + relabeled = relax(to_scan) + + d = {u: d[u] for u in pred} + return pred, d + + +@nx._dispatchable(edge_attrs="weight") +def negative_edge_cycle(G, weight="weight", heuristic=True): + """Returns True if there exists a negative edge cycle anywhere in G. + + Parameters + ---------- + G : NetworkX graph + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + heuristic : bool + Determines whether to use a heuristic to early detect negative + cycles at a negligible cost. In case of graphs with a negative cycle, + the performance of detection increases by at least an order of magnitude. + + Returns + ------- + negative_cycle : bool + True if a negative edge cycle exists, otherwise False. + + Examples + -------- + >>> G = nx.cycle_graph(5, create_using=nx.DiGraph()) + >>> print(nx.negative_edge_cycle(G)) + False + >>> G[1][2]["weight"] = -7 + >>> print(nx.negative_edge_cycle(G)) + True + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + This algorithm uses bellman_ford_predecessor_and_distance() but finds + negative cycles on any component by first adding a new node connected to + every node, and starting bellman_ford_predecessor_and_distance on that + node. It then removes that extra node. + """ + if G.size() == 0: + return False + + # find unused node to use temporarily + newnode = -1 + while newnode in G: + newnode -= 1 + # connect it to all nodes + G.add_edges_from([(newnode, n) for n in G]) + + try: + bellman_ford_predecessor_and_distance( + G, newnode, weight=weight, heuristic=heuristic + ) + except nx.NetworkXUnbounded: + return True + finally: + G.remove_node(newnode) + return False + + +@nx._dispatchable(edge_attrs="weight") +def find_negative_cycle(G, source, weight="weight"): + """Returns a cycle with negative total weight if it exists. + + Bellman-Ford is used to find shortest_paths. That algorithm + stops if there exists a negative cycle. This algorithm + picks up from there and returns the found negative cycle. + + The cycle consists of a list of nodes in the cycle order. The last + node equals the first to make it a cycle. + You can look up the edge weights in the original graph. In the case + of multigraphs the relevant edge is the minimal weight edge between + the nodes in the 2-tuple. + + If the graph has no negative cycle, a NetworkXError is raised. + + Parameters + ---------- + G : NetworkX graph + + source: node label + The search for the negative cycle will start from this node. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_weighted_edges_from( + ... [(0, 1, 2), (1, 2, 2), (2, 0, 1), (1, 4, 2), (4, 0, -5)] + ... ) + >>> nx.find_negative_cycle(G, 0) + [4, 0, 1, 4] + + Returns + ------- + cycle : list + A list of nodes in the order of the cycle found. The last node + equals the first to indicate a cycle. + + Raises + ------ + NetworkXError + If no negative cycle is found. + """ + weight = _weight_function(G, weight) + pred = {source: []} + + v = _inner_bellman_ford(G, [source], weight, pred=pred) + if v is None: + raise nx.NetworkXError("No negative cycles detected.") + + # negative cycle detected... find it + neg_cycle = [] + stack = [(v, list(pred[v]))] + seen = {v} + while stack: + node, preds = stack[-1] + if v in preds: + # found the cycle + neg_cycle.extend([node, v]) + neg_cycle = list(reversed(neg_cycle)) + return neg_cycle + + if preds: + nbr = preds.pop() + if nbr not in seen: + stack.append((nbr, list(pred[nbr]))) + neg_cycle.append(node) + seen.add(nbr) + else: + stack.pop() + if neg_cycle: + neg_cycle.pop() + else: + if v in G[v] and weight(G, v, v) < 0: + return [v, v] + # should not reach here + raise nx.NetworkXError("Negative cycle is detected but not found") + # should not get here... + msg = "negative cycle detected but not identified" + raise nx.NetworkXUnbounded(msg) + + +@nx._dispatchable(edge_attrs="weight") +def bidirectional_dijkstra(G, source, target, weight="weight"): + r"""Dijkstra's algorithm for shortest paths using bidirectional search. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node. + + target : node + Ending node. + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + Returns + ------- + length, path : number and list + length is the distance from source to target. + path is a list of nodes on a path from source to target. + + Raises + ------ + NodeNotFound + If `source` or `target` is not in `G`. + + NetworkXNoPath + If no path exists between source and target. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> length, path = nx.bidirectional_dijkstra(G, 0, 4) + >>> print(length) + 4 + >>> print(path) + [0, 1, 2, 3, 4] + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + In practice bidirectional Dijkstra is much more than twice as fast as + ordinary Dijkstra. + + Ordinary Dijkstra expands nodes in a sphere-like manner from the + source. The radius of this sphere will eventually be the length + of the shortest path. Bidirectional Dijkstra will expand nodes + from both the source and the target, making two spheres of half + this radius. Volume of the first sphere is `\pi*r*r` while the + others are `2*\pi*r/2*r/2`, making up half the volume. + + This algorithm is not guaranteed to work if edge weights + are negative or are floating point numbers + (overflows and roundoff errors can cause problems). + + See Also + -------- + shortest_path + shortest_path_length + """ + if source not in G: + raise nx.NodeNotFound(f"Source {source} is not in G") + + if target not in G: + raise nx.NodeNotFound(f"Target {target} is not in G") + + if source == target: + return (0, [source]) + + weight = _weight_function(G, weight) + # Init: [Forward, Backward] + dists = [{}, {}] # dictionary of final distances + preds = [{source: None}, {target: None}] # dictionary of preds + + def path(curr, direction): + ret = [] + while curr is not None: + ret.append(curr) + curr = preds[direction][curr] + return list(reversed(ret)) if direction == 0 else ret + + fringe = [[], []] # heap of (distance, node) for choosing node to expand + seen = [{source: 0}, {target: 0}] # dict of distances to seen nodes + c = count() + # initialize fringe heap + heappush(fringe[0], (0, next(c), source)) + heappush(fringe[1], (0, next(c), target)) + # neighbors for extracting correct neighbor information + if G.is_directed(): + neighbors = [G._succ, G._pred] + else: + neighbors = [G._adj, G._adj] + # variables to hold shortest discovered path + finaldist = None + meetnode = None + direction = 1 + while fringe[0] and fringe[1]: + # choose direction + # direction == 0 is forward direction and direction == 1 is back + direction = 1 - direction + # extract closest to expand + (dist, _, v) = heappop(fringe[direction]) + if v in dists[direction]: + # Shortest path to v has already been found + continue + # update distance + dists[direction][v] = dist # equal to seen[direction][v] + if v in dists[1 - direction]: + # if we have scanned v in both directions we are done + # we have now discovered the shortest path + return (finaldist, path(meetnode, 0) + path(preds[1][meetnode], 1)) + + for w, d in neighbors[direction][v].items(): + # weight(v, w, d) for forward and weight(w, v, d) for back direction + cost = weight(v, w, d) if direction == 0 else weight(w, v, d) + if cost is None: + continue + vwLength = dist + cost + if w in dists[direction]: + if vwLength < dists[direction][w]: + raise ValueError("Contradictory paths found: negative weights?") + elif w not in seen[direction] or vwLength < seen[direction][w]: + # relaxing + seen[direction][w] = vwLength + heappush(fringe[direction], (vwLength, next(c), w)) + preds[direction][w] = v + if w in seen[1 - direction]: + # see if this path is better than the already + # discovered shortest path + finaldist_w = vwLength + seen[1 - direction][w] + if finaldist is None or finaldist > finaldist_w: + finaldist, meetnode = finaldist_w, w + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") + + +@nx._dispatchable(edge_attrs="weight") +def johnson(G, weight="weight"): + r"""Uses Johnson's Algorithm to compute shortest paths. + + Johnson's Algorithm finds a shortest path between each pair of + nodes in a weighted graph even if negative weights are present. + + Parameters + ---------- + G : NetworkX graph + + weight : string or function + If this is a string, then edge weights will be accessed via the + edge attribute with this key (that is, the weight of the edge + joining `u` to `v` will be ``G.edges[u, v][weight]``). If no + such edge attribute exists, the weight of the edge is assumed to + be one. + + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number. + + Returns + ------- + distance : dictionary + Dictionary, keyed by source and target, of shortest paths. + + Examples + -------- + >>> graph = nx.DiGraph() + >>> graph.add_weighted_edges_from( + ... [("0", "3", 3), ("0", "1", -5), ("0", "2", 2), ("1", "2", 4), ("2", "3", 1)] + ... ) + >>> paths = nx.johnson(graph, weight="weight") + >>> paths["0"]["2"] + ['0', '1', '2'] + + Notes + ----- + Johnson's algorithm is suitable even for graphs with negative weights. It + works by using the Bellman–Ford algorithm to compute a transformation of + the input graph that removes all negative weights, allowing Dijkstra's + algorithm to be used on the transformed graph. + + The time complexity of this algorithm is $O(n^2 \log n + n m)$, + where $n$ is the number of nodes and $m$ the number of edges in the + graph. For dense graphs, this may be faster than the Floyd–Warshall + algorithm. + + See Also + -------- + floyd_warshall_predecessor_and_distance + floyd_warshall_numpy + all_pairs_shortest_path + all_pairs_shortest_path_length + all_pairs_dijkstra_path + bellman_ford_predecessor_and_distance + all_pairs_bellman_ford_path + all_pairs_bellman_ford_path_length + + """ + dist = {v: 0 for v in G} + pred = {v: [] for v in G} + weight = _weight_function(G, weight) + + # Calculate distance of shortest paths + dist_bellman = _bellman_ford(G, list(G), weight, pred=pred, dist=dist) + + # Update the weight function to take into account the Bellman--Ford + # relaxation distances. + def new_weight(u, v, d): + return weight(u, v, d) + dist_bellman[u] - dist_bellman[v] + + def dist_path(v): + paths = {v: [v]} + _dijkstra(G, v, new_weight, paths=paths) + return paths + + return {v: dist_path(v) for v in G} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/similarity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/similarity.py new file mode 100644 index 0000000000000000000000000000000000000000..25aada68d0a6c2c80545c8db54ceb3e775def44c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/similarity.py @@ -0,0 +1,2107 @@ +"""Functions measuring similarity using graph edit distance. + +The graph edit distance is the number of edge/node changes needed +to make two graphs isomorphic. + +The default algorithm/implementation is sub-optimal for some graphs. +The problem of finding the exact Graph Edit Distance (GED) is NP-hard +so it is often slow. If the simple interface `graph_edit_distance` +takes too long for your graph, try `optimize_graph_edit_distance` +and/or `optimize_edit_paths`. + +At the same time, I encourage capable people to investigate +alternative GED algorithms, in order to improve the choices available. +""" + +import math +import time +from dataclasses import dataclass +from itertools import product + +import networkx as nx +from networkx.utils import np_random_state + +__all__ = [ + "graph_edit_distance", + "optimal_edit_paths", + "optimize_graph_edit_distance", + "optimize_edit_paths", + "simrank_similarity", + "panther_similarity", + "panther_vector_similarity", + "generate_random_paths", +] + + +@nx._dispatchable( + graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True +) +def graph_edit_distance( + G1, + G2, + node_match=None, + edge_match=None, + node_subst_cost=None, + node_del_cost=None, + node_ins_cost=None, + edge_subst_cost=None, + edge_del_cost=None, + edge_ins_cost=None, + roots=None, + upper_bound=None, + timeout=None, +): + """Returns GED (graph edit distance) between graphs G1 and G2. + + Graph edit distance is a graph similarity measure analogous to + Levenshtein distance for strings. It is defined as minimum cost + of edit path (sequence of node and edge edit operations) + transforming graph G1 to graph isomorphic to G2. + + Parameters + ---------- + G1, G2: graphs + The two graphs G1 and G2 must be of the same type. + + node_match : callable + A function that returns True if node n1 in G1 and n2 in G2 + should be considered equal during matching. + + The function will be called like + + node_match(G1.nodes[n1], G2.nodes[n2]). + + That is, the function will receive the node attribute + dictionaries for n1 and n2 as inputs. + + Ignored if node_subst_cost is specified. If neither + node_match nor node_subst_cost are specified then node + attributes are not considered. + + edge_match : callable + A function that returns True if the edge attribute dictionaries + for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should + be considered equal during matching. + + The function will be called like + + edge_match(G1[u1][v1], G2[u2][v2]). + + That is, the function will receive the edge attribute + dictionaries of the edges under consideration. + + Ignored if edge_subst_cost is specified. If neither + edge_match nor edge_subst_cost are specified then edge + attributes are not considered. + + node_subst_cost, node_del_cost, node_ins_cost : callable + Functions that return the costs of node substitution, node + deletion, and node insertion, respectively. + + The functions will be called like + + node_subst_cost(G1.nodes[n1], G2.nodes[n2]), + node_del_cost(G1.nodes[n1]), + node_ins_cost(G2.nodes[n2]). + + That is, the functions will receive the node attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function node_subst_cost overrides node_match if specified. + If neither node_match nor node_subst_cost are specified then + default node substitution cost of 0 is used (node attributes + are not considered during matching). + + If node_del_cost is not specified then default node deletion + cost of 1 is used. If node_ins_cost is not specified then + default node insertion cost of 1 is used. + + edge_subst_cost, edge_del_cost, edge_ins_cost : callable + Functions that return the costs of edge substitution, edge + deletion, and edge insertion, respectively. + + The functions will be called like + + edge_subst_cost(G1[u1][v1], G2[u2][v2]), + edge_del_cost(G1[u1][v1]), + edge_ins_cost(G2[u2][v2]). + + That is, the functions will receive the edge attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function edge_subst_cost overrides edge_match if specified. + If neither edge_match nor edge_subst_cost are specified then + default edge substitution cost of 0 is used (edge attributes + are not considered during matching). + + If edge_del_cost is not specified then default edge deletion + cost of 1 is used. If edge_ins_cost is not specified then + default edge insertion cost of 1 is used. + + roots : 2-tuple + Tuple where first element is a node in G1 and the second + is a node in G2. + These nodes are forced to be matched in the comparison to + allow comparison between rooted graphs. + + upper_bound : numeric + Maximum edit distance to consider. Return None if no edit + distance under or equal to upper_bound exists. + + timeout : numeric + Maximum number of seconds to execute. + After timeout is met, the current best GED is returned. + + Examples + -------- + >>> G1 = nx.cycle_graph(6) + >>> G2 = nx.wheel_graph(7) + >>> nx.graph_edit_distance(G1, G2) + 7.0 + + >>> G1 = nx.star_graph(5) + >>> G2 = nx.star_graph(5) + >>> nx.graph_edit_distance(G1, G2, roots=(0, 0)) + 0.0 + >>> nx.graph_edit_distance(G1, G2, roots=(1, 0)) + 8.0 + + See Also + -------- + optimal_edit_paths, optimize_graph_edit_distance, + + is_isomorphic: test for graph edit distance of 0 + + References + ---------- + .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick + Martineau. An Exact Graph Edit Distance Algorithm for Solving + Pattern Recognition Problems. 4th International Conference on + Pattern Recognition Applications and Methods 2015, Jan 2015, + Lisbon, Portugal. 2015, + <10.5220/0005209202710278>. + https://hal.archives-ouvertes.fr/hal-01168816 + + """ + bestcost = None + for _, _, cost in optimize_edit_paths( + G1, + G2, + node_match, + edge_match, + node_subst_cost, + node_del_cost, + node_ins_cost, + edge_subst_cost, + edge_del_cost, + edge_ins_cost, + upper_bound, + True, + roots, + timeout, + ): + # assert bestcost is None or cost < bestcost + bestcost = cost + return bestcost + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}) +def optimal_edit_paths( + G1, + G2, + node_match=None, + edge_match=None, + node_subst_cost=None, + node_del_cost=None, + node_ins_cost=None, + edge_subst_cost=None, + edge_del_cost=None, + edge_ins_cost=None, + upper_bound=None, +): + """Returns all minimum-cost edit paths transforming G1 to G2. + + Graph edit path is a sequence of node and edge edit operations + transforming graph G1 to graph isomorphic to G2. Edit operations + include substitutions, deletions, and insertions. + + Parameters + ---------- + G1, G2: graphs + The two graphs G1 and G2 must be of the same type. + + node_match : callable + A function that returns True if node n1 in G1 and n2 in G2 + should be considered equal during matching. + + The function will be called like + + node_match(G1.nodes[n1], G2.nodes[n2]). + + That is, the function will receive the node attribute + dictionaries for n1 and n2 as inputs. + + Ignored if node_subst_cost is specified. If neither + node_match nor node_subst_cost are specified then node + attributes are not considered. + + edge_match : callable + A function that returns True if the edge attribute dictionaries + for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should + be considered equal during matching. + + The function will be called like + + edge_match(G1[u1][v1], G2[u2][v2]). + + That is, the function will receive the edge attribute + dictionaries of the edges under consideration. + + Ignored if edge_subst_cost is specified. If neither + edge_match nor edge_subst_cost are specified then edge + attributes are not considered. + + node_subst_cost, node_del_cost, node_ins_cost : callable + Functions that return the costs of node substitution, node + deletion, and node insertion, respectively. + + The functions will be called like + + node_subst_cost(G1.nodes[n1], G2.nodes[n2]), + node_del_cost(G1.nodes[n1]), + node_ins_cost(G2.nodes[n2]). + + That is, the functions will receive the node attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function node_subst_cost overrides node_match if specified. + If neither node_match nor node_subst_cost are specified then + default node substitution cost of 0 is used (node attributes + are not considered during matching). + + If node_del_cost is not specified then default node deletion + cost of 1 is used. If node_ins_cost is not specified then + default node insertion cost of 1 is used. + + edge_subst_cost, edge_del_cost, edge_ins_cost : callable + Functions that return the costs of edge substitution, edge + deletion, and edge insertion, respectively. + + The functions will be called like + + edge_subst_cost(G1[u1][v1], G2[u2][v2]), + edge_del_cost(G1[u1][v1]), + edge_ins_cost(G2[u2][v2]). + + That is, the functions will receive the edge attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function edge_subst_cost overrides edge_match if specified. + If neither edge_match nor edge_subst_cost are specified then + default edge substitution cost of 0 is used (edge attributes + are not considered during matching). + + If edge_del_cost is not specified then default edge deletion + cost of 1 is used. If edge_ins_cost is not specified then + default edge insertion cost of 1 is used. + + upper_bound : numeric + Maximum edit distance to consider. + + Returns + ------- + edit_paths : list of tuples (node_edit_path, edge_edit_path) + - node_edit_path : list of tuples ``(u, v)`` indicating node transformations + between `G1` and `G2`. ``u`` is `None` for insertion, ``v`` is `None` + for deletion. + - edge_edit_path : list of tuples ``((u1, v1), (u2, v2))`` indicating edge + transformations between `G1` and `G2`. ``(None, (u2,v2))`` for insertion + and ``((u1,v1), None)`` for deletion. + + cost : numeric + Optimal edit path cost (graph edit distance). When the cost + is zero, it indicates that `G1` and `G2` are isomorphic. + + Examples + -------- + >>> G1 = nx.cycle_graph(4) + >>> G2 = nx.wheel_graph(5) + >>> paths, cost = nx.optimal_edit_paths(G1, G2) + >>> len(paths) + 40 + >>> cost + 5.0 + + Notes + ----- + To transform `G1` into a graph isomorphic to `G2`, apply the node + and edge edits in the returned ``edit_paths``. + In the case of isomorphic graphs, the cost is zero, and the paths + represent different isomorphic mappings (isomorphisms). That is, the + edits involve renaming nodes and edges to match the structure of `G2`. + + See Also + -------- + graph_edit_distance, optimize_edit_paths + + References + ---------- + .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick + Martineau. An Exact Graph Edit Distance Algorithm for Solving + Pattern Recognition Problems. 4th International Conference on + Pattern Recognition Applications and Methods 2015, Jan 2015, + Lisbon, Portugal. 2015, + <10.5220/0005209202710278>. + https://hal.archives-ouvertes.fr/hal-01168816 + + """ + paths = [] + bestcost = None + for vertex_path, edge_path, cost in optimize_edit_paths( + G1, + G2, + node_match, + edge_match, + node_subst_cost, + node_del_cost, + node_ins_cost, + edge_subst_cost, + edge_del_cost, + edge_ins_cost, + upper_bound, + False, + ): + # assert bestcost is None or cost <= bestcost + if bestcost is not None and cost < bestcost: + paths = [] + paths.append((vertex_path, edge_path)) + bestcost = cost + return paths, bestcost + + +@nx._dispatchable(graphs={"G1": 0, "G2": 1}) +def optimize_graph_edit_distance( + G1, + G2, + node_match=None, + edge_match=None, + node_subst_cost=None, + node_del_cost=None, + node_ins_cost=None, + edge_subst_cost=None, + edge_del_cost=None, + edge_ins_cost=None, + upper_bound=None, +): + """Returns consecutive approximations of GED (graph edit distance) + between graphs G1 and G2. + + Graph edit distance is a graph similarity measure analogous to + Levenshtein distance for strings. It is defined as minimum cost + of edit path (sequence of node and edge edit operations) + transforming graph G1 to graph isomorphic to G2. + + Parameters + ---------- + G1, G2: graphs + The two graphs G1 and G2 must be of the same type. + + node_match : callable + A function that returns True if node n1 in G1 and n2 in G2 + should be considered equal during matching. + + The function will be called like + + node_match(G1.nodes[n1], G2.nodes[n2]). + + That is, the function will receive the node attribute + dictionaries for n1 and n2 as inputs. + + Ignored if node_subst_cost is specified. If neither + node_match nor node_subst_cost are specified then node + attributes are not considered. + + edge_match : callable + A function that returns True if the edge attribute dictionaries + for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should + be considered equal during matching. + + The function will be called like + + edge_match(G1[u1][v1], G2[u2][v2]). + + That is, the function will receive the edge attribute + dictionaries of the edges under consideration. + + Ignored if edge_subst_cost is specified. If neither + edge_match nor edge_subst_cost are specified then edge + attributes are not considered. + + node_subst_cost, node_del_cost, node_ins_cost : callable + Functions that return the costs of node substitution, node + deletion, and node insertion, respectively. + + The functions will be called like + + node_subst_cost(G1.nodes[n1], G2.nodes[n2]), + node_del_cost(G1.nodes[n1]), + node_ins_cost(G2.nodes[n2]). + + That is, the functions will receive the node attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function node_subst_cost overrides node_match if specified. + If neither node_match nor node_subst_cost are specified then + default node substitution cost of 0 is used (node attributes + are not considered during matching). + + If node_del_cost is not specified then default node deletion + cost of 1 is used. If node_ins_cost is not specified then + default node insertion cost of 1 is used. + + edge_subst_cost, edge_del_cost, edge_ins_cost : callable + Functions that return the costs of edge substitution, edge + deletion, and edge insertion, respectively. + + The functions will be called like + + edge_subst_cost(G1[u1][v1], G2[u2][v2]), + edge_del_cost(G1[u1][v1]), + edge_ins_cost(G2[u2][v2]). + + That is, the functions will receive the edge attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function edge_subst_cost overrides edge_match if specified. + If neither edge_match nor edge_subst_cost are specified then + default edge substitution cost of 0 is used (edge attributes + are not considered during matching). + + If edge_del_cost is not specified then default edge deletion + cost of 1 is used. If edge_ins_cost is not specified then + default edge insertion cost of 1 is used. + + upper_bound : numeric + Maximum edit distance to consider. + + Returns + ------- + Generator of consecutive approximations of graph edit distance. + + Examples + -------- + >>> G1 = nx.cycle_graph(6) + >>> G2 = nx.wheel_graph(7) + >>> for v in nx.optimize_graph_edit_distance(G1, G2): + ... minv = v + >>> minv + 7.0 + + See Also + -------- + graph_edit_distance, optimize_edit_paths + + References + ---------- + .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick + Martineau. An Exact Graph Edit Distance Algorithm for Solving + Pattern Recognition Problems. 4th International Conference on + Pattern Recognition Applications and Methods 2015, Jan 2015, + Lisbon, Portugal. 2015, + <10.5220/0005209202710278>. + https://hal.archives-ouvertes.fr/hal-01168816 + """ + for _, _, cost in optimize_edit_paths( + G1, + G2, + node_match, + edge_match, + node_subst_cost, + node_del_cost, + node_ins_cost, + edge_subst_cost, + edge_del_cost, + edge_ins_cost, + upper_bound, + True, + ): + yield cost + + +@nx._dispatchable( + graphs={"G1": 0, "G2": 1}, preserve_edge_attrs=True, preserve_node_attrs=True +) +def optimize_edit_paths( + G1, + G2, + node_match=None, + edge_match=None, + node_subst_cost=None, + node_del_cost=None, + node_ins_cost=None, + edge_subst_cost=None, + edge_del_cost=None, + edge_ins_cost=None, + upper_bound=None, + strictly_decreasing=True, + roots=None, + timeout=None, +): + """GED (graph edit distance) calculation: advanced interface. + + Graph edit path is a sequence of node and edge edit operations + transforming graph G1 to graph isomorphic to G2. Edit operations + include substitutions, deletions, and insertions. + + Graph edit distance is defined as minimum cost of edit path. + + Parameters + ---------- + G1, G2: graphs + The two graphs G1 and G2 must be of the same type. + + node_match : callable + A function that returns True if node n1 in G1 and n2 in G2 + should be considered equal during matching. + + The function will be called like + + node_match(G1.nodes[n1], G2.nodes[n2]). + + That is, the function will receive the node attribute + dictionaries for n1 and n2 as inputs. + + Ignored if node_subst_cost is specified. If neither + node_match nor node_subst_cost are specified then node + attributes are not considered. + + edge_match : callable + A function that returns True if the edge attribute dictionaries + for the pair of nodes (u1, v1) in G1 and (u2, v2) in G2 should + be considered equal during matching. + + The function will be called like + + edge_match(G1[u1][v1], G2[u2][v2]). + + That is, the function will receive the edge attribute + dictionaries of the edges under consideration. + + Ignored if edge_subst_cost is specified. If neither + edge_match nor edge_subst_cost are specified then edge + attributes are not considered. + + node_subst_cost, node_del_cost, node_ins_cost : callable + Functions that return the costs of node substitution, node + deletion, and node insertion, respectively. + + The functions will be called like + + node_subst_cost(G1.nodes[n1], G2.nodes[n2]), + node_del_cost(G1.nodes[n1]), + node_ins_cost(G2.nodes[n2]). + + That is, the functions will receive the node attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function node_subst_cost overrides node_match if specified. + If neither node_match nor node_subst_cost are specified then + default node substitution cost of 0 is used (node attributes + are not considered during matching). + + If node_del_cost is not specified then default node deletion + cost of 1 is used. If node_ins_cost is not specified then + default node insertion cost of 1 is used. + + edge_subst_cost, edge_del_cost, edge_ins_cost : callable + Functions that return the costs of edge substitution, edge + deletion, and edge insertion, respectively. + + The functions will be called like + + edge_subst_cost(G1[u1][v1], G2[u2][v2]), + edge_del_cost(G1[u1][v1]), + edge_ins_cost(G2[u2][v2]). + + That is, the functions will receive the edge attribute + dictionaries as inputs. The functions are expected to return + positive numeric values. + + Function edge_subst_cost overrides edge_match if specified. + If neither edge_match nor edge_subst_cost are specified then + default edge substitution cost of 0 is used (edge attributes + are not considered during matching). + + If edge_del_cost is not specified then default edge deletion + cost of 1 is used. If edge_ins_cost is not specified then + default edge insertion cost of 1 is used. + + upper_bound : numeric + Maximum edit distance to consider. + + strictly_decreasing : bool + If True, return consecutive approximations of strictly + decreasing cost. Otherwise, return all edit paths of cost + less than or equal to the previous minimum cost. + + roots : 2-tuple + Tuple where first element is a node in G1 and the second + is a node in G2. + These nodes are forced to be matched in the comparison to + allow comparison between rooted graphs. + + timeout : numeric + Maximum number of seconds to execute. + After timeout is met, the current best GED is returned. + + Returns + ------- + Generator of tuples (node_edit_path, edge_edit_path, cost) + node_edit_path : list of tuples (u, v) + edge_edit_path : list of tuples ((u1, v1), (u2, v2)) + cost : numeric + + See Also + -------- + graph_edit_distance, optimize_graph_edit_distance, optimal_edit_paths + + References + ---------- + .. [1] Zeina Abu-Aisheh, Romain Raveaux, Jean-Yves Ramel, Patrick + Martineau. An Exact Graph Edit Distance Algorithm for Solving + Pattern Recognition Problems. 4th International Conference on + Pattern Recognition Applications and Methods 2015, Jan 2015, + Lisbon, Portugal. 2015, + <10.5220/0005209202710278>. + https://hal.archives-ouvertes.fr/hal-01168816 + + """ + # TODO: support DiGraph + + import numpy as np + import scipy as sp + + @dataclass + class CostMatrix: + C: ... + lsa_row_ind: ... + lsa_col_ind: ... + ls: ... + + def make_CostMatrix(C, m, n): + # assert(C.shape == (m + n, m + n)) + lsa_row_ind, lsa_col_ind = sp.optimize.linear_sum_assignment(C) + + # Fixup dummy assignments: + # each substitution i<->j should have dummy assignment m+j<->n+i + # NOTE: fast reduce of Cv relies on it + # Create masks for substitution and dummy indices + is_subst = (lsa_row_ind < m) & (lsa_col_ind < n) + is_dummy = (lsa_row_ind >= m) & (lsa_col_ind >= n) + + # Map dummy assignments to the correct indices + lsa_row_ind[is_dummy] = lsa_col_ind[is_subst] + m + lsa_col_ind[is_dummy] = lsa_row_ind[is_subst] + n + + return CostMatrix( + C, lsa_row_ind, lsa_col_ind, C[lsa_row_ind, lsa_col_ind].sum() + ) + + def extract_C(C, i, j, m, n): + # assert(C.shape == (m + n, m + n)) + row_ind = [k in i or k - m in j for k in range(m + n)] + col_ind = [k in j or k - n in i for k in range(m + n)] + return C[row_ind, :][:, col_ind] + + def reduce_C(C, i, j, m, n): + # assert(C.shape == (m + n, m + n)) + row_ind = [k not in i and k - m not in j for k in range(m + n)] + col_ind = [k not in j and k - n not in i for k in range(m + n)] + return C[row_ind, :][:, col_ind] + + def reduce_ind(ind, i): + # assert set(ind) == set(range(len(ind))) + rind = ind[[k not in i for k in ind]] + for k in set(i): + rind[rind >= k] -= 1 + return rind + + def match_edges(u, v, pending_g, pending_h, Ce, matched_uv=None): + """ + Parameters: + u, v: matched vertices, u=None or v=None for + deletion/insertion + pending_g, pending_h: lists of edges not yet mapped + Ce: CostMatrix of pending edge mappings + matched_uv: partial vertex edit path + list of tuples (u, v) of previously matched vertex + mappings u<->v, u=None or v=None for + deletion/insertion + + Returns: + list of (i, j): indices of edge mappings g<->h + localCe: local CostMatrix of edge mappings + (basically submatrix of Ce at cross of rows i, cols j) + """ + M = len(pending_g) + N = len(pending_h) + # assert Ce.C.shape == (M + N, M + N) + + # only attempt to match edges after one node match has been made + # this will stop self-edges on the first node being automatically deleted + # even when a substitution is the better option + + substitution_possible = M and N + at_least_one_node_match = matched_uv is None or len(matched_uv) == 0 + if at_least_one_node_match and substitution_possible: + g_ind = [] + h_ind = [] + else: + g_ind = [ + i + for i in range(M) + if pending_g[i][:2] == (u, u) + or any( + pending_g[i][:2] in ((p, u), (u, p), (p, p)) for p, q in matched_uv + ) + ] + h_ind = [ + j + for j in range(N) + if pending_h[j][:2] == (v, v) + or any( + pending_h[j][:2] in ((q, v), (v, q), (q, q)) for p, q in matched_uv + ) + ] + + m = len(g_ind) + n = len(h_ind) + + if m or n: + C = extract_C(Ce.C, g_ind, h_ind, M, N) + # assert C.shape == (m + n, m + n) + + # Forbid structurally invalid matches + # NOTE: inf remembered from Ce construction + for k, i in enumerate(g_ind): + g = pending_g[i][:2] + for l, j in enumerate(h_ind): + h = pending_h[j][:2] + if nx.is_directed(G1) or nx.is_directed(G2): + if any( + g == (p, u) and h == (q, v) or g == (u, p) and h == (v, q) + for p, q in matched_uv + ): + continue + else: + if any( + g in ((p, u), (u, p)) and h in ((q, v), (v, q)) + for p, q in matched_uv + ): + continue + if g == (u, u) or any(g == (p, p) for p, q in matched_uv): + continue + if h == (v, v) or any(h == (q, q) for p, q in matched_uv): + continue + C[k, l] = inf + + localCe = make_CostMatrix(C, m, n) + ij = [ + ( + g_ind[k] if k < m else M + h_ind[l], + h_ind[l] if l < n else N + g_ind[k], + ) + for k, l in zip(localCe.lsa_row_ind, localCe.lsa_col_ind) + if k < m or l < n + ] + + else: + ij = [] + localCe = CostMatrix(np.empty((0, 0)), [], [], 0) + + return ij, localCe + + def reduce_Ce(Ce, ij, m, n): + if len(ij): + i, j = zip(*ij) + m_i = m - sum(1 for t in i if t < m) + n_j = n - sum(1 for t in j if t < n) + return make_CostMatrix(reduce_C(Ce.C, i, j, m, n), m_i, n_j) + return Ce + + def get_edit_ops( + matched_uv, pending_u, pending_v, Cv, pending_g, pending_h, Ce, matched_cost + ): + """ + Parameters: + matched_uv: partial vertex edit path + list of tuples (u, v) of vertex mappings u<->v, + u=None or v=None for deletion/insertion + pending_u, pending_v: lists of vertices not yet mapped + Cv: CostMatrix of pending vertex mappings + pending_g, pending_h: lists of edges not yet mapped + Ce: CostMatrix of pending edge mappings + matched_cost: cost of partial edit path + + Returns: + sequence of + (i, j): indices of vertex mapping u<->v + Cv_ij: reduced CostMatrix of pending vertex mappings + (basically Cv with row i, col j removed) + list of (x, y): indices of edge mappings g<->h + Ce_xy: reduced CostMatrix of pending edge mappings + (basically Ce with rows x, cols y removed) + cost: total cost of edit operation + NOTE: most promising ops first + """ + m = len(pending_u) + n = len(pending_v) + # assert Cv.C.shape == (m + n, m + n) + + # 1) a vertex mapping from optimal linear sum assignment + i, j = min( + (k, l) for k, l in zip(Cv.lsa_row_ind, Cv.lsa_col_ind) if k < m or l < n + ) + xy, localCe = match_edges( + pending_u[i] if i < m else None, + pending_v[j] if j < n else None, + pending_g, + pending_h, + Ce, + matched_uv, + ) + Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h)) + # assert Ce.ls <= localCe.ls + Ce_xy.ls + if prune(matched_cost + Cv.ls + localCe.ls + Ce_xy.ls): + pass + else: + # get reduced Cv efficiently + Cv_ij = CostMatrix( + reduce_C(Cv.C, (i,), (j,), m, n), + reduce_ind(Cv.lsa_row_ind, (i, m + j)), + reduce_ind(Cv.lsa_col_ind, (j, n + i)), + Cv.ls - Cv.C[i, j], + ) + yield (i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls + + # 2) other candidates, sorted by lower-bound cost estimate + other = [] + fixed_i, fixed_j = i, j + if m <= n: + candidates = ( + (t, fixed_j) + for t in range(m + n) + if t != fixed_i and (t < m or t == m + fixed_j) + ) + else: + candidates = ( + (fixed_i, t) + for t in range(m + n) + if t != fixed_j and (t < n or t == n + fixed_i) + ) + for i, j in candidates: + if prune(matched_cost + Cv.C[i, j] + Ce.ls): + continue + Cv_ij = make_CostMatrix( + reduce_C(Cv.C, (i,), (j,), m, n), + m - 1 if i < m else m, + n - 1 if j < n else n, + ) + # assert Cv.ls <= Cv.C[i, j] + Cv_ij.ls + if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + Ce.ls): + continue + xy, localCe = match_edges( + pending_u[i] if i < m else None, + pending_v[j] if j < n else None, + pending_g, + pending_h, + Ce, + matched_uv, + ) + if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls): + continue + Ce_xy = reduce_Ce(Ce, xy, len(pending_g), len(pending_h)) + # assert Ce.ls <= localCe.ls + Ce_xy.ls + if prune(matched_cost + Cv.C[i, j] + Cv_ij.ls + localCe.ls + Ce_xy.ls): + continue + other.append(((i, j), Cv_ij, xy, Ce_xy, Cv.C[i, j] + localCe.ls)) + + yield from sorted(other, key=lambda t: t[4] + t[1].ls + t[3].ls) + + def get_edit_paths( + matched_uv, + pending_u, + pending_v, + Cv, + matched_gh, + pending_g, + pending_h, + Ce, + matched_cost, + ): + """ + Parameters: + matched_uv: partial vertex edit path + list of tuples (u, v) of vertex mappings u<->v, + u=None or v=None for deletion/insertion + pending_u, pending_v: lists of vertices not yet mapped + Cv: CostMatrix of pending vertex mappings + matched_gh: partial edge edit path + list of tuples (g, h) of edge mappings g<->h, + g=None or h=None for deletion/insertion + pending_g, pending_h: lists of edges not yet mapped + Ce: CostMatrix of pending edge mappings + matched_cost: cost of partial edit path + + Returns: + sequence of (vertex_path, edge_path, cost) + vertex_path: complete vertex edit path + list of tuples (u, v) of vertex mappings u<->v, + u=None or v=None for deletion/insertion + edge_path: complete edge edit path + list of tuples (g, h) of edge mappings g<->h, + g=None or h=None for deletion/insertion + cost: total cost of edit path + NOTE: path costs are non-increasing + """ + if prune(matched_cost + Cv.ls + Ce.ls): + return + + if not max(len(pending_u), len(pending_v)): + # assert not len(pending_g) + # assert not len(pending_h) + # path completed! + # assert matched_cost <= maxcost_value + nonlocal maxcost_value + maxcost_value = min(maxcost_value, matched_cost) + yield matched_uv, matched_gh, matched_cost + + else: + edit_ops = get_edit_ops( + matched_uv, + pending_u, + pending_v, + Cv, + pending_g, + pending_h, + Ce, + matched_cost, + ) + for ij, Cv_ij, xy, Ce_xy, edit_cost in edit_ops: + i, j = ij + # assert Cv.C[i, j] + sum(Ce.C[t] for t in xy) == edit_cost + if prune(matched_cost + edit_cost + Cv_ij.ls + Ce_xy.ls): + continue + + # dive deeper + u = pending_u.pop(i) if i < len(pending_u) else None + v = pending_v.pop(j) if j < len(pending_v) else None + matched_uv.append((u, v)) + for x, y in xy: + len_g = len(pending_g) + len_h = len(pending_h) + matched_gh.append( + ( + pending_g[x] if x < len_g else None, + pending_h[y] if y < len_h else None, + ) + ) + sortedx = sorted(x for x, y in xy) + sortedy = sorted(y for x, y in xy) + G = [ + (pending_g.pop(x) if x < len(pending_g) else None) + for x in reversed(sortedx) + ] + H = [ + (pending_h.pop(y) if y < len(pending_h) else None) + for y in reversed(sortedy) + ] + + yield from get_edit_paths( + matched_uv, + pending_u, + pending_v, + Cv_ij, + matched_gh, + pending_g, + pending_h, + Ce_xy, + matched_cost + edit_cost, + ) + + # backtrack + if u is not None: + pending_u.insert(i, u) + if v is not None: + pending_v.insert(j, v) + matched_uv.pop() + for x, g in zip(sortedx, reversed(G)): + if g is not None: + pending_g.insert(x, g) + for y, h in zip(sortedy, reversed(H)): + if h is not None: + pending_h.insert(y, h) + for _ in xy: + matched_gh.pop() + + # Initialization + + pending_u = list(G1.nodes) + pending_v = list(G2.nodes) + + initial_cost = 0 + if roots: + root_u, root_v = roots + if root_u not in pending_u or root_v not in pending_v: + raise nx.NodeNotFound("Root node not in graph.") + + # remove roots from pending + pending_u.remove(root_u) + pending_v.remove(root_v) + + # cost matrix of vertex mappings + m = len(pending_u) + n = len(pending_v) + C = np.zeros((m + n, m + n)) + if node_subst_cost: + C[0:m, 0:n] = np.array( + [ + node_subst_cost(G1.nodes[u], G2.nodes[v]) + for u in pending_u + for v in pending_v + ] + ).reshape(m, n) + if roots: + initial_cost = node_subst_cost(G1.nodes[root_u], G2.nodes[root_v]) + elif node_match: + C[0:m, 0:n] = np.array( + [ + 1 - int(node_match(G1.nodes[u], G2.nodes[v])) + for u in pending_u + for v in pending_v + ] + ).reshape(m, n) + if roots: + initial_cost = 1 - node_match(G1.nodes[root_u], G2.nodes[root_v]) + else: + # all zeroes + pass + # assert not min(m, n) or C[0:m, 0:n].min() >= 0 + if node_del_cost: + del_costs = [node_del_cost(G1.nodes[u]) for u in pending_u] + else: + del_costs = [1] * len(pending_u) + # assert not m or min(del_costs) >= 0 + if node_ins_cost: + ins_costs = [node_ins_cost(G2.nodes[v]) for v in pending_v] + else: + ins_costs = [1] * len(pending_v) + # assert not n or min(ins_costs) >= 0 + inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1 + C[0:m, n : n + m] = np.array( + [del_costs[i] if i == j else inf for i in range(m) for j in range(m)] + ).reshape(m, m) + C[m : m + n, 0:n] = np.array( + [ins_costs[i] if i == j else inf for i in range(n) for j in range(n)] + ).reshape(n, n) + Cv = make_CostMatrix(C, m, n) + + pending_g = list(G1.edges) + pending_h = list(G2.edges) + + # cost matrix of edge mappings + m = len(pending_g) + n = len(pending_h) + C = np.zeros((m + n, m + n)) + if edge_subst_cost: + C[0:m, 0:n] = np.array( + [ + edge_subst_cost(G1.edges[g], G2.edges[h]) + for g in pending_g + for h in pending_h + ] + ).reshape(m, n) + elif edge_match: + C[0:m, 0:n] = np.array( + [ + 1 - int(edge_match(G1.edges[g], G2.edges[h])) + for g in pending_g + for h in pending_h + ] + ).reshape(m, n) + else: + # all zeroes + pass + # assert not min(m, n) or C[0:m, 0:n].min() >= 0 + if edge_del_cost: + del_costs = [edge_del_cost(G1.edges[g]) for g in pending_g] + else: + del_costs = [1] * len(pending_g) + # assert not m or min(del_costs) >= 0 + if edge_ins_cost: + ins_costs = [edge_ins_cost(G2.edges[h]) for h in pending_h] + else: + ins_costs = [1] * len(pending_h) + # assert not n or min(ins_costs) >= 0 + inf = C[0:m, 0:n].sum() + sum(del_costs) + sum(ins_costs) + 1 + C[0:m, n : n + m] = np.array( + [del_costs[i] if i == j else inf for i in range(m) for j in range(m)] + ).reshape(m, m) + C[m : m + n, 0:n] = np.array( + [ins_costs[i] if i == j else inf for i in range(n) for j in range(n)] + ).reshape(n, n) + Ce = make_CostMatrix(C, m, n) + + maxcost_value = Cv.C.sum() + Ce.C.sum() + 1 + + if timeout is not None: + if timeout <= 0: + raise nx.NetworkXError("Timeout value must be greater than 0") + start = time.perf_counter() + + def prune(cost): + if timeout is not None: + if time.perf_counter() - start > timeout: + return True + if upper_bound is not None: + if cost > upper_bound: + return True + if cost > maxcost_value: + return True + if strictly_decreasing and cost >= maxcost_value: + return True + return False + + # Now go! + + done_uv = [] if roots is None else [roots] + + for vertex_path, edge_path, cost in get_edit_paths( + done_uv, pending_u, pending_v, Cv, [], pending_g, pending_h, Ce, initial_cost + ): + # assert sorted(G1.nodes) == sorted(u for u, v in vertex_path if u is not None) + # assert sorted(G2.nodes) == sorted(v for u, v in vertex_path if v is not None) + # assert sorted(G1.edges) == sorted(g for g, h in edge_path if g is not None) + # assert sorted(G2.edges) == sorted(h for g, h in edge_path if h is not None) + # print(vertex_path, edge_path, cost, file = sys.stderr) + # assert cost == maxcost_value + yield list(vertex_path), list(edge_path), float(cost) + + +@nx._dispatchable +def simrank_similarity( + G, + source=None, + target=None, + importance_factor=0.9, + max_iterations=1000, + tolerance=1e-4, +): + """Returns the SimRank similarity of nodes in the graph ``G``. + + SimRank is a similarity metric that says "two objects are considered + to be similar if they are referenced by similar objects." [1]_. + + The pseudo-code definition from the paper is:: + + def simrank(G, u, v): + in_neighbors_u = G.predecessors(u) + in_neighbors_v = G.predecessors(v) + scale = C / (len(in_neighbors_u) * len(in_neighbors_v)) + return scale * sum( + simrank(G, w, x) for w, x in product(in_neighbors_u, in_neighbors_v) + ) + + where ``G`` is the graph, ``u`` is the source, ``v`` is the target, + and ``C`` is a float decay or importance factor between 0 and 1. + + The SimRank algorithm for determining node similarity is defined in + [2]_. + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + + source : node + If this is specified, the returned dictionary maps each node + ``v`` in the graph to the similarity between ``source`` and + ``v``. + + target : node + If both ``source`` and ``target`` are specified, the similarity + value between ``source`` and ``target`` is returned. If + ``target`` is specified but ``source`` is not, this argument is + ignored. + + importance_factor : float + The relative importance of indirect neighbors with respect to + direct neighbors. + + max_iterations : integer + Maximum number of iterations. + + tolerance : float + Error tolerance used to check convergence. When an iteration of + the algorithm finds that no similarity value changes more than + this amount, the algorithm halts. + + Returns + ------- + similarity : dictionary or float + If ``source`` and ``target`` are both ``None``, this returns a + dictionary of dictionaries, where keys are node pairs and value + are similarity of the pair of nodes. + + If ``source`` is not ``None`` but ``target`` is, this returns a + dictionary mapping node to the similarity of ``source`` and that + node. + + If neither ``source`` nor ``target`` is ``None``, this returns + the similarity value for the given pair of nodes. + + Raises + ------ + ExceededMaxIterations + If the algorithm does not converge within ``max_iterations``. + + NodeNotFound + If either ``source`` or ``target`` is not in `G`. + + Examples + -------- + >>> G = nx.cycle_graph(2) + >>> nx.simrank_similarity(G) + {0: {0: 1.0, 1: 0.0}, 1: {0: 0.0, 1: 1.0}} + >>> nx.simrank_similarity(G, source=0) + {0: 1.0, 1: 0.0} + >>> nx.simrank_similarity(G, source=0, target=0) + 1.0 + + The result of this function can be converted to a numpy array + representing the SimRank matrix by using the node order of the + graph to determine which row and column represent each node. + Other ordering of nodes is also possible. + + >>> import numpy as np + >>> sim = nx.simrank_similarity(G) + >>> np.array([[sim[u][v] for v in G] for u in G]) + array([[1., 0.], + [0., 1.]]) + >>> sim_1d = nx.simrank_similarity(G, source=0) + >>> np.array([sim[0][v] for v in G]) + array([1., 0.]) + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/SimRank + .. [2] G. Jeh and J. Widom. + "SimRank: a measure of structural-context similarity", + In KDD'02: Proceedings of the Eighth ACM SIGKDD + International Conference on Knowledge Discovery and Data Mining, + pp. 538--543. ACM Press, 2002. + """ + import numpy as np + + nodelist = list(G) + if source is not None: + if source not in nodelist: + raise nx.NodeNotFound(f"Source node {source} not in G") + else: + s_indx = nodelist.index(source) + else: + s_indx = None + + if target is not None: + if target not in nodelist: + raise nx.NodeNotFound(f"Target node {target} not in G") + else: + t_indx = nodelist.index(target) + else: + t_indx = None + + x = _simrank_similarity_numpy( + G, s_indx, t_indx, importance_factor, max_iterations, tolerance + ) + + if isinstance(x, np.ndarray): + if x.ndim == 1: + return dict(zip(G, x.tolist())) + # else x.ndim == 2 + return {u: dict(zip(G, row)) for u, row in zip(G, x.tolist())} + return float(x) + + +def _simrank_similarity_python( + G, + source=None, + target=None, + importance_factor=0.9, + max_iterations=1000, + tolerance=1e-4, +): + """Returns the SimRank similarity of nodes in the graph ``G``. + + This pure Python version is provided for pedagogical purposes. + + Examples + -------- + >>> G = nx.cycle_graph(2) + >>> nx.similarity._simrank_similarity_python(G) + {0: {0: 1, 1: 0.0}, 1: {0: 0.0, 1: 1}} + >>> nx.similarity._simrank_similarity_python(G, source=0) + {0: 1, 1: 0.0} + >>> nx.similarity._simrank_similarity_python(G, source=0, target=0) + 1 + """ + # build up our similarity adjacency dictionary output + newsim = {u: {v: 1 if u == v else 0 for v in G} for u in G} + + # These functions compute the update to the similarity value of the nodes + # `u` and `v` with respect to the previous similarity values. + def avg_sim(s): + return sum(newsim[w][x] for (w, x) in s) / len(s) if s else 0.0 + + Gadj = G.pred if G.is_directed() else G.adj + + def sim(u, v): + return importance_factor * avg_sim(list(product(Gadj[u], Gadj[v]))) + + for its in range(max_iterations): + oldsim = newsim + newsim = {u: {v: sim(u, v) if u != v else 1 for v in G} for u in G} + is_close = all( + all( + abs(newsim[u][v] - old) <= tolerance * (1 + abs(old)) + for v, old in nbrs.items() + ) + for u, nbrs in oldsim.items() + ) + if is_close: + break + + if its + 1 == max_iterations: + raise nx.ExceededMaxIterations( + f"simrank did not converge after {max_iterations} iterations." + ) + + if source is not None and target is not None: + return newsim[source][target] + if source is not None: + return newsim[source] + return newsim + + +def _simrank_similarity_numpy( + G, + source=None, + target=None, + importance_factor=0.9, + max_iterations=1000, + tolerance=1e-4, +): + """Calculate SimRank of nodes in ``G`` using matrices with ``numpy``. + + The SimRank algorithm for determining node similarity is defined in + [1]_. + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + + source : node + If this is specified, the returned dictionary maps each node + ``v`` in the graph to the similarity between ``source`` and + ``v``. + + target : node + If both ``source`` and ``target`` are specified, the similarity + value between ``source`` and ``target`` is returned. If + ``target`` is specified but ``source`` is not, this argument is + ignored. + + importance_factor : float + The relative importance of indirect neighbors with respect to + direct neighbors. + + max_iterations : integer + Maximum number of iterations. + + tolerance : float + Error tolerance used to check convergence. When an iteration of + the algorithm finds that no similarity value changes more than + this amount, the algorithm halts. + + Returns + ------- + similarity : numpy array or float + If ``source`` and ``target`` are both ``None``, this returns a + 2D array containing SimRank scores of the nodes. + + If ``source`` is not ``None`` but ``target`` is, this returns an + 1D array containing SimRank scores of ``source`` and that + node. + + If neither ``source`` nor ``target`` is ``None``, this returns + the similarity value for the given pair of nodes. + + Examples + -------- + >>> G = nx.cycle_graph(2) + >>> nx.similarity._simrank_similarity_numpy(G) + array([[1., 0.], + [0., 1.]]) + >>> nx.similarity._simrank_similarity_numpy(G, source=0) + array([1., 0.]) + >>> nx.similarity._simrank_similarity_numpy(G, source=0, target=0) + 1.0 + + References + ---------- + .. [1] G. Jeh and J. Widom. + "SimRank: a measure of structural-context similarity", + In KDD'02: Proceedings of the Eighth ACM SIGKDD + International Conference on Knowledge Discovery and Data Mining, + pp. 538--543. ACM Press, 2002. + """ + # This algorithm follows roughly + # + # S = max{C * (A.T * S * A), I} + # + # where C is the importance factor, A is the column normalized + # adjacency matrix, and I is the identity matrix. + import numpy as np + + adjacency_matrix = nx.to_numpy_array(G) + + # column-normalize the ``adjacency_matrix`` + s = np.array(adjacency_matrix.sum(axis=0)) + s[s == 0] = 1 + adjacency_matrix /= s # adjacency_matrix.sum(axis=0) + + newsim = np.eye(len(G), dtype=np.float64) + for its in range(max_iterations): + prevsim = newsim.copy() + newsim = importance_factor * ((adjacency_matrix.T @ prevsim) @ adjacency_matrix) + np.fill_diagonal(newsim, 1.0) + + if np.allclose(prevsim, newsim, atol=tolerance): + break + + if its + 1 == max_iterations: + raise nx.ExceededMaxIterations( + f"simrank did not converge after {max_iterations} iterations." + ) + + if source is not None and target is not None: + return float(newsim[source, target]) + if source is not None: + return newsim[source] + return newsim + + +@np_random_state("seed") +def _prepare_panther_paths( + G, + source, + path_length=5, + c=0.5, + delta=0.1, + eps=None, + weight="weight", + remove_isolates=True, + k=None, + seed=None, +): + """Common preparation code for Panther similarity algorithms. + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + source : node + Source node for similarity calculation + path_length : int + How long the randomly generated paths should be + c : float + A universal constant that controls the number of random paths to generate + delta : float + The probability parameter for similarity approximation + eps : float or None + The error bound for similarity approximation + weight : string or None + The name of an edge attribute that holds the numerical value used as a weight + remove_isolates : bool + Whether to remove isolated nodes from graph processing + k : int or None + The number of most similar nodes to return. If provided, validates that + ``k`` is not greater than the number of nodes in the graph. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + PantherPaths + A tuple containing the prepared data: + - G: The graph (possibly with isolates removed) + - inv_node_map: Dictionary mapping node names to indices + - index_map: Populated index map of paths + - inv_sample_size: Inverse of sample size (for fast calculation) + - eps: Error bound for similarity approximation + """ + import numpy as np + + if source not in G: + raise nx.NodeNotFound(f"Source node {source} not in G") + + isolates = set(nx.isolates(G)) + + if source in isolates: + raise nx.NetworkXUnfeasible( + f"Panther similarity is not defined for the isolated source node {source}." + ) + + if remove_isolates: + G = G.subgraph(node for node in G if node not in isolates).copy() + + # According to [1], they empirically determined + # a good value for ``eps`` to be sqrt( 1 / |E| ) + if eps is None: + eps = np.sqrt(1.0 / G.number_of_edges()) + + num_nodes = G.number_of_nodes() + + # Check if k is provided and validate it against the number of nodes + if k is not None and not remove_isolates: # For panther_vector_similarity + if num_nodes < k: + raise nx.NetworkXUnfeasible( + f"The number of requested nodes {k} is greater than the number of nodes {num_nodes}." + ) + + inv_node_map = {name: index for index, name in enumerate(G)} + + # Calculate the sample size ``R`` for how many paths + # to randomly generate + t_choose_2 = math.comb(path_length, 2) + sample_size = int((c / eps**2) * (np.log2(t_choose_2) + 1 + np.log(1 / delta))) + index_map = {} + + # Check for isolated nodes before generating random paths + # If there are still isolated nodes in the graph after filtering, + # they will cause issues with path generation + remaining_isolates = set(nx.isolates(G)) + if remaining_isolates: + raise nx.NetworkXUnfeasible( + f"Cannot generate random paths with isolated nodes present: {remaining_isolates}" + ) + + # Generate the random paths and populate the index_map + for _ in generate_random_paths( + G, + sample_size, + path_length=path_length, + index_map=index_map, + weight=weight, + seed=seed, + ): + # NOTE: index_map is modified in-place by `generate_random_paths` + pass + + return ( + G, # The graph with isolated nodes removed + inv_node_map, + index_map, + 1 / sample_size, + eps, + ) + + +@np_random_state("seed") +@nx._dispatchable(edge_attrs="weight") +def panther_similarity( + G, + source, + k=5, + path_length=5, + c=0.5, + delta=0.1, + eps=None, + weight="weight", + seed=None, +): + r"""Returns the Panther similarity of nodes in the graph `G` to node ``v``. + + Panther is a similarity metric that says "two objects are considered + to be similar if they frequently appear on the same paths." [1]_. + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + source : node + Source node for which to find the top `k` similar other nodes + k : int (default = 5) + The number of most similar nodes to return. + path_length : int (default = 5) + How long the randomly generated paths should be (``T`` in [1]_) + c : float (default = 0.5) + A universal constant that controls the number of random paths to generate. + Higher values increase the number of sample paths and potentially improve + accuracy at the cost of more computation. Defaults to 0.5 as recommended + in [1]_. + delta : float (default = 0.1) + The probability that the similarity $S$ is not an epsilon-approximation to (R, phi), + where $R$ is the number of random paths and $\phi$ is the probability + that an element sampled from a set $A \subseteq D$, where $D$ is the domain. + eps : float or None (default = None) + The error bound for similarity approximation. This controls the accuracy + of the sampled paths in representing the true similarity. Smaller values + yield more accurate results but require more sample paths. If `None`, a + value of ``sqrt(1/|E|)`` is used, which the authors found empirically + effective. + weight : string or None, optional (default="weight") + The name of an edge attribute that holds the numerical value + used as a weight. If None then each edge has weight 1. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + similarity : dictionary + Dictionary of nodes to similarity scores (as floats). Note: + the self-similarity (i.e., ``v``) will not be included in + the returned dictionary. So, for ``k = 5``, a dictionary of + top 4 nodes and their similarity scores will be returned. + + Raises + ------ + NetworkXUnfeasible + If `source` is an isolated node. + + NodeNotFound + If `source` is not in `G`. + + Notes + ----- + The isolated nodes in `G` are ignored. + + Examples + -------- + >>> G = nx.star_graph(10) + >>> sim = nx.panther_similarity(G, 0) + + References + ---------- + .. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J. + Panther: Fast top-k similarity search on large networks. + In Proceedings of the ACM SIGKDD International Conference + on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454). + Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267. + """ + import numpy as np + + # Use helper method to prepare common data structures + G, inv_node_map, index_map, inv_sample_size, eps = _prepare_panther_paths( + G, + source, + path_length=path_length, + c=c, + delta=delta, + eps=eps, + weight=weight, + k=k, + seed=seed, + ) + + num_nodes = G.number_of_nodes() + node_list = list(G.nodes) + + # Check number of nodes after any modifications by _prepare_panther_paths + if num_nodes < k: + raise nx.NetworkXUnfeasible( + f"The number of requested nodes {k} is greater than the number of nodes {num_nodes}." + ) + + S = np.zeros(num_nodes) + source_paths = set(index_map[source]) + + # Calculate the path similarities + # between ``source`` (v) and ``node`` (v_j) + # using our inverted index mapping of + # vertices to paths + for node, paths in index_map.items(): + # Only consider paths where both + # ``node`` and ``source`` are present + common_paths = source_paths.intersection(paths) + S[inv_node_map[node]] = len(common_paths) * inv_sample_size + + # Retrieve top ``k+1`` similar to account for removing self-similarity + # Note: the below performed anywhere from 4-10x faster + # (depending on input sizes) vs the equivalent ``np.argsort(S)[::-1]`` + partition_k = min(k + 1, num_nodes) + top_k_unsorted = np.argpartition(S, -partition_k)[-partition_k:] + top_k_sorted = top_k_unsorted[np.argsort(S[top_k_unsorted])][::-1] + + # Add back the similarity scores + # Convert numpy scalars to native Python types for dispatch compatibility + top_k_with_val = dict( + zip((node_list[i] for i in top_k_sorted), S[top_k_sorted].tolist()) + ) + + # Remove the self-similarity + top_k_with_val.pop(source, None) + return top_k_with_val + + +@np_random_state("seed") +@nx._dispatchable(edge_attrs="weight") +def panther_vector_similarity( + G, + source, + *, + D=10, + k=5, + path_length=5, + c=0.5, + delta=0.1, + eps=None, + weight="weight", + seed=None, +): + r"""Returns the Panther vector similarity (Panther++) of nodes in `G`. + + Computes similarity between nodes based on the "Panther++" algorithm [1]_, which extends + the basic Panther algorithm by using feature vectors to better capture structural + similarity. + + While basic Panther similarity measures how often two nodes appear on the same paths, + Panther vector similarity (Panther++) creates a ``D``-dimensional feature vector for each + node using its top similarity scores with other nodes, then computes similarity based + on the Euclidean distance between these feature vectors. This approach better captures + structural similarity and addresses the bias towards close neighbors present in + the original Panther algorithm. + + This approach is preferred when: + + 1. You need better structural similarity than basic path co-occurrence + 2. You want to overcome the close-neighbor bias of standard Panther + 3. You're working with large graphs where k-d tree indexing would be beneficial + 4. Graph edit distance-like similarity is more appropriate than path co-occurrence + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + source : node + Source node for which to find the top ``k`` similar other nodes + D : int + The number of similarity scores to use (in descending order) + for each feature vector. Defaults to 10. Note that the original paper + used D=50 [1]_, but KDTree is optimized for lower dimensions. + k : int + The number of most similar nodes to return + path_length : int + How long the randomly generated paths should be (``T`` in [1]_) + c : float + A universal constant that controls the number of random paths to generate. + Higher values increase the number of sample paths and potentially improve + accuracy at the cost of more computation. Defaults to 0.5 as recommended + in [1]_. + delta : float + The probability that ``S`` is not an epsilon-approximation to (R, phi) + eps : float + The error bound for similarity approximation. This controls the accuracy + of the sampled paths in representing the true similarity. Smaller values + yield more accurate results but require more sample paths. If None, a + value of ``sqrt(1/|E|)`` is used, which the authors found empirically + effective. + weight : string or None, optional (default="weight") + The name of an edge attribute that holds the numerical value + used as a weight. If `None` then each edge has weight 1. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + similarity : dict + Dict of nodes to similarity scores (as floats). + Note: the self-similarity (i.e., `node`) is not included in the dict. + + Examples + -------- + >>> G = nx.star_graph(100) + + The "hub" node is distinct from the "spoke" nodes + + >>> from pprint import pprint + >>> pprint(nx.panther_vector_similarity(G, source=0, seed=42)) + {35: 0.10402634656233918, + 61: 0.10434063328712018, + 65: 0.10401247833456054, + 85: 0.10506718868571752, + 88: 0.10402634656233918} + + But "spoke" nodes are similar to one another + + >>> result = nx.panther_vector_similarity(G, source=1, seed=42) + >>> len(result) + 5 + >>> all(similarity == 1.0 for similarity in result.values()) + True + + Notes + ----- + Results may be nondeterministic when feature vectors have the same distances, + as the KDTree's internal tie-breaking behavior can vary between runs. + Using the same ``seed`` parameter ensures reproducible results. + + References + ---------- + .. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J. + Panther: Fast top-k similarity search on large networks. + In Proceedings of the ACM SIGKDD International Conference + on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454). + Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267. + """ + import numpy as np + import scipy as sp + + # Use helper method to prepare common data structures but keep isolates in the graph + G, inv_node_map, index_map, inv_sample_size, eps = _prepare_panther_paths( + G, + source, + path_length=path_length, + c=c, + delta=delta, + eps=eps, + weight=weight, + remove_isolates=False, + k=k, + seed=seed, + ) + num_nodes = G.number_of_nodes() + node_list = list(G.nodes) + + # Ensure D doesn't exceed the number of nodes + if num_nodes < D: + raise nx.NetworkXUnfeasible( + f"The number of requested similarity scores {D} is greater than the number of nodes {num_nodes}." + ) + + similarities = np.zeros((num_nodes, num_nodes)) + theta = np.zeros((num_nodes, D)) + index_map_sets = {node: set(paths) for node, paths in index_map.items()} + + # Calculate the path similarities for each node + for vi_idx, vi in enumerate(G.nodes): + vi_paths = index_map_sets[vi] + + for node, node_paths in index_map_sets.items(): + # Calculate similarity score + common_path_count = len(vi_paths.intersection(node_paths)) + similarities[vi_idx, inv_node_map[node]] = ( + common_path_count * inv_sample_size + ) + + # Build up the feature vector using the largest D similarity scores + theta[vi_idx] = np.sort(np.partition(similarities[vi_idx], -D)[-D:])[::-1] + + # Insert the feature vectors into a k-d tree + # for fast retrieval + kdtree = sp.spatial.KDTree(theta) + + # Retrieve top ``k+1`` similar vertices (i.e., vectors) + # (based on their Euclidean distance) + # Note that it's k+1 because the source node will be included and later removed + query_k = min(k + 1, num_nodes) + neighbor_distances, nearest_neighbors = kdtree.query( + theta[inv_node_map[source]], k=query_k + ) + + # Ensure results are always arrays (KDTree returns scalars when k=1) + neighbor_distances = np.atleast_1d(neighbor_distances) + nearest_neighbors = np.atleast_1d(nearest_neighbors) + + # The paper defines the similarity S(v_i, v_j) as + # 1 / || Theta(v_i) - Theta(v_j) || + # Calculate reciprocals and normalize to [0, 1] range + + # Handle the case where distances are very small or zero (common in small graphs) + # Use the passed in eps parameter instead of defining a new epsilon + neighbor_distances = np.maximum(neighbor_distances, eps) + similarities = 1 / neighbor_distances + + # Always normalize to ensure values are between 0 and 1 + if len(similarities) > 0 and (max_sim := np.max(similarities)) > 0: + similarities /= max_sim + + # Add back the similarity scores (i.e., distances) + # Convert numpy scalars to native Python types for dispatch compatibility + top_k_with_val = dict( + zip((node_list[n] for n in nearest_neighbors), similarities.tolist()) + ) + + # Remove the self-similarity + top_k_with_val.pop(source, None) + + # Ensure we return exactly k results (sorted by similarity) + if len(top_k_with_val) > k: + sorted_items = sorted(top_k_with_val.items(), key=lambda x: x[1], reverse=True) + top_k_with_val = dict(sorted_items[:k]) + + return top_k_with_val + + +@np_random_state("seed") +@nx._dispatchable(edge_attrs="weight") +def generate_random_paths( + G, + sample_size, + path_length=5, + index_map=None, + weight="weight", + seed=None, + *, + source=None, +): + """Randomly generate `sample_size` paths of length `path_length`. + + Parameters + ---------- + G : NetworkX graph + A NetworkX graph + sample_size : integer + The number of paths to generate. This is ``R`` in [1]_. + path_length : integer (default = 5) + The maximum size of the path to randomly generate. + This is ``T`` in [1]_. According to the paper, ``T >= 5`` is + recommended. + index_map : dictionary, optional + If provided, this will be populated with the inverted + index of nodes mapped to the set of generated random path + indices within ``paths``. + weight : string or None, optional (default="weight") + The name of an edge attribute that holds the numerical value + used as a weight. If None then each edge has weight 1. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + source : node, optional + Node to use as the starting point for all generated paths. + If None then starting nodes are selected at random with uniform probability. + + Returns + ------- + paths : generator of lists + Generator of `sample_size` paths each with length `path_length`. + + Examples + -------- + The generator yields `sample_size` number of paths of length `path_length` + drawn from `G`: + + >>> G = nx.complete_graph(5) + >>> next(nx.generate_random_paths(G, sample_size=1, path_length=3, seed=42)) + [3, 4, 2, 3] + >>> list(nx.generate_random_paths(G, sample_size=3, path_length=4, seed=42)) + [[3, 4, 2, 3, 0], [2, 0, 2, 1, 0], [2, 0, 4, 3, 0]] + + By passing a dictionary into `index_map`, it will build an + inverted index mapping of nodes to the paths in which that node is present: + + >>> G = nx.wheel_graph(10) + >>> index_map = {} + >>> random_paths = list( + ... nx.generate_random_paths(G, sample_size=3, index_map=index_map, seed=2771) + ... ) + >>> random_paths + [[3, 2, 1, 9, 8, 7], [4, 0, 5, 6, 7, 8], [3, 0, 5, 0, 9, 8]] + >>> paths_containing_node_0 = [ + ... random_paths[path_idx] for path_idx in index_map.get(0, []) + ... ] + >>> paths_containing_node_0 + [[4, 0, 5, 6, 7, 8], [3, 0, 5, 0, 9, 8]] + + References + ---------- + .. [1] Zhang, J., Tang, J., Ma, C., Tong, H., Jing, Y., & Li, J. + Panther: Fast top-k similarity search on large networks. + In Proceedings of the ACM SIGKDD International Conference + on Knowledge Discovery and Data Mining (Vol. 2015-August, pp. 1445–1454). + Association for Computing Machinery. https://doi.org/10.1145/2783258.2783267. + """ + import numpy as np + + randint_fn = ( + seed.integers if isinstance(seed, np.random.Generator) else seed.randint + ) + + # Calculate transition probabilities between + # every pair of vertices according to Eq. (3) + adj_mat = nx.to_numpy_array(G, weight=weight) + + # Handle isolated nodes by checking for zero row sums + row_sums = adj_mat.sum(axis=1).reshape(-1, 1) + inv_row_sums = np.reciprocal(row_sums) + transition_probabilities = adj_mat * inv_row_sums + + node_map = list(G) + num_nodes = G.number_of_nodes() + + for path_index in range(sample_size): + if source is None: + # Sample current vertex v = v_i uniformly at random + node_index = randint_fn(num_nodes) + node = node_map[node_index] + else: + if source not in node_map: + raise nx.NodeNotFound(f"Initial node {source} not in G") + + node = source + node_index = node_map.index(node) + + # Add v into p_r and add p_r into the path set + # of v, i.e., P_v + path = [node] + + # Build the inverted index (P_v) of vertices to paths + if index_map is not None: + if node in index_map: + index_map[node].add(path_index) + else: + index_map[node] = {path_index} + + starting_index = node_index + for _ in range(path_length): + # Randomly sample a neighbor (v_j) according + # to transition probabilities from ``node`` (v) to its neighbors + nbr_index = seed.choice( + num_nodes, p=transition_probabilities[starting_index] + ) + + # Set current vertex (v = v_j) + starting_index = nbr_index + + # Add v into p_r + nbr_node = node_map[nbr_index] + path.append(nbr_node) + + # Add p_r into P_v + if index_map is not None: + if nbr_node in index_map: + index_map[nbr_node].add(path_index) + else: + index_map[nbr_node] = {path_index} + + yield path diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/simple_paths.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/simple_paths.py new file mode 100644 index 0000000000000000000000000000000000000000..d9656ba53c3ae48b07fa47c1ac3e691b32e2d8ca --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/simple_paths.py @@ -0,0 +1,966 @@ +from heapq import heappop, heappush +from itertools import count + +import networkx as nx +from networkx.algorithms.shortest_paths.weighted import _weight_function +from networkx.utils import not_implemented_for, pairwise + +__all__ = [ + "all_simple_paths", + "is_simple_path", + "shortest_simple_paths", + "all_simple_edge_paths", +] + + +@nx._dispatchable +def is_simple_path(G, nodes): + """Returns True if and only if `nodes` form a simple path in `G`. + + A *simple path* in a graph is a nonempty sequence of nodes in which + no node appears more than once in the sequence, and each adjacent + pair of nodes in the sequence is adjacent in the graph. + + Parameters + ---------- + G : graph + A NetworkX graph. + nodes : list + A list of one or more nodes in the graph `G`. + + Returns + ------- + bool + Whether the given list of nodes represents a simple path in `G`. + + Notes + ----- + An empty list of nodes is not a path but a list of one node is a + path. Here's an explanation why. + + This function operates on *node paths*. One could also consider + *edge paths*. There is a bijection between node paths and edge + paths. + + The *length of a path* is the number of edges in the path, so a list + of nodes of length *n* corresponds to a path of length *n* - 1. + Thus the smallest edge path would be a list of zero edges, the empty + path. This corresponds to a list of one node. + + To convert between a node path and an edge path, you can use code + like the following:: + + >>> from networkx.utils import pairwise + >>> nodes = [0, 1, 2, 3] + >>> edges = list(pairwise(nodes)) + >>> edges + [(0, 1), (1, 2), (2, 3)] + >>> nodes = [edges[0][0]] + [v for u, v in edges] + >>> nodes + [0, 1, 2, 3] + + Examples + -------- + >>> G = nx.cycle_graph(4) + >>> nx.is_simple_path(G, [2, 3, 0]) + True + >>> nx.is_simple_path(G, [0, 2]) + False + + """ + # The empty list is not a valid path. Could also return + # NetworkXPointlessConcept here. + if len(nodes) == 0: + return False + + # If the list is a single node, just check that the node is actually + # in the graph. + if len(nodes) == 1: + return nodes[0] in G + + # check that all nodes in the list are in the graph, if at least one + # is not in the graph, then this is not a simple path + if not all(n in G for n in nodes): + return False + + # If the list contains repeated nodes, then it's not a simple path + if len(set(nodes)) != len(nodes): + return False + + # Test that each adjacent pair of nodes is adjacent. + return all(v in G[u] for u, v in pairwise(nodes)) + + +@nx._dispatchable +def all_simple_paths(G, source, target, cutoff=None): + """Generate all simple paths in the graph G from source to target. + + A simple path is a path with no repeated nodes. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : nodes + Single node or iterable of nodes at which to end path + + cutoff : integer, optional + Depth to stop the search. Only paths with length <= `cutoff` are returned. + where the mathematical "length of a path" is `len(path) -1` (number of edges). + + Returns + ------- + path_generator: generator + A generator that produces lists of simple paths. If there are no paths + between the source and target within the given cutoff the generator + produces no output. If it is possible to traverse the same sequence of + nodes in multiple ways, namely through parallel edges, then it will be + returned multiple times (once for each viable edge combination). + + Examples + -------- + This iterator generates lists of nodes:: + + >>> G = nx.complete_graph(4) + >>> for path in nx.all_simple_paths(G, source=0, target=3): + ... print(path) + ... + [0, 1, 2, 3] + [0, 1, 3] + [0, 2, 1, 3] + [0, 2, 3] + [0, 3] + + You can generate only those paths that are shorter than a certain + length by using the `cutoff` keyword argument:: + + >>> paths = nx.all_simple_paths(G, source=0, target=3, cutoff=2) + >>> print(list(paths)) + [[0, 1, 3], [0, 2, 3], [0, 3]] + + To get each path as the corresponding list of edges, you can use the + :func:`networkx.utils.pairwise` helper function:: + + >>> paths = nx.all_simple_paths(G, source=0, target=3) + >>> for path in map(nx.utils.pairwise, paths): + ... print(list(path)) + [(0, 1), (1, 2), (2, 3)] + [(0, 1), (1, 3)] + [(0, 2), (2, 1), (1, 3)] + [(0, 2), (2, 3)] + [(0, 3)] + + Pass an iterable of nodes as target to generate all paths ending in any of several nodes:: + + >>> G = nx.complete_graph(4) + >>> for path in nx.all_simple_paths(G, source=0, target=[3, 2]): + ... print(path) + ... + [0, 1, 2] + [0, 1, 2, 3] + [0, 1, 3] + [0, 1, 3, 2] + [0, 2] + [0, 2, 1, 3] + [0, 2, 3] + [0, 3] + [0, 3, 1, 2] + [0, 3, 2] + + The singleton path from ``source`` to itself is considered a simple path and is + included in the results: + + >>> G = nx.empty_graph(5) + >>> list(nx.all_simple_paths(G, source=0, target=0)) + [[0]] + + >>> G = nx.path_graph(3) + >>> list(nx.all_simple_paths(G, source=0, target={0, 1, 2})) + [[0], [0, 1], [0, 1, 2]] + + Iterate over each path from the root nodes to the leaf nodes in a + directed acyclic graph using a functional programming approach:: + + >>> from itertools import chain + >>> from itertools import product + >>> from itertools import starmap + >>> from functools import partial + >>> + >>> chaini = chain.from_iterable + >>> + >>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)]) + >>> roots = (v for v, d in G.in_degree() if d == 0) + >>> leaves = (v for v, d in G.out_degree() if d == 0) + >>> all_paths = partial(nx.all_simple_paths, G) + >>> list(chaini(starmap(all_paths, product(roots, leaves)))) + [[0, 1, 2], [0, 3, 2]] + + The same list computed using an iterative approach:: + + >>> G = nx.DiGraph([(0, 1), (1, 2), (0, 3), (3, 2)]) + >>> roots = (v for v, d in G.in_degree() if d == 0) + >>> leaves = (v for v, d in G.out_degree() if d == 0) + >>> all_paths = [] + >>> for root in roots: + ... for leaf in leaves: + ... paths = nx.all_simple_paths(G, root, leaf) + ... all_paths.extend(paths) + >>> all_paths + [[0, 1, 2], [0, 3, 2]] + + Iterate over each path from the root nodes to the leaf nodes in a + directed acyclic graph passing all leaves together to avoid unnecessary + compute:: + + >>> G = nx.DiGraph([(0, 1), (2, 1), (1, 3), (1, 4)]) + >>> roots = (v for v, d in G.in_degree() if d == 0) + >>> leaves = [v for v, d in G.out_degree() if d == 0] + >>> all_paths = [] + >>> for root in roots: + ... paths = nx.all_simple_paths(G, root, leaves) + ... all_paths.extend(paths) + >>> all_paths + [[0, 1, 3], [0, 1, 4], [2, 1, 3], [2, 1, 4]] + + If parallel edges offer multiple ways to traverse a given sequence of + nodes, this sequence of nodes will be returned multiple times: + + >>> G = nx.MultiDiGraph([(0, 1), (0, 1), (1, 2)]) + >>> list(nx.all_simple_paths(G, 0, 2)) + [[0, 1, 2], [0, 1, 2]] + + Notes + ----- + This algorithm uses a modified depth-first search to generate the + paths [1]_. A single path can be found in $O(V+E)$ time but the + number of simple paths in a graph can be very large, e.g. $O(n!)$ in + the complete graph of order $n$. + + This function does not check that a path exists between `source` and + `target`. For large graphs, this may result in very long runtimes. + Consider using `has_path` to check that a path exists between `source` and + `target` before calling this function on large graphs. + + References + ---------- + .. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms", + Addison Wesley Professional, 3rd ed., 2001. + + See Also + -------- + all_shortest_paths, shortest_path, has_path + + """ + for edge_path in all_simple_edge_paths(G, source, target, cutoff): + yield [source] + [edge[1] for edge in edge_path] + + +@nx._dispatchable +def all_simple_edge_paths(G, source, target, cutoff=None): + """Generate lists of edges for all simple paths in G from source to target. + + A simple path is a path with no repeated nodes. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : nodes + Single node or iterable of nodes at which to end path + + cutoff : integer, optional + Depth to stop the search. Only paths with length <= `cutoff` are returned. + Note that the length of an edge path is the number of edges. + + Returns + ------- + path_generator: generator + A generator that produces lists of simple paths. If there are no paths + between the source and target within the given cutoff the generator + produces no output. + For multigraphs, the list of edges have elements of the form `(u,v,k)`. + Where `k` corresponds to the edge key. + + Examples + -------- + + Print the simple path edges of a Graph:: + + >>> g = nx.Graph([(1, 2), (2, 4), (1, 3), (3, 4)]) + >>> for path in sorted(nx.all_simple_edge_paths(g, 1, 4)): + ... print(path) + [(1, 2), (2, 4)] + [(1, 3), (3, 4)] + + Print the simple path edges of a MultiGraph. Returned edges come with + their associated keys:: + + >>> mg = nx.MultiGraph() + >>> mg.add_edge(1, 2, key="k0") + 'k0' + >>> mg.add_edge(1, 2, key="k1") + 'k1' + >>> mg.add_edge(2, 3, key="k0") + 'k0' + >>> for path in sorted(nx.all_simple_edge_paths(mg, 1, 3)): + ... print(path) + [(1, 2, 'k0'), (2, 3, 'k0')] + [(1, 2, 'k1'), (2, 3, 'k0')] + + When ``source`` is one of the targets, the empty path starting and ending at + ``source`` without traversing any edge is considered a valid simple edge path + and is included in the results: + + >>> G = nx.Graph() + >>> G.add_node(0) + >>> paths = list(nx.all_simple_edge_paths(G, 0, 0)) + >>> for path in paths: + ... print(path) + [] + >>> len(paths) + 1 + + You can use the `cutoff` parameter to only generate paths that are + shorter than a certain length: + + >>> g = nx.Graph([(1, 2), (2, 3), (3, 4), (4, 5), (1, 4), (1, 5)]) + >>> for path in sorted(nx.all_simple_edge_paths(g, 1, 5)): + ... print(path) + [(1, 2), (2, 3), (3, 4), (4, 5)] + [(1, 4), (4, 5)] + [(1, 5)] + >>> for path in sorted(nx.all_simple_edge_paths(g, 1, 5, cutoff=1)): + ... print(path) + [(1, 5)] + >>> for path in sorted(nx.all_simple_edge_paths(g, 1, 5, cutoff=2)): + ... print(path) + [(1, 4), (4, 5)] + [(1, 5)] + + Notes + ----- + This algorithm uses a modified depth-first search to generate the + paths [1]_. A single path can be found in $O(V+E)$ time but the + number of simple paths in a graph can be very large, e.g. $O(n!)$ in + the complete graph of order $n$. + + References + ---------- + .. [1] R. Sedgewick, "Algorithms in C, Part 5: Graph Algorithms", + Addison Wesley Professional, 3rd ed., 2001. + + See Also + -------- + all_shortest_paths, shortest_path, all_simple_paths + + """ + if source not in G: + raise nx.NodeNotFound(f"source node {source} not in graph") + + if target in G: + targets = {target} + else: + try: + targets = set(target) + except TypeError as err: + raise nx.NodeNotFound(f"target node {target} not in graph") from err + + cutoff = cutoff if cutoff is not None else len(G) - 1 + + if cutoff >= 0 and targets: + yield from _all_simple_edge_paths(G, source, targets, cutoff) + + +def _all_simple_edge_paths(G, source, targets, cutoff): + # We simulate recursion with a stack, keeping the current path being explored + # and the outgoing edge iterators at each point in the stack. + # To avoid unnecessary checks, the loop is structured in a way such that a path + # is considered for yielding only after a new node/edge is added. + # We bootstrap the search by adding a dummy iterator to the stack that only yields + # a dummy edge to source (so that the trivial path has a chance of being included). + + get_edges = ( + (lambda node: G.edges(node, keys=True)) + if G.is_multigraph() + else (lambda node: G.edges(node)) + ) + + # The current_path is a dictionary that maps nodes in the path to the edge that was + # used to enter that node (instead of a list of edges) because we want both a fast + # membership test for nodes in the path and the preservation of insertion order. + current_path = {None: None} + stack = [iter([(None, source)])] + + while stack: + # 1. Try to extend the current path. + next_edge = next((e for e in stack[-1] if e[1] not in current_path), None) + if next_edge is None: + # All edges of the last node in the current path have been explored. + stack.pop() + current_path.popitem() + continue + previous_node, next_node, *_ = next_edge + + # 2. Check if we've reached a target. + if next_node in targets: + yield (list(current_path.values()) + [next_edge])[2:] # remove dummy edge + + # 3. Only expand the search through the next node if it makes sense. + if len(current_path) - 1 < cutoff and ( + targets - current_path.keys() - {next_node} + ): + current_path[next_node] = next_edge + stack.append(iter(get_edges(next_node))) + + +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def shortest_simple_paths(G, source, target, weight=None): + """Generate all simple paths in the graph G from source to target, + starting from shortest ones. + + A simple path is a path with no repeated nodes. + + If a weighted shortest path search is to be used, no negative weights + are allowed. + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node for path + + target : node + Ending node for path + + weight : string or function + If it is a string, it is the name of the edge attribute to be + used as a weight. + + If it is a function, the weight of an edge is the value returned + by the function. The function must accept exactly three positional + arguments: the two endpoints of an edge and the dictionary of edge + attributes for that edge. The function must return a number or None. + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + If None all edges are considered to have unit weight. Default + value None. + + Returns + ------- + path_generator: generator + A generator that produces lists of simple paths, in order from + shortest to longest. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + NetworkXError + If source or target nodes are not in the input graph. + + NetworkXNotImplemented + If the input graph is a Multi[Di]Graph. + + Examples + -------- + + >>> G = nx.cycle_graph(7) + >>> paths = list(nx.shortest_simple_paths(G, 0, 3)) + >>> print(paths) + [[0, 1, 2, 3], [0, 6, 5, 4, 3]] + + You can use this function to efficiently compute the k shortest/best + paths between two nodes. + + >>> from itertools import islice + >>> def k_shortest_paths(G, source, target, k, weight=None): + ... return list( + ... islice(nx.shortest_simple_paths(G, source, target, weight=weight), k) + ... ) + >>> for path in k_shortest_paths(G, 0, 3, 2): + ... print(path) + [0, 1, 2, 3] + [0, 6, 5, 4, 3] + + Notes + ----- + This procedure is based on algorithm by Jin Y. Yen [1]_. Finding + the first $K$ paths requires $O(KN^3)$ operations. + + See Also + -------- + all_shortest_paths + shortest_path + all_simple_paths + + References + ---------- + .. [1] Jin Y. Yen, "Finding the K Shortest Loopless Paths in a + Network", Management Science, Vol. 17, No. 11, Theory Series + (Jul., 1971), pp. 712-716. + + """ + if source not in G: + raise nx.NodeNotFound(f"source node {source} not in graph") + + if target not in G: + raise nx.NodeNotFound(f"target node {target} not in graph") + + if weight is None: + length_func = len + shortest_path_func = _bidirectional_shortest_path + else: + wt = _weight_function(G, weight) + + def length_func(path): + return sum( + wt(u, v, G.get_edge_data(u, v)) for (u, v) in zip(path, path[1:]) + ) + + shortest_path_func = _bidirectional_dijkstra + + listA = [] + listB = PathBuffer() + prev_path = None + while True: + if not prev_path: + length, path = shortest_path_func(G, source, target, weight=weight) + listB.push(length, path) + else: + ignore_nodes = set() + ignore_edges = set() + for i in range(1, len(prev_path)): + root = prev_path[:i] + root_length = length_func(root) + for path in listA: + if path[:i] == root: + ignore_edges.add((path[i - 1], path[i])) + try: + length, spur = shortest_path_func( + G, + root[-1], + target, + ignore_nodes=ignore_nodes, + ignore_edges=ignore_edges, + weight=weight, + ) + path = root[:-1] + spur + listB.push(root_length + length, path) + except nx.NetworkXNoPath: + pass + ignore_nodes.add(root[-1]) + + if listB: + path = listB.pop() + yield path + listA.append(path) + prev_path = path + else: + break + + +class PathBuffer: + def __init__(self): + self.paths = set() + self.sortedpaths = [] + self.counter = count() + + def __len__(self): + return len(self.sortedpaths) + + def push(self, cost, path): + hashable_path = tuple(path) + if hashable_path not in self.paths: + heappush(self.sortedpaths, (cost, next(self.counter), path)) + self.paths.add(hashable_path) + + def pop(self): + (cost, num, path) = heappop(self.sortedpaths) + hashable_path = tuple(path) + self.paths.remove(hashable_path) + return path + + +def _bidirectional_shortest_path( + G, source, target, ignore_nodes=None, ignore_edges=None, weight=None +): + """Returns the shortest path between source and target ignoring + nodes and edges in the containers ignore_nodes and ignore_edges. + + This is a custom modification of the standard bidirectional shortest + path implementation at networkx.algorithms.unweighted + + Parameters + ---------- + G : NetworkX graph + + source : node + starting node for path + + target : node + ending node for path + + ignore_nodes : container of nodes + nodes to ignore, optional + + ignore_edges : container of edges + edges to ignore, optional + + weight : None + This function accepts a weight argument for convenience of + shortest_simple_paths function. It will be ignored. + + Returns + ------- + path: list + List of nodes in a path from source to target. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + See Also + -------- + shortest_path + + """ + # call helper to do the real work + results = _bidirectional_pred_succ(G, source, target, ignore_nodes, ignore_edges) + pred, succ, w = results + + # build path from pred+w+succ + path = [] + # from w to target + while w is not None: + path.append(w) + w = succ[w] + # from source to w + w = pred[path[0]] + while w is not None: + path.insert(0, w) + w = pred[w] + + return len(path), path + + +def _bidirectional_pred_succ(G, source, target, ignore_nodes=None, ignore_edges=None): + """Bidirectional shortest path helper. + Returns (pred,succ,w) where + pred is a dictionary of predecessors from w to the source, and + succ is a dictionary of successors from w to the target. + """ + # does BFS from both source and target and meets in the middle + if ignore_nodes and (source in ignore_nodes or target in ignore_nodes): + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") + if target == source: + return ({target: None}, {source: None}, source) + + # handle either directed or undirected + if G.is_directed(): + Gpred = G.predecessors + Gsucc = G.successors + else: + Gpred = G.neighbors + Gsucc = G.neighbors + + # support optional nodes filter + if ignore_nodes: + + def filter_iter(nodes): + def iterate(v): + for w in nodes(v): + if w not in ignore_nodes: + yield w + + return iterate + + Gpred = filter_iter(Gpred) + Gsucc = filter_iter(Gsucc) + + # support optional edges filter + if ignore_edges: + if G.is_directed(): + + def filter_pred_iter(pred_iter): + def iterate(v): + for w in pred_iter(v): + if (w, v) not in ignore_edges: + yield w + + return iterate + + def filter_succ_iter(succ_iter): + def iterate(v): + for w in succ_iter(v): + if (v, w) not in ignore_edges: + yield w + + return iterate + + Gpred = filter_pred_iter(Gpred) + Gsucc = filter_succ_iter(Gsucc) + + else: + + def filter_iter(nodes): + def iterate(v): + for w in nodes(v): + if (v, w) not in ignore_edges and (w, v) not in ignore_edges: + yield w + + return iterate + + Gpred = filter_iter(Gpred) + Gsucc = filter_iter(Gsucc) + + # predecessor and successors in search + pred = {source: None} + succ = {target: None} + + # initialize fringes, start with forward + forward_fringe = [source] + reverse_fringe = [target] + + while forward_fringe and reverse_fringe: + if len(forward_fringe) <= len(reverse_fringe): + this_level = forward_fringe + forward_fringe = [] + for v in this_level: + for w in Gsucc(v): + if w not in pred: + forward_fringe.append(w) + pred[w] = v + if w in succ: + # found path + return pred, succ, w + else: + this_level = reverse_fringe + reverse_fringe = [] + for v in this_level: + for w in Gpred(v): + if w not in succ: + succ[w] = v + reverse_fringe.append(w) + if w in pred: + # found path + return pred, succ, w + + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") + + +def _bidirectional_dijkstra( + G, source, target, weight="weight", ignore_nodes=None, ignore_edges=None +): + """Dijkstra's algorithm for shortest paths using bidirectional search. + + This function returns the shortest path between source and target + ignoring nodes and edges in the containers ignore_nodes and + ignore_edges. + + This is a custom modification of the standard Dijkstra bidirectional + shortest path implementation at networkx.algorithms.weighted + + Parameters + ---------- + G : NetworkX graph + + source : node + Starting node. + + target : node + Ending node. + + weight: string, function, optional (default='weight') + Edge data key or weight function corresponding to the edge weight + If this is a function, the weight of an edge is the value + returned by the function. The function must accept exactly three + positional arguments: the two endpoints of an edge and the + dictionary of edge attributes for that edge. The function must + return a number or None to indicate a hidden edge. + + ignore_nodes : container of nodes + nodes to ignore, optional + + ignore_edges : container of edges + edges to ignore, optional + + Returns + ------- + length : number + Shortest path length. + + Returns a tuple of two dictionaries keyed by node. + The first dictionary stores distance from the source. + The second stores the path from the source to that node. + + Raises + ------ + NetworkXNoPath + If no path exists between source and target. + + Notes + ----- + Edge weight attributes must be numerical. + Distances are calculated as sums of weighted edges traversed. + + The weight function can be used to hide edges by returning None. + So ``weight = lambda u, v, d: 1 if d['color']=="red" else None`` + will find the shortest red path. + + In practice bidirectional Dijkstra is much more than twice as fast as + ordinary Dijkstra. + + Ordinary Dijkstra expands nodes in a sphere-like manner from the + source. The radius of this sphere will eventually be the length + of the shortest path. Bidirectional Dijkstra will expand nodes + from both the source and the target, making two spheres of half + this radius. Volume of the first sphere is pi*r*r while the + others are 2*pi*r/2*r/2, making up half the volume. + + This algorithm is not guaranteed to work if edge weights + are negative or are floating point numbers + (overflows and roundoff errors can cause problems). + + See Also + -------- + shortest_path + shortest_path_length + """ + if ignore_nodes and (source in ignore_nodes or target in ignore_nodes): + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") + if source == target: + if source not in G: + raise nx.NodeNotFound(f"Node {source} not in graph") + return (0, [source]) + + # handle either directed or undirected + if G.is_directed(): + Gpred = G.predecessors + Gsucc = G.successors + else: + Gpred = G.neighbors + Gsucc = G.neighbors + + # support optional nodes filter + if ignore_nodes: + + def filter_iter(nodes): + def iterate(v): + for w in nodes(v): + if w not in ignore_nodes: + yield w + + return iterate + + Gpred = filter_iter(Gpred) + Gsucc = filter_iter(Gsucc) + + # support optional edges filter + if ignore_edges: + if G.is_directed(): + + def filter_pred_iter(pred_iter): + def iterate(v): + for w in pred_iter(v): + if (w, v) not in ignore_edges: + yield w + + return iterate + + def filter_succ_iter(succ_iter): + def iterate(v): + for w in succ_iter(v): + if (v, w) not in ignore_edges: + yield w + + return iterate + + Gpred = filter_pred_iter(Gpred) + Gsucc = filter_succ_iter(Gsucc) + + else: + + def filter_iter(nodes): + def iterate(v): + for w in nodes(v): + if (v, w) not in ignore_edges and (w, v) not in ignore_edges: + yield w + + return iterate + + Gpred = filter_iter(Gpred) + Gsucc = filter_iter(Gsucc) + + wt = _weight_function(G, weight) + # Init: Forward Backward + dists = [{}, {}] # dictionary of final distances + paths = [{source: [source]}, {target: [target]}] # dictionary of paths + fringe = [[], []] # heap of (distance, node) tuples for + # extracting next node to expand + seen = [{source: 0}, {target: 0}] # dictionary of distances to + # nodes seen + c = count() + # initialize fringe heap + heappush(fringe[0], (0, next(c), source)) + heappush(fringe[1], (0, next(c), target)) + # neighs for extracting correct neighbor information + neighs = [Gsucc, Gpred] + # variables to hold shortest discovered path + # finaldist = 1e30000 + finalpath = [] + dir = 1 + while fringe[0] and fringe[1]: + # choose direction + # dir == 0 is forward direction and dir == 1 is back + dir = 1 - dir + # extract closest to expand + (dist, _, v) = heappop(fringe[dir]) + if v in dists[dir]: + # Shortest path to v has already been found + continue + # update distance + dists[dir][v] = dist # equal to seen[dir][v] + if v in dists[1 - dir]: + # if we have scanned v in both directions we are done + # we have now discovered the shortest path + return (finaldist, finalpath) + + for w in neighs[dir](v): + if dir == 0: # forward + minweight = wt(v, w, G.get_edge_data(v, w)) + else: # back, must remember to change v,w->w,v + minweight = wt(w, v, G.get_edge_data(w, v)) + if minweight is None: + continue + vwLength = dists[dir][v] + minweight + + if w in dists[dir]: + if vwLength < dists[dir][w]: + raise ValueError("Contradictory paths found: negative weights?") + elif w not in seen[dir] or vwLength < seen[dir][w]: + # relaxing + seen[dir][w] = vwLength + heappush(fringe[dir], (vwLength, next(c), w)) + paths[dir][w] = paths[dir][v] + [w] + if w in seen[0] and w in seen[1]: + # see if this path is better than the already + # discovered shortest path + totaldist = seen[0][w] + seen[1][w] + if finalpath == [] or finaldist > totaldist: + finaldist = totaldist + revpath = paths[1][w][:] + revpath.reverse() + finalpath = paths[0][w] + revpath[1:] + raise nx.NetworkXNoPath(f"No path between {source} and {target}.") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smallworld.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smallworld.py new file mode 100644 index 0000000000000000000000000000000000000000..456a4ca11c0aa19d1d770bf90e5713ce80e270d8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smallworld.py @@ -0,0 +1,404 @@ +"""Functions for estimating the small-world-ness of graphs. + +A small world network is characterized by a small average shortest path length, +and a large clustering coefficient. + +Small-worldness is commonly measured with the coefficient sigma or omega. + +Both coefficients compare the average clustering coefficient and shortest path +length of a given graph against the same quantities for an equivalent random +or lattice graph. + +For more information, see the Wikipedia article on small-world network [1]_. + +.. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network + +""" + +import networkx as nx +from networkx.utils import not_implemented_for, py_random_state + +__all__ = ["random_reference", "lattice_reference", "sigma", "omega"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@py_random_state(3) +@nx._dispatchable(returns_graph=True) +def random_reference(G, niter=1, connectivity=True, seed=None): + """Compute a random graph by swapping edges of a given graph. + + Parameters + ---------- + G : graph + An undirected graph with 4 or more nodes. + + niter : integer (optional, default=1) + An edge is rewired approximately `niter` times. + + connectivity : boolean (optional, default=True) + When True, ensure connectivity for the randomized graph. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : graph + The randomized graph. + + Raises + ------ + NetworkXError + If there are fewer than 4 nodes or 2 edges in `G` + + Notes + ----- + The implementation is adapted from the algorithm by Maslov and Sneppen + (2002) [1]_. + + References + ---------- + .. [1] Maslov, Sergei, and Kim Sneppen. + "Specificity and stability in topology of protein networks." + Science 296.5569 (2002): 910-913. + """ + if len(G) < 4: + raise nx.NetworkXError("Graph has fewer than four nodes.") + if len(G.edges) < 2: + raise nx.NetworkXError("Graph has fewer that 2 edges") + + from networkx.utils import cumulative_distribution, discrete_sequence + + local_conn = nx.connectivity.local_edge_connectivity + + G = G.copy() + keys, degrees = zip(*G.degree()) # keys, degree + cdf = cumulative_distribution(degrees) # cdf of degree + nnodes = len(G) + nedges = nx.number_of_edges(G) + niter = niter * nedges + ntries = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2)) + swapcount = 0 + + for i in range(niter): + n = 0 + while n < ntries: + # pick two random edges without creating edge list + # choose source node indices from discrete distribution + (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed) + if ai == ci: + continue # same source, skip + a = keys[ai] # convert index to label + c = keys[ci] + # choose target uniformly from neighbors + b = seed.choice(list(G.neighbors(a))) + d = seed.choice(list(G.neighbors(c))) + if b in [a, c, d] or d in [a, b, c]: + continue # all vertices should be different + + # don't create parallel edges + if (d not in G[a]) and (b not in G[c]): + G.add_edge(a, d) + G.add_edge(c, b) + G.remove_edge(a, b) + G.remove_edge(c, d) + + # Check if the graph is still connected + if connectivity and local_conn(G, a, b) == 0: + # Not connected, revert the swap + G.remove_edge(a, d) + G.remove_edge(c, b) + G.add_edge(a, b) + G.add_edge(c, d) + else: + swapcount += 1 + break + n += 1 + return G + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@py_random_state(4) +@nx._dispatchable(returns_graph=True) +def lattice_reference(G, niter=5, D=None, connectivity=True, seed=None): + """Latticize the given graph by swapping edges. + + Parameters + ---------- + G : graph + An undirected graph. + + niter : integer (optional, default=1) + An edge is rewired approximately niter times. + + D : numpy.array (optional, default=None) + Distance to the diagonal matrix. + + connectivity : boolean (optional, default=True) + Ensure connectivity for the latticized graph when set to True. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : graph + The latticized graph. + + Raises + ------ + NetworkXError + If there are fewer than 4 nodes or 2 edges in `G` + + Notes + ----- + The implementation is adapted from the algorithm by Sporns et al. [1]_. + which is inspired from the original work by Maslov and Sneppen(2002) [2]_. + + References + ---------- + .. [1] Sporns, Olaf, and Jonathan D. Zwi. + "The small world of the cerebral cortex." + Neuroinformatics 2.2 (2004): 145-162. + .. [2] Maslov, Sergei, and Kim Sneppen. + "Specificity and stability in topology of protein networks." + Science 296.5569 (2002): 910-913. + """ + import numpy as np + + from networkx.utils import cumulative_distribution, discrete_sequence + + local_conn = nx.connectivity.local_edge_connectivity + + if len(G) < 4: + raise nx.NetworkXError("Graph has fewer than four nodes.") + if len(G.edges) < 2: + raise nx.NetworkXError("Graph has fewer that 2 edges") + # Instead of choosing uniformly at random from a generated edge list, + # this algorithm chooses nonuniformly from the set of nodes with + # probability weighted by degree. + G = G.copy() + keys, degrees = zip(*G.degree()) # keys, degree + cdf = cumulative_distribution(degrees) # cdf of degree + + nnodes = len(G) + nedges = nx.number_of_edges(G) + if D is None: + D = np.zeros((nnodes, nnodes)) + un = np.arange(1, nnodes) + um = np.arange(nnodes - 1, 0, -1) + u = np.append((0,), np.where(un < um, un, um)) + + for v in range(int(np.ceil(nnodes / 2))): + D[nnodes - v - 1, :] = np.append(u[v + 1 :], u[: v + 1]) + D[v, :] = D[nnodes - v - 1, :][::-1] + + niter = niter * nedges + # maximal number of rewiring attempts per 'niter' + max_attempts = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2)) + + for _ in range(niter): + n = 0 + while n < max_attempts: + # pick two random edges without creating edge list + # choose source node indices from discrete distribution + (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed) + if ai == ci: + continue # same source, skip + a = keys[ai] # convert index to label + c = keys[ci] + # choose target uniformly from neighbors + b = seed.choice(list(G.neighbors(a))) + d = seed.choice(list(G.neighbors(c))) + bi = keys.index(b) + di = keys.index(d) + + if b in [a, c, d] or d in [a, b, c]: + continue # all vertices should be different + + # don't create parallel edges + if (d not in G[a]) and (b not in G[c]): + if D[ai, bi] + D[ci, di] >= D[ai, ci] + D[bi, di]: + # only swap if we get closer to the diagonal + G.add_edge(a, d) + G.add_edge(c, b) + G.remove_edge(a, b) + G.remove_edge(c, d) + + # Check if the graph is still connected + if connectivity and local_conn(G, a, b) == 0: + # Not connected, revert the swap + G.remove_edge(a, d) + G.remove_edge(c, b) + G.add_edge(a, b) + G.add_edge(c, d) + else: + break + n += 1 + + return G + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@py_random_state(3) +@nx._dispatchable +def sigma(G, niter=100, nrand=10, seed=None): + """Returns the small-world coefficient (sigma) of the given graph. + + The small-world coefficient is defined as: + sigma = C/Cr / L/Lr + where C and L are respectively the average clustering coefficient and + average shortest path length of G. Cr and Lr are respectively the average + clustering coefficient and average shortest path length of an equivalent + random graph. + + A graph is commonly classified as small-world if sigma>1. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + niter : integer (optional, default=100) + Approximate number of rewiring per edge to compute the equivalent + random graph. + nrand : integer (optional, default=10) + Number of random graphs generated to compute the average clustering + coefficient (Cr) and average shortest path length (Lr). + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + sigma : float + The small-world coefficient of G. + + Notes + ----- + The implementation is adapted from Humphries et al. [1]_ [2]_. + + References + ---------- + .. [1] The brainstem reticular formation is a small-world, not scale-free, + network M. D. Humphries, K. Gurney and T. J. Prescott, + Proc. Roy. Soc. B 2006 273, 503-511, doi:10.1098/rspb.2005.3354. + .. [2] Humphries and Gurney (2008). + "Network 'Small-World-Ness': A Quantitative Method for Determining + Canonical Network Equivalence". + PLoS One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051. + """ + import numpy as np + + # Compute the mean clustering coefficient and average shortest path length + # for an equivalent random graph + randMetrics = {"C": [], "L": []} + for i in range(nrand): + Gr = random_reference(G, niter=niter, seed=seed) + randMetrics["C"].append(nx.transitivity(Gr)) + randMetrics["L"].append(nx.average_shortest_path_length(Gr)) + + C = nx.transitivity(G) + L = nx.average_shortest_path_length(G) + Cr = np.mean(randMetrics["C"]) + Lr = np.mean(randMetrics["L"]) + + sigma = (C / Cr) / (L / Lr) + + return float(sigma) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@py_random_state(3) +@nx._dispatchable +def omega(G, niter=5, nrand=10, seed=None): + """Returns the small-world coefficient (omega) of a graph + + The small-world coefficient of a graph G is: + + omega = Lr/L - C/Cl + + where C and L are respectively the average clustering coefficient and + average shortest path length of G. Lr is the average shortest path length + of an equivalent random graph and Cl is the average clustering coefficient + of an equivalent lattice graph. + + The small-world coefficient (omega) measures how much G is like a lattice + or a random graph. Negative values mean G is similar to a lattice whereas + positive values mean G is a random graph. + Values close to 0 mean that G has small-world characteristics. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + niter: integer (optional, default=5) + Approximate number of rewiring per edge to compute the equivalent + random graph. + + nrand: integer (optional, default=10) + Number of random graphs generated to compute the maximal clustering + coefficient (Cr) and average shortest path length (Lr). + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + + Returns + ------- + omega : float + The small-world coefficient (omega) + + Notes + ----- + The implementation is adapted from the algorithm by Telesford et al. [1]_. + + References + ---------- + .. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011). + "The Ubiquity of Small-World Networks". + Brain Connectivity. 1 (0038): 367-75. PMC 3604768. PMID 22432451. + doi:10.1089/brain.2011.0038. + """ + import numpy as np + + # Compute the mean clustering coefficient and average shortest path length + # for an equivalent random graph + randMetrics = {"C": [], "L": []} + + # Calculate initial average clustering coefficient which potentially will + # get replaced by higher clustering coefficients from generated lattice + # reference graphs + Cl = nx.average_clustering(G) + + niter_lattice_reference = niter + niter_random_reference = niter * 2 + + for _ in range(nrand): + # Generate random graph + Gr = random_reference(G, niter=niter_random_reference, seed=seed) + randMetrics["L"].append(nx.average_shortest_path_length(Gr)) + + # Generate lattice graph + Gl = lattice_reference(G, niter=niter_lattice_reference, seed=seed) + + # Replace old clustering coefficient, if clustering is higher in + # generated lattice reference + Cl_temp = nx.average_clustering(Gl) + if Cl_temp > Cl: + Cl = Cl_temp + + C = nx.average_clustering(G) + L = nx.average_shortest_path_length(G) + Lr = np.mean(randMetrics["L"]) + + omega = (Lr / L) - (C / Cl) + + return float(omega) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smetric.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smetric.py new file mode 100644 index 0000000000000000000000000000000000000000..d985aa805b4fb21300680afe389aae4732793a73 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/smetric.py @@ -0,0 +1,30 @@ +import networkx as nx + +__all__ = ["s_metric"] + + +@nx._dispatchable +def s_metric(G): + """Returns the s-metric [1]_ of graph. + + The s-metric is defined as the sum of the products ``deg(u) * deg(v)`` + for every edge ``(u, v)`` in `G`. + + Parameters + ---------- + G : graph + The graph used to compute the s-metric. + + Returns + ------- + s : float + The s-metric of the graph. + + References + ---------- + .. [1] Lun Li, David Alderson, John C. Doyle, and Walter Willinger, + Towards a Theory of Scale-Free Graphs: + Definition, Properties, and Implications (Extended Version), 2005. + https://arxiv.org/abs/cond-mat/0501169 + """ + return float(sum(G.degree(u) * G.degree(v) for (u, v) in G.edges())) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/sparsifiers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/sparsifiers.py new file mode 100644 index 0000000000000000000000000000000000000000..59322372e6c1e06d595d8dff0f8680d1daa8a99e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/sparsifiers.py @@ -0,0 +1,296 @@ +"""Functions for computing sparsifiers of graphs.""" + +import math + +import networkx as nx +from networkx.utils import not_implemented_for, py_random_state + +__all__ = ["spanner"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@py_random_state(3) +@nx._dispatchable(edge_attrs="weight", returns_graph=True) +def spanner(G, stretch, weight=None, seed=None): + """Returns a spanner of the given graph with the given stretch. + + A spanner of a graph G = (V, E) with stretch t is a subgraph + H = (V, E_S) such that E_S is a subset of E and the distance between + any pair of nodes in H is at most t times the distance between the + nodes in G. + + Parameters + ---------- + G : NetworkX graph + An undirected simple graph. + + stretch : float + The stretch of the spanner. + + weight : object + The edge attribute to use as distance. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + NetworkX graph + A spanner of the given graph with the given stretch. + + Raises + ------ + ValueError + If a stretch less than 1 is given. + + Notes + ----- + This function implements the spanner algorithm by Baswana and Sen, + see [1]. + + This algorithm is a randomized las vegas algorithm: The expected + running time is O(km) where k = (stretch + 1) // 2 and m is the + number of edges in G. The returned graph is always a spanner of the + given graph with the specified stretch. For weighted graphs the + number of edges in the spanner is O(k * n^(1 + 1 / k)) where k is + defined as above and n is the number of nodes in G. For unweighted + graphs the number of edges is O(n^(1 + 1 / k) + kn). + + References + ---------- + [1] S. Baswana, S. Sen. A Simple and Linear Time Randomized + Algorithm for Computing Sparse Spanners in Weighted Graphs. + Random Struct. Algorithms 30(4): 532-563 (2007). + """ + if stretch < 1: + raise ValueError("stretch must be at least 1") + + k = (stretch + 1) // 2 + + # initialize spanner H with empty edge set + H = nx.empty_graph() + H.add_nodes_from(G.nodes) + + # phase 1: forming the clusters + # the residual graph has V' from the paper as its node set + # and E' from the paper as its edge set + residual_graph = _setup_residual_graph(G, weight) + # clustering is a dictionary that maps nodes in a cluster to the + # cluster center + clustering = {v: v for v in G.nodes} + sample_prob = math.pow(G.number_of_nodes(), -1 / k) + size_limit = 2 * math.pow(G.number_of_nodes(), 1 + 1 / k) + + i = 0 + while i < k - 1: + # step 1: sample centers + sampled_centers = set() + for center in set(clustering.values()): + if seed.random() < sample_prob: + sampled_centers.add(center) + + # combined loop for steps 2 and 3 + edges_to_add = set() + edges_to_remove = set() + new_clustering = {} + for v in residual_graph.nodes: + if clustering[v] in sampled_centers: + continue + + # step 2: find neighboring (sampled) clusters and + # lightest edges to them + lightest_edge_neighbor, lightest_edge_weight = _lightest_edge_dicts( + residual_graph, clustering, v + ) + neighboring_sampled_centers = ( + set(lightest_edge_weight.keys()) & sampled_centers + ) + + # step 3: add edges to spanner + if not neighboring_sampled_centers: + # connect to each neighboring center via lightest edge + for neighbor in lightest_edge_neighbor.values(): + edges_to_add.add((v, neighbor)) + # remove all incident edges + for neighbor in residual_graph.adj[v]: + edges_to_remove.add((v, neighbor)) + + else: # there is a neighboring sampled center + closest_center = min( + neighboring_sampled_centers, key=lightest_edge_weight.get + ) + closest_center_weight = lightest_edge_weight[closest_center] + closest_center_neighbor = lightest_edge_neighbor[closest_center] + + edges_to_add.add((v, closest_center_neighbor)) + new_clustering[v] = closest_center + + # connect to centers with edge weight less than + # closest_center_weight + for center, edge_weight in lightest_edge_weight.items(): + if edge_weight < closest_center_weight: + neighbor = lightest_edge_neighbor[center] + edges_to_add.add((v, neighbor)) + + # remove edges to centers with edge weight less than + # closest_center_weight + for neighbor in residual_graph.adj[v]: + nbr_cluster = clustering[neighbor] + nbr_weight = lightest_edge_weight[nbr_cluster] + if ( + nbr_cluster == closest_center + or nbr_weight < closest_center_weight + ): + edges_to_remove.add((v, neighbor)) + + # check whether iteration added too many edges to spanner, + # if so repeat + if len(edges_to_add) > size_limit: + # an iteration is repeated O(1) times on expectation + continue + + # iteration succeeded + i = i + 1 + + # actually add edges to spanner + for u, v in edges_to_add: + _add_edge_to_spanner(H, residual_graph, u, v, weight) + + # actually delete edges from residual graph + residual_graph.remove_edges_from(edges_to_remove) + + # copy old clustering data to new_clustering + for node, center in clustering.items(): + if center in sampled_centers: + new_clustering[node] = center + clustering = new_clustering + + # step 4: remove intra-cluster edges + for u in residual_graph.nodes: + for v in list(residual_graph.adj[u]): + if clustering[u] == clustering[v]: + residual_graph.remove_edge(u, v) + + # update residual graph node set + for v in list(residual_graph.nodes): + if v not in clustering: + residual_graph.remove_node(v) + + # phase 2: vertex-cluster joining + for v in residual_graph.nodes: + lightest_edge_neighbor, _ = _lightest_edge_dicts(residual_graph, clustering, v) + for neighbor in lightest_edge_neighbor.values(): + _add_edge_to_spanner(H, residual_graph, v, neighbor, weight) + + return H + + +def _setup_residual_graph(G, weight): + """Setup residual graph as a copy of G with unique edges weights. + + The node set of the residual graph corresponds to the set V' from + the Baswana-Sen paper and the edge set corresponds to the set E' + from the paper. + + This function associates distinct weights to the edges of the + residual graph (even for unweighted input graphs), as required by + the algorithm. + + Parameters + ---------- + G : NetworkX graph + An undirected simple graph. + + weight : object + The edge attribute to use as distance. + + Returns + ------- + NetworkX graph + The residual graph used for the Baswana-Sen algorithm. + """ + residual_graph = G.copy() + + # establish unique edge weights, even for unweighted graphs + for u, v in G.edges(): + if not weight: + residual_graph[u][v]["weight"] = (id(u), id(v)) + else: + residual_graph[u][v]["weight"] = (G[u][v][weight], id(u), id(v)) + + return residual_graph + + +def _lightest_edge_dicts(residual_graph, clustering, node): + """Find the lightest edge to each cluster. + + Searches for the minimum-weight edge to each cluster adjacent to + the given node. + + Parameters + ---------- + residual_graph : NetworkX graph + The residual graph used by the Baswana-Sen algorithm. + + clustering : dictionary + The current clustering of the nodes. + + node : node + The node from which the search originates. + + Returns + ------- + lightest_edge_neighbor, lightest_edge_weight : dictionary, dictionary + lightest_edge_neighbor is a dictionary that maps a center C to + a node v in the corresponding cluster such that the edge from + the given node to v is the lightest edge from the given node to + any node in cluster. lightest_edge_weight maps a center C to the + weight of the aforementioned edge. + + Notes + ----- + If a cluster has no node that is adjacent to the given node in the + residual graph then the center of the cluster is not a key in the + returned dictionaries. + """ + lightest_edge_neighbor = {} + lightest_edge_weight = {} + for neighbor in residual_graph.adj[node]: + nbr_center = clustering[neighbor] + weight = residual_graph[node][neighbor]["weight"] + if ( + nbr_center not in lightest_edge_weight + or weight < lightest_edge_weight[nbr_center] + ): + lightest_edge_neighbor[nbr_center] = neighbor + lightest_edge_weight[nbr_center] = weight + return lightest_edge_neighbor, lightest_edge_weight + + +def _add_edge_to_spanner(H, residual_graph, u, v, weight): + """Add the edge {u, v} to the spanner H and take weight from + the residual graph. + + Parameters + ---------- + H : NetworkX graph + The spanner under construction. + + residual_graph : NetworkX graph + The residual graph used by the Baswana-Sen algorithm. The weight + for the edge is taken from this graph. + + u : node + One endpoint of the edge. + + v : node + The other endpoint of the edge. + + weight : object + The edge attribute to use as distance. + """ + H.add_edge(u, v) + if weight: + H[u][v][weight] = residual_graph[u][v]["weight"][0] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/structuralholes.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/structuralholes.py new file mode 100644 index 0000000000000000000000000000000000000000..f43c58c5fd91f13ec10c5adba9c20decb26b708b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/structuralholes.py @@ -0,0 +1,374 @@ +"""Functions for computing measures of structural holes.""" + +import networkx as nx + +__all__ = ["constraint", "local_constraint", "effective_size"] + + +@nx._dispatchable(edge_attrs="weight") +def mutual_weight(G, u, v, weight=None): + """Returns the sum of the weights of the edge from `u` to `v` and + the edge from `v` to `u` in `G`. + + `weight` is the edge data key that represents the edge weight. If + the specified key is `None` or is not in the edge data for an edge, + that edge is assumed to have weight 1. + + Pre-conditions: `u` and `v` must both be in `G`. + + """ + try: + a_uv = G[u][v].get(weight, 1) + except KeyError: + a_uv = 0 + try: + a_vu = G[v][u].get(weight, 1) + except KeyError: + a_vu = 0 + return a_uv + a_vu + + +@nx._dispatchable(edge_attrs="weight") +def normalized_mutual_weight(G, u, v, norm=sum, weight=None): + """Returns normalized mutual weight of the edges from `u` to `v` + with respect to the mutual weights of the neighbors of `u` in `G`. + + `norm` specifies how the normalization factor is computed. It must + be a function that takes a single argument and returns a number. + The argument will be an iterable of mutual weights + of pairs ``(u, w)``, where ``w`` ranges over each (in- and + out-)neighbor of ``u``. Commons values for `normalization` are + ``sum`` and ``max``. + + `weight` can be ``None`` or a string, if None, all edge weights + are considered equal. Otherwise holds the name of the edge + attribute used as weight. + + """ + scale = norm(mutual_weight(G, u, w, weight) for w in set(nx.all_neighbors(G, u))) + return 0 if scale == 0 else mutual_weight(G, u, v, weight) / scale + + +@nx._dispatchable(edge_attrs="weight") +def effective_size(G, nodes=None, weight=None): + r"""Returns the effective size of all nodes in the graph ``G``. + + The *effective size* of a node's ego network is based on the concept + of redundancy. A person's ego network has redundancy to the extent + that her contacts are connected to each other as well. The + nonredundant part of a person's relationships is the effective + size of her ego network [1]_. Formally, the effective size of a + node $u$, denoted $e(u)$, is defined by + + .. math:: + + e(u) = \sum_{v \in N(u) \setminus \{u\}} + \left(1 - \sum_{w \in N(v)} p_{uw} m_{vw}\right) + + where $N(u)$ is the set of neighbors of $u$ and $p_{uw}$ is the + normalized mutual weight of the (directed or undirected) edges + joining $u$ and $v$, for each vertex $u$ and $v$ [1]_. And $m_{vw}$ + is the mutual weight of $v$ and $w$ divided by $v$ highest mutual + weight with any of its neighbors. The *mutual weight* of $u$ and $v$ + is the sum of the weights of edges joining them (edge weights are + assumed to be one if the graph is unweighted). + + For the case of unweighted and undirected graphs, Borgatti proposed + a simplified formula to compute effective size [2]_ + + .. math:: + + e(u) = n - \frac{2t}{n} + + where `t` is the number of ties in the ego network (not including + ties to ego) and `n` is the number of nodes (excluding ego). + + Parameters + ---------- + G : NetworkX graph + The graph containing ``v``. Directed graphs are treated like + undirected graphs when computing neighbors of ``v``. + + nodes : container, optional + Container of nodes in the graph ``G`` to compute the effective size. + If None, the effective size of every node is computed. + + weight : None or string, optional + If None, all edge weights are considered equal. + Otherwise holds the name of the edge attribute used as weight. + + Returns + ------- + dict + Dictionary with nodes as keys and the effective size of the node as values. + + Notes + ----- + Isolated nodes, including nodes which only have self-loop edges, do not + have a well-defined effective size:: + + >>> G = nx.path_graph(3) + >>> G.add_edge(4, 4) + >>> nx.effective_size(G) + {0: 1.0, 1: 2.0, 2: 1.0, 4: nan} + + Burt also defined the related concept of *efficiency* of a node's ego + network, which is its effective size divided by the degree of that + node [1]_. So you can easily compute efficiency: + + >>> G = nx.DiGraph() + >>> G.add_edges_from([(0, 1), (0, 2), (1, 0), (2, 1)]) + >>> esize = nx.effective_size(G) + >>> efficiency = {n: v / G.degree(n) for n, v in esize.items()} + + See also + -------- + constraint + + References + ---------- + .. [1] Burt, Ronald S. + *Structural Holes: The Social Structure of Competition.* + Cambridge: Harvard University Press, 1995. + + .. [2] Borgatti, S. + "Structural Holes: Unpacking Burt's Redundancy Measures" + CONNECTIONS 20(1):35-38. + http://www.analytictech.com/connections/v20(1)/holes.htm + + """ + + def redundancy(G, u, v, weight=None): + nmw = normalized_mutual_weight + r = sum( + nmw(G, u, w, weight=weight) * nmw(G, v, w, norm=max, weight=weight) + for w in set(nx.all_neighbors(G, u)) + ) + return 1 - r + + # Check if scipy is available + try: + # Needed for errstate + import numpy as np + + # make sure nx.adjacency_matrix will not raise + import scipy as sp + + has_scipy = True + except: + has_scipy = False + + if nodes is None and has_scipy: + # In order to compute constraint of all nodes, + # algorithms based on sparse matrices can be much faster + + # Obtain the adjacency matrix + P = nx.adjacency_matrix(G, weight=weight) + + # Calculate mutual weights + mutual_weights1 = P + P.T + mutual_weights2 = mutual_weights1.copy() + + with np.errstate(divide="ignore"): + # Mutual_weights1 = Normalize mutual weights by row sums + mutual_weights1 /= mutual_weights1.sum(axis=1)[:, np.newaxis] + + # Mutual_weights2 = Normalize mutual weights by row max + mutual_weights2 /= mutual_weights2.max(axis=1).toarray() + + # Calculate effective sizes + r = 1 - (mutual_weights1 @ mutual_weights2.T).toarray() + effective_size = ((mutual_weights1 > 0) * r).sum(axis=1) + + # Special treatment: isolated nodes (ignoring selfloops) marked with "nan" + sum_mutual_weights = mutual_weights1.sum(axis=1) - mutual_weights1.diagonal() + isolated_nodes = sum_mutual_weights == 0 + effective_size[isolated_nodes] = float("nan") + # Use tolist() to automatically convert numpy scalars -> Python scalars + return dict(zip(G, effective_size.tolist())) + + # Results for only requested nodes + effective_size = {} + if nodes is None: + nodes = G + # Use Borgatti's simplified formula for unweighted and undirected graphs + if not G.is_directed() and weight is None: + for v in nodes: + # Effective size is not defined for isolated nodes, including nodes + # with only self-edges + if all(u == v for u in G[v]): + effective_size[v] = float("nan") + continue + E = nx.ego_graph(G, v, center=False, undirected=True) + effective_size[v] = len(E) - (2 * E.size()) / len(E) + else: + for v in nodes: + # Effective size is not defined for isolated nodes, including nodes + # with only self-edges + if all(u == v for u in G[v]): + effective_size[v] = float("nan") + continue + effective_size[v] = sum( + redundancy(G, v, u, weight) for u in set(nx.all_neighbors(G, v)) + ) + return effective_size + + +@nx._dispatchable(edge_attrs="weight") +def constraint(G, nodes=None, weight=None): + r"""Returns the constraint on all nodes in the graph ``G``. + + The *constraint* is a measure of the extent to which a node *v* is + invested in those nodes that are themselves invested in the + neighbors of *v*. Formally, the *constraint on v*, denoted `c(v)`, + is defined by + + .. math:: + + c(v) = \sum_{w \in N(v) \setminus \{v\}} \ell(v, w) + + where $N(v)$ is the subset of the neighbors of `v` that are either + predecessors or successors of `v` and $\ell(v, w)$ is the local + constraint on `v` with respect to `w` [1]_. For the definition of local + constraint, see :func:`local_constraint`. + + Parameters + ---------- + G : NetworkX graph + The graph containing ``v``. This can be either directed or undirected. + + nodes : container, optional + Container of nodes in the graph ``G`` to compute the constraint. If + None, the constraint of every node is computed. + + weight : None or string, optional + If None, all edge weights are considered equal. + Otherwise holds the name of the edge attribute used as weight. + + Returns + ------- + dict + Dictionary with nodes as keys and the constraint on the node as values. + + See also + -------- + local_constraint + + References + ---------- + .. [1] Burt, Ronald S. + "Structural holes and good ideas". + American Journal of Sociology (110): 349–399. + + """ + + # Check if scipy is available + try: + # Needed for errstate + import numpy as np + + # make sure nx.adjacency_matrix will not raise + import scipy as sp + + has_scipy = True + except: + has_scipy = False + + if nodes is None and has_scipy: + # In order to compute constraint of all nodes, + # algorithms based on sparse matrices can be much faster + + # Obtain the adjacency matrix + P = nx.adjacency_matrix(G, weight=weight) + + # Calculate mutual weights + mutual_weights = P + P.T + + # Normalize mutual weights by row sums + sum_mutual_weights = mutual_weights.sum(axis=1) + with np.errstate(divide="ignore"): + mutual_weights /= sum_mutual_weights[:, np.newaxis] + + # Calculate local constraints and constraints + local_constraints = (mutual_weights + mutual_weights @ mutual_weights) ** 2 + constraints = ((mutual_weights > 0) * local_constraints).sum(axis=1) + + # Special treatment: isolated nodes marked with "nan" + isolated_nodes = sum_mutual_weights - 2 * mutual_weights.diagonal() == 0 + constraints[isolated_nodes] = float("nan") + # Use tolist() to automatically convert numpy scalars -> Python scalars + return dict(zip(G, constraints.tolist())) + + # Result for only requested nodes + constraint = {} + if nodes is None: + nodes = G + for v in nodes: + # Constraint is not defined for isolated nodes + if len(G[v]) == 0: + constraint[v] = float("nan") + continue + constraint[v] = sum( + local_constraint(G, v, n, weight) for n in set(nx.all_neighbors(G, v)) + ) + return constraint + + +@nx._dispatchable(edge_attrs="weight") +def local_constraint(G, u, v, weight=None): + r"""Returns the local constraint on the node ``u`` with respect to + the node ``v`` in the graph ``G``. + + Formally, the *local constraint on u with respect to v*, denoted + $\ell(u, v)$, is defined by + + .. math:: + + \ell(u, v) = \left(p_{uv} + \sum_{w \in N(v)} p_{uw} p_{wv}\right)^2, + + where $N(v)$ is the set of neighbors of $v$ and $p_{uv}$ is the + normalized mutual weight of the (directed or undirected) edges + joining $u$ and $v$, for each vertex $u$ and $v$ [1]_. The *mutual + weight* of $u$ and $v$ is the sum of the weights of edges joining + them (edge weights are assumed to be one if the graph is + unweighted). + + Parameters + ---------- + G : NetworkX graph + The graph containing ``u`` and ``v``. This can be either + directed or undirected. + + u : node + A node in the graph ``G``. + + v : node + A node in the graph ``G``. + + weight : None or string, optional + If None, all edge weights are considered equal. + Otherwise holds the name of the edge attribute used as weight. + + Returns + ------- + float + The constraint of the node ``v`` in the graph ``G``. + + See also + -------- + constraint + + References + ---------- + .. [1] Burt, Ronald S. + "Structural holes and good ideas". + American Journal of Sociology (110): 349–399. + + """ + nmw = normalized_mutual_weight + direct = nmw(G, u, v, weight=weight) + indirect = sum( + nmw(G, u, w, weight=weight) * nmw(G, w, v, weight=weight) + for w in set(nx.all_neighbors(G, u)) + ) + return (direct + indirect) ** 2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/summarization.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/summarization.py new file mode 100644 index 0000000000000000000000000000000000000000..23db8da4efffa7dcbabfb75e031187d1b2b190dc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/summarization.py @@ -0,0 +1,564 @@ +""" +Graph summarization finds smaller representations of graphs resulting in faster +runtime of algorithms, reduced storage needs, and noise reduction. +Summarization has applications in areas such as visualization, pattern mining, +clustering and community detection, and more. Core graph summarization +techniques are grouping/aggregation, bit-compression, +simplification/sparsification, and influence based. Graph summarization +algorithms often produce either summary graphs in the form of supergraphs or +sparsified graphs, or a list of independent structures. Supergraphs are the +most common product, which consist of supernodes and original nodes and are +connected by edges and superedges, which represent aggregate edges between +nodes and supernodes. + +Grouping/aggregation based techniques compress graphs by representing +close/connected nodes and edges in a graph by a single node/edge in a +supergraph. Nodes can be grouped together into supernodes based on their +structural similarities or proximity within a graph to reduce the total number +of nodes in a graph. Edge-grouping techniques group edges into lossy/lossless +nodes called compressor or virtual nodes to reduce the total number of edges in +a graph. Edge-grouping techniques can be lossless, meaning that they can be +used to re-create the original graph, or techniques can be lossy, requiring +less space to store the summary graph, but at the expense of lower +reconstruction accuracy of the original graph. + +Bit-compression techniques minimize the amount of information needed to +describe the original graph, while revealing structural patterns in the +original graph. The two-part minimum description length (MDL) is often used to +represent the model and the original graph in terms of the model. A key +difference between graph compression and graph summarization is that graph +summarization focuses on finding structural patterns within the original graph, +whereas graph compression focuses on compressions the original graph to be as +small as possible. **NOTE**: Some bit-compression methods exist solely to +compress a graph without creating a summary graph or finding comprehensible +structural patterns. + +Simplification/Sparsification techniques attempt to create a sparse +representation of a graph by removing unimportant nodes and edges from the +graph. Sparsified graphs differ from supergraphs created by +grouping/aggregation by only containing a subset of the original nodes and +edges of the original graph. + +Influence based techniques aim to find a high-level description of influence +propagation in a large graph. These methods are scarce and have been mostly +applied to social graphs. + +*dedensification* is a grouping/aggregation based technique to compress the +neighborhoods around high-degree nodes in unweighted graphs by adding +compressor nodes that summarize multiple edges of the same type to +high-degree nodes (nodes with a degree greater than a given threshold). +Dedensification was developed for the purpose of increasing performance of +query processing around high-degree nodes in graph databases and enables direct +operations on the compressed graph. The structural patterns surrounding +high-degree nodes in the original is preserved while using fewer edges and +adding a small number of compressor nodes. The degree of nodes present in the +original graph is also preserved. The current implementation of dedensification +supports graphs with one edge type. + +For more information on graph summarization, see `Graph Summarization Methods +and Applications: A Survey `_ +""" + +from collections import Counter, defaultdict + +import networkx as nx + +__all__ = ["dedensify", "snap_aggregation"] + + +@nx._dispatchable(mutates_input={"not copy": 3}, returns_graph=True) +def dedensify(G, threshold, prefix=None, copy=True): + """Compresses neighborhoods around high-degree nodes + + Reduces the number of edges to high-degree nodes by adding compressor nodes + that summarize multiple edges of the same type to high-degree nodes (nodes + with a degree greater than a given threshold). Dedensification also has + the added benefit of reducing the number of edges around high-degree nodes. + The implementation currently supports graphs with a single edge type. + + Parameters + ---------- + G: graph + A networkx graph + threshold: int + Minimum degree threshold of a node to be considered a high degree node. + The threshold must be greater than or equal to 2. + prefix: str or None, optional (default: None) + An optional prefix for denoting compressor nodes + copy: bool, optional (default: True) + Indicates if dedensification should be done inplace + + Returns + ------- + dedensified networkx graph : (graph, set) + 2-tuple of the dedensified graph and set of compressor nodes + + Notes + ----- + According to the algorithm in [1]_, removes edges in a graph by + compressing/decompressing the neighborhoods around high degree nodes by + adding compressor nodes that summarize multiple edges of the same type + to high-degree nodes. Dedensification will only add a compressor node when + doing so will reduce the total number of edges in the given graph. This + implementation currently supports graphs with a single edge type. + + Examples + -------- + Dedensification will only add compressor nodes when doing so would result + in fewer edges:: + + >>> original_graph = nx.DiGraph() + >>> original_graph.add_nodes_from( + ... ["1", "2", "3", "4", "5", "6", "A", "B", "C"] + ... ) + >>> original_graph.add_edges_from( + ... [ + ... ("1", "C"), ("1", "B"), + ... ("2", "C"), ("2", "B"), ("2", "A"), + ... ("3", "B"), ("3", "A"), ("3", "6"), + ... ("4", "C"), ("4", "B"), ("4", "A"), + ... ("5", "B"), ("5", "A"), + ... ("6", "5"), + ... ("A", "6") + ... ] + ... ) + >>> c_graph, c_nodes = nx.dedensify(original_graph, threshold=2) + >>> original_graph.number_of_edges() + 15 + >>> c_graph.number_of_edges() + 14 + + A dedensified, directed graph can be "densified" to reconstruct the + original graph:: + + >>> original_graph = nx.DiGraph() + >>> original_graph.add_nodes_from( + ... ["1", "2", "3", "4", "5", "6", "A", "B", "C"] + ... ) + >>> original_graph.add_edges_from( + ... [ + ... ("1", "C"), ("1", "B"), + ... ("2", "C"), ("2", "B"), ("2", "A"), + ... ("3", "B"), ("3", "A"), ("3", "6"), + ... ("4", "C"), ("4", "B"), ("4", "A"), + ... ("5", "B"), ("5", "A"), + ... ("6", "5"), + ... ("A", "6") + ... ] + ... ) + >>> c_graph, c_nodes = nx.dedensify(original_graph, threshold=2) + >>> # re-densifies the compressed graph into the original graph + >>> for c_node in c_nodes: + ... all_neighbors = set(nx.all_neighbors(c_graph, c_node)) + ... out_neighbors = set(c_graph.neighbors(c_node)) + ... for out_neighbor in out_neighbors: + ... c_graph.remove_edge(c_node, out_neighbor) + ... in_neighbors = all_neighbors - out_neighbors + ... for in_neighbor in in_neighbors: + ... c_graph.remove_edge(in_neighbor, c_node) + ... for out_neighbor in out_neighbors: + ... c_graph.add_edge(in_neighbor, out_neighbor) + ... c_graph.remove_node(c_node) + ... + >>> nx.is_isomorphic(original_graph, c_graph) + True + + References + ---------- + .. [1] Maccioni, A., & Abadi, D. J. (2016, August). + Scalable pattern matching over compressed graphs via dedensification. + In Proceedings of the 22nd ACM SIGKDD International Conference on + Knowledge Discovery and Data Mining (pp. 1755-1764). + http://www.cs.umd.edu/~abadi/papers/graph-dedense.pdf + """ + if threshold < 2: + raise nx.NetworkXError("The degree threshold must be >= 2") + + degrees = G.in_degree if G.is_directed() else G.degree + # Group nodes based on degree threshold + high_degree_nodes = {n for n, d in degrees if d > threshold} + low_degree_nodes = G.nodes() - high_degree_nodes + + auxiliary = {} + for node in G: + high_degree_nbrs = frozenset(high_degree_nodes & set(G[node])) + if high_degree_nbrs: + if high_degree_nbrs in auxiliary: + auxiliary[high_degree_nbrs].add(node) + else: + auxiliary[high_degree_nbrs] = {node} + + if copy: + G = G.copy() + + compressor_nodes = set() + for index, (high_degree_nodes, low_degree_nodes) in enumerate(auxiliary.items()): + low_degree_node_count = len(low_degree_nodes) + high_degree_node_count = len(high_degree_nodes) + old_edges = high_degree_node_count * low_degree_node_count + new_edges = high_degree_node_count + low_degree_node_count + if old_edges <= new_edges: + continue + compression_node = "".join(str(node) for node in high_degree_nodes) + if prefix: + compression_node = str(prefix) + compression_node + for node in low_degree_nodes: + for high_node in high_degree_nodes: + if G.has_edge(node, high_node): + G.remove_edge(node, high_node) + + G.add_edge(node, compression_node) + for node in high_degree_nodes: + G.add_edge(compression_node, node) + compressor_nodes.add(compression_node) + return G, compressor_nodes + + +def _snap_build_graph( + G, + groups, + node_attributes, + edge_attributes, + neighbor_info, + edge_types, + prefix, + supernode_attribute, + superedge_attribute, +): + """ + Build the summary graph from the data structures produced in the SNAP aggregation algorithm + + Used in the SNAP aggregation algorithm to build the output summary graph and supernode + lookup dictionary. This process uses the original graph and the data structures to + create the supernodes with the correct node attributes, and the superedges with the correct + edge attributes + + Parameters + ---------- + G: networkx.Graph + the original graph to be summarized + groups: dict + A dictionary of unique group IDs and their corresponding node groups + node_attributes: iterable + An iterable of the node attributes considered in the summarization process + edge_attributes: iterable + An iterable of the edge attributes considered in the summarization process + neighbor_info: dict + A data structure indicating the number of edges a node has with the + groups in the current summarization of each edge type + edge_types: dict + dictionary of edges in the graph and their corresponding attributes recognized + in the summarization + prefix: string + The prefix to be added to all supernodes + supernode_attribute: str + The node attribute for recording the supernode groupings of nodes + superedge_attribute: str + The edge attribute for recording the edge types represented by superedges + + Returns + ------- + summary graph: Networkx graph + """ + output = G.__class__() + node_label_lookup = {} + for index, group_id in enumerate(groups): + group_set = groups[group_id] + supernode = f"{prefix}{index}" + node_label_lookup[group_id] = supernode + supernode_attributes = { + attr: G.nodes[next(iter(group_set))][attr] for attr in node_attributes + } + supernode_attributes[supernode_attribute] = group_set + output.add_node(supernode, **supernode_attributes) + + for group_id in groups: + group_set = groups[group_id] + source_supernode = node_label_lookup[group_id] + for other_group, group_edge_types in neighbor_info[ + next(iter(group_set)) + ].items(): + if group_edge_types: + target_supernode = node_label_lookup[other_group] + summary_graph_edge = (source_supernode, target_supernode) + + edge_types = [ + dict(zip(edge_attributes, edge_type)) + for edge_type in group_edge_types + ] + + has_edge = output.has_edge(*summary_graph_edge) + if output.is_multigraph(): + if not has_edge: + for edge_type in edge_types: + output.add_edge(*summary_graph_edge, **edge_type) + elif not output.is_directed(): + existing_edge_data = output.get_edge_data(*summary_graph_edge) + for edge_type in edge_types: + if edge_type not in existing_edge_data.values(): + output.add_edge(*summary_graph_edge, **edge_type) + else: + superedge_attributes = {superedge_attribute: edge_types} + output.add_edge(*summary_graph_edge, **superedge_attributes) + + return output + + +def _snap_eligible_group(G, groups, group_lookup, edge_types): + """ + Determines if a group is eligible to be split. + + A group is eligible to be split if all nodes in the group have edges of the same type(s) + with the same other groups. + + Parameters + ---------- + G: graph + graph to be summarized + groups: dict + A dictionary of unique group IDs and their corresponding node groups + group_lookup: dict + dictionary of nodes and their current corresponding group ID + edge_types: dict + dictionary of edges in the graph and their corresponding attributes recognized + in the summarization + + Returns + ------- + tuple: group ID to split, and neighbor-groups participation_counts data structure + """ + nbr_info = {node: {gid: Counter() for gid in groups} for node in group_lookup} + for group_id in groups: + current_group = groups[group_id] + + # build nbr_info for nodes in group + for node in current_group: + nbr_info[node] = {group_id: Counter() for group_id in groups} + edges = G.edges(node, keys=True) if G.is_multigraph() else G.edges(node) + for edge in edges: + neighbor = edge[1] + edge_type = edge_types[edge] + neighbor_group_id = group_lookup[neighbor] + nbr_info[node][neighbor_group_id][edge_type] += 1 + + # check if group_id is eligible to be split + group_size = len(current_group) + for other_group_id in groups: + edge_counts = Counter() + for node in current_group: + edge_counts.update(nbr_info[node][other_group_id].keys()) + + if not all(count == group_size for count in edge_counts.values()): + # only the nbr_info of the returned group_id is required for handling group splits + return group_id, nbr_info + + # if no eligible groups, complete nbr_info is calculated + return None, nbr_info + + +def _snap_split(groups, neighbor_info, group_lookup, group_id): + """ + Splits a group based on edge types and updates the groups accordingly + + Splits the group with the given group_id based on the edge types + of the nodes so that each new grouping will all have the same + edges with other nodes. + + Parameters + ---------- + groups: dict + A dictionary of unique group IDs and their corresponding node groups + neighbor_info: dict + A data structure indicating the number of edges a node has with the + groups in the current summarization of each edge type + edge_types: dict + dictionary of edges in the graph and their corresponding attributes recognized + in the summarization + group_lookup: dict + dictionary of nodes and their current corresponding group ID + group_id: object + ID of group to be split + + Returns + ------- + dict + The updated groups based on the split + """ + new_group_mappings = defaultdict(set) + for node in groups[group_id]: + signature = tuple( + frozenset(edge_types) for edge_types in neighbor_info[node].values() + ) + new_group_mappings[signature].add(node) + + # leave the biggest new_group as the original group + new_groups = sorted(new_group_mappings.values(), key=len) + for new_group in new_groups[:-1]: + # Assign unused integer as the new_group_id + # ids are tuples, so will not interact with the original group_ids + new_group_id = len(groups) + groups[new_group_id] = new_group + groups[group_id] -= new_group + for node in new_group: + group_lookup[node] = new_group_id + + return groups + + +@nx._dispatchable( + node_attrs="[node_attributes]", edge_attrs="[edge_attributes]", returns_graph=True +) +def snap_aggregation( + G, + node_attributes, + edge_attributes=(), + prefix="Supernode-", + supernode_attribute="group", + superedge_attribute="types", +): + """Creates a summary graph based on attributes and connectivity. + + This function uses the Summarization by Grouping Nodes on Attributes + and Pairwise edges (SNAP) algorithm for summarizing a given + graph by grouping nodes by node attributes and their edge attributes + into supernodes in a summary graph. This name SNAP should not be + confused with the Stanford Network Analysis Project (SNAP). + + Here is a high-level view of how this algorithm works: + + 1) Group nodes by node attribute values. + + 2) Iteratively split groups until all nodes in each group have edges + to nodes in the same groups. That is, until all the groups are homogeneous + in their member nodes' edges to other groups. For example, + if all the nodes in group A only have edge to nodes in group B, then the + group is homogeneous and does not need to be split. If all nodes in group B + have edges with nodes in groups {A, C}, but some also have edges with other + nodes in B, then group B is not homogeneous and needs to be split into + groups have edges with {A, C} and a group of nodes having + edges with {A, B, C}. This way, viewers of the summary graph can + assume that all nodes in the group have the exact same node attributes and + the exact same edges. + + 3) Build the output summary graph, where the groups are represented by + super-nodes. Edges represent the edges shared between all the nodes in each + respective groups. + + A SNAP summary graph can be used to visualize graphs that are too large to display + or visually analyze, or to efficiently identify sets of similar nodes with similar connectivity + patterns to other sets of similar nodes based on specified node and/or edge attributes in a graph. + + Parameters + ---------- + G: graph + Networkx Graph to be summarized + node_attributes: iterable, required + An iterable of the node attributes used to group nodes in the summarization process. Nodes + with the same values for these attributes will be grouped together in the summary graph. + edge_attributes: iterable, optional + An iterable of the edge attributes considered in the summarization process. If provided, unique + combinations of the attribute values found in the graph are used to + determine the edge types in the graph. If not provided, all edges + are considered to be of the same type. + prefix: str + The prefix used to denote supernodes in the summary graph. Defaults to 'Supernode-'. + supernode_attribute: str + The node attribute for recording the supernode groupings of nodes. Defaults to 'group'. + superedge_attribute: str + The edge attribute for recording the edge types of multiple edges. Defaults to 'types'. + + Returns + ------- + networkx.Graph: summary graph + + Examples + -------- + SNAP aggregation takes a graph and summarizes it in the context of user-provided + node and edge attributes such that a viewer can more easily extract and + analyze the information represented by the graph + + >>> nodes = { + ... "A": dict(color="Red"), + ... "B": dict(color="Red"), + ... "C": dict(color="Red"), + ... "D": dict(color="Red"), + ... "E": dict(color="Blue"), + ... "F": dict(color="Blue"), + ... } + >>> edges = [ + ... ("A", "E", "Strong"), + ... ("B", "F", "Strong"), + ... ("C", "E", "Weak"), + ... ("D", "F", "Weak"), + ... ] + >>> G = nx.Graph() + >>> for node in nodes: + ... attributes = nodes[node] + ... G.add_node(node, **attributes) + >>> for source, target, type in edges: + ... G.add_edge(source, target, type=type) + >>> node_attributes = ("color",) + >>> edge_attributes = ("type",) + >>> summary_graph = nx.snap_aggregation( + ... G, node_attributes=node_attributes, edge_attributes=edge_attributes + ... ) + + Notes + ----- + The summary graph produced is called a maximum Attribute-edge + compatible (AR-compatible) grouping. According to [1]_, an + AR-compatible grouping means that all nodes in each group have the same + exact node attribute values and the same exact edges and + edge types to one or more nodes in the same groups. The maximal + AR-compatible grouping is the grouping with the minimal cardinality. + + The AR-compatible grouping is the most detailed grouping provided by + any of the SNAP algorithms. + + References + ---------- + .. [1] Y. Tian, R. A. Hankins, and J. M. Patel. Efficient aggregation + for graph summarization. In Proc. 2008 ACM-SIGMOD Int. Conf. + Management of Data (SIGMOD’08), pages 567–580, Vancouver, Canada, + June 2008. + """ + edge_types = { + edge: tuple(attrs.get(attr) for attr in edge_attributes) + for edge, attrs in G.edges.items() + } + if not G.is_directed(): + if G.is_multigraph(): + # list is needed to avoid mutating while iterating + edges = [((v, u, k), etype) for (u, v, k), etype in edge_types.items()] + else: + # list is needed to avoid mutating while iterating + edges = [((v, u), etype) for (u, v), etype in edge_types.items()] + edge_types.update(edges) + + group_lookup = { + node: tuple(attrs[attr] for attr in node_attributes) + for node, attrs in G.nodes.items() + } + groups = defaultdict(set) + for node, node_type in group_lookup.items(): + groups[node_type].add(node) + + eligible_group_id, nbr_info = _snap_eligible_group( + G, groups, group_lookup, edge_types + ) + while eligible_group_id: + groups = _snap_split(groups, nbr_info, group_lookup, eligible_group_id) + eligible_group_id, nbr_info = _snap_eligible_group( + G, groups, group_lookup, edge_types + ) + return _snap_build_graph( + G, + groups, + node_attributes, + edge_attributes, + nbr_info, + edge_types, + prefix, + supernode_attribute, + superedge_attribute, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/swap.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/swap.py new file mode 100644 index 0000000000000000000000000000000000000000..cb3cc1c0e75c375ae49976e21fcccf2dc6c76231 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/swap.py @@ -0,0 +1,406 @@ +"""Swap edges in a graph.""" + +import math + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = ["double_edge_swap", "connected_double_edge_swap", "directed_edge_swap"] + + +@nx.utils.not_implemented_for("undirected") +@py_random_state(3) +@nx._dispatchable(mutates_input=True, returns_graph=True) +def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None): + """Swap three edges in a directed graph while keeping the node degrees fixed. + + A directed edge swap swaps three edges such that a -> b -> c -> d becomes + a -> c -> b -> d. This pattern of swapping allows all possible states with the + same in- and out-degree distribution in a directed graph to be reached. + + If the swap would create parallel edges (e.g. if a -> c already existed in the + previous example), another attempt is made to find a suitable trio of edges. + + Parameters + ---------- + G : DiGraph + A directed graph + + nswap : integer (optional, default=1) + Number of three-edge (directed) swaps to perform + + max_tries : integer (optional, default=100) + Maximum number of attempts to swap edges + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : DiGraph + The graph after the edges are swapped. + + Raises + ------ + NetworkXError + If `G` is not directed, or + If nswap > max_tries, or + If there are fewer than 4 nodes or 3 edges in `G`. + NetworkXAlgorithmError + If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made + + Notes + ----- + Does not enforce any connectivity constraints. + + The graph G is modified in place. + + A later swap is allowed to undo a previous swap. + + References + ---------- + .. [1] Erdős, Péter L., et al. “A Simple Havel-Hakimi Type Algorithm to Realize + Graphical Degree Sequences of Directed Graphs.” ArXiv:0905.4913 [Math], + Jan. 2010. https://doi.org/10.48550/arXiv.0905.4913. + Published 2010 in Elec. J. Combinatorics (17(1)). R66. + http://www.combinatorics.org/Volume_17/PDF/v17i1r66.pdf + .. [2] “Combinatorics - Reaching All Possible Simple Directed Graphs with a given + Degree Sequence with 2-Edge Swaps.” Mathematics Stack Exchange, + https://math.stackexchange.com/questions/22272/. Accessed 30 May 2022. + """ + if nswap > max_tries: + raise nx.NetworkXError("Number of swaps > number of tries allowed.") + if len(G) < 4: + raise nx.NetworkXError("DiGraph has fewer than four nodes.") + if len(G.edges) < 3: + raise nx.NetworkXError("DiGraph has fewer than 3 edges") + + # Instead of choosing uniformly at random from a generated edge list, + # this algorithm chooses nonuniformly from the set of nodes with + # probability weighted by degree. + tries = 0 + swapcount = 0 + keys, degrees = zip(*G.degree()) # keys, degree + cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree + discrete_sequence = nx.utils.discrete_sequence + + while swapcount < nswap: + # choose source node index from discrete distribution + start_index = discrete_sequence(1, cdistribution=cdf, seed=seed)[0] + start = keys[start_index] + tries += 1 + + if tries > max_tries: + msg = f"Maximum number of swap attempts ({tries}) exceeded before desired swaps achieved ({nswap})." + raise nx.NetworkXAlgorithmError(msg) + + # If the given node doesn't have any out edges, then there isn't anything to swap + if G.out_degree(start) == 0: + continue + second = seed.choice(list(G.succ[start])) + if start == second: + continue + + if G.out_degree(second) == 0: + continue + third = seed.choice(list(G.succ[second])) + if second == third: + continue + + if G.out_degree(third) == 0: + continue + fourth = seed.choice(list(G.succ[third])) + if third == fourth: + continue + + if ( + third not in G.succ[start] + and fourth not in G.succ[second] + and second not in G.succ[third] + ): + # Swap nodes + G.add_edge(start, third) + G.add_edge(third, second) + G.add_edge(second, fourth) + G.remove_edge(start, second) + G.remove_edge(second, third) + G.remove_edge(third, fourth) + swapcount += 1 + + return G + + +@py_random_state(3) +@nx._dispatchable(mutates_input=True, returns_graph=True) +def double_edge_swap(G, nswap=1, max_tries=100, seed=None): + """Swap two edges in the graph while keeping the node degrees fixed. + + A double-edge swap removes two randomly chosen edges u-v and x-y + and creates the new edges u-x and v-y:: + + u--v u v + becomes | | + x--y x y + + If either the edge u-x or v-y already exist no swap is performed + and another attempt is made to find a suitable edge pair. + + Parameters + ---------- + G : graph + An undirected graph + + nswap : integer (optional, default=1) + Number of double-edge swaps to perform + + max_tries : integer (optional) + Maximum number of attempts to swap edges + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : graph + The graph after double edge swaps. + + Raises + ------ + NetworkXError + If `G` is directed, or + If `nswap` > `max_tries`, or + If there are fewer than 4 nodes or 2 edges in `G`. + NetworkXAlgorithmError + If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made + + Notes + ----- + Does not enforce any connectivity constraints. + + The graph G is modified in place. + """ + if G.is_directed(): + raise nx.NetworkXError( + "double_edge_swap() not defined for directed graphs. Use directed_edge_swap instead." + ) + if nswap > max_tries: + raise nx.NetworkXError("Number of swaps > number of tries allowed.") + if len(G) < 4: + raise nx.NetworkXError("Graph has fewer than four nodes.") + if len(G.edges) < 2: + raise nx.NetworkXError("Graph has fewer than 2 edges") + # Instead of choosing uniformly at random from a generated edge list, + # this algorithm chooses nonuniformly from the set of nodes with + # probability weighted by degree. + n = 0 + swapcount = 0 + keys, degrees = zip(*G.degree()) # keys, degree + cdf = nx.utils.cumulative_distribution(degrees) # cdf of degree + discrete_sequence = nx.utils.discrete_sequence + while swapcount < nswap: + # if random.random() < 0.5: continue # trick to avoid periodicities? + # pick two random edges without creating edge list + # choose source node indices from discrete distribution + (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) + if ui == xi: + continue # same source, skip + u = keys[ui] # convert index to label + x = keys[xi] + # choose target uniformly from neighbors + v = seed.choice(list(G[u])) + y = seed.choice(list(G[x])) + if v == y: + continue # same target, skip + if (x not in G[u]) and (y not in G[v]): # don't create parallel edges + G.add_edge(u, x) + G.add_edge(v, y) + G.remove_edge(u, v) + G.remove_edge(x, y) + swapcount += 1 + if n >= max_tries: + e = ( + f"Maximum number of swap attempts ({n}) exceeded " + f"before desired swaps achieved ({nswap})." + ) + raise nx.NetworkXAlgorithmError(e) + n += 1 + return G + + +@py_random_state(3) +@nx._dispatchable(mutates_input=True) +def connected_double_edge_swap(G, nswap=1, _window_threshold=3, seed=None): + """Attempts the specified number of double-edge swaps in the graph `G`. + + A double-edge swap removes two randomly chosen edges `(u, v)` and `(x, + y)` and creates the new edges `(u, x)` and `(v, y)`:: + + u--v u v + becomes | | + x--y x y + + If either `(u, x)` or `(v, y)` already exist, then no swap is performed + so the actual number of swapped edges is always *at most* `nswap`. + + Parameters + ---------- + G : graph + An undirected graph + + nswap : integer (optional, default=1) + Number of double-edge swaps to perform + + _window_threshold : integer + + The window size below which connectedness of the graph will be checked + after each swap. + + The "window" in this function is a dynamically updated integer that + represents the number of swap attempts to make before checking if the + graph remains connected. It is an optimization used to decrease the + running time of the algorithm in exchange for increased complexity of + implementation. + + If the window size is below this threshold, then the algorithm checks + after each swap if the graph remains connected by checking if there is a + path joining the two nodes whose edge was just removed. If the window + size is above this threshold, then the algorithm performs do all the + swaps in the window and only then check if the graph is still connected. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + int + The number of successful swaps + + Raises + ------ + + NetworkXError + + If the input graph is not connected, or if the graph has fewer than four + nodes. + + Notes + ----- + + The initial graph `G` must be connected, and the resulting graph is + connected. The graph `G` is modified in place. + + References + ---------- + .. [1] C. Gkantsidis and M. Mihail and E. Zegura, + The Markov chain simulation method for generating connected + power law random graphs, 2003. + http://citeseer.ist.psu.edu/gkantsidis03markov.html + """ + if not nx.is_connected(G): + raise nx.NetworkXError("Graph not connected") + if len(G) < 4: + raise nx.NetworkXError("Graph has fewer than four nodes.") + n = 0 + swapcount = 0 + deg = G.degree() + # Label key for nodes + dk = [n for n, d in G.degree()] + cdf = nx.utils.cumulative_distribution([d for n, d in G.degree()]) + discrete_sequence = nx.utils.discrete_sequence + window = 1 + while n < nswap: + wcount = 0 + swapped = [] + # If the window is small, we just check each time whether the graph is + # connected by checking if the nodes that were just separated are still + # connected. + if window < _window_threshold: + # This Boolean keeps track of whether there was a failure or not. + fail = False + while wcount < window and n < nswap: + # Pick two random edges without creating the edge list. Choose + # source nodes from the discrete degree distribution. + (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) + # If the source nodes are the same, skip this pair. + if ui == xi: + continue + # Convert an index to a node label. + u = dk[ui] + x = dk[xi] + # Choose targets uniformly from neighbors. + v = seed.choice(list(G.neighbors(u))) + y = seed.choice(list(G.neighbors(x))) + # If the target nodes are the same, skip this pair. + if v == y: + continue + if x not in G[u] and y not in G[v]: + G.remove_edge(u, v) + G.remove_edge(x, y) + G.add_edge(u, x) + G.add_edge(v, y) + swapped.append((u, v, x, y)) + swapcount += 1 + n += 1 + # If G remains connected... + if nx.has_path(G, u, v): + wcount += 1 + # Otherwise, undo the changes. + else: + G.add_edge(u, v) + G.add_edge(x, y) + G.remove_edge(u, x) + G.remove_edge(v, y) + swapcount -= 1 + fail = True + # If one of the swaps failed, reduce the window size. + if fail: + window = math.ceil(window / 2) + else: + window += 1 + # If the window is large, then there is a good chance that a bunch of + # swaps will work. It's quicker to do all those swaps first and then + # check if the graph remains connected. + else: + while wcount < window and n < nswap: + # Pick two random edges without creating the edge list. Choose + # source nodes from the discrete degree distribution. + (ui, xi) = discrete_sequence(2, cdistribution=cdf, seed=seed) + # If the source nodes are the same, skip this pair. + if ui == xi: + continue + # Convert an index to a node label. + u = dk[ui] + x = dk[xi] + # Choose targets uniformly from neighbors. + v = seed.choice(list(G.neighbors(u))) + y = seed.choice(list(G.neighbors(x))) + # If the target nodes are the same, skip this pair. + if v == y: + continue + if x not in G[u] and y not in G[v]: + G.remove_edge(u, v) + G.remove_edge(x, y) + G.add_edge(u, x) + G.add_edge(v, y) + swapped.append((u, v, x, y)) + swapcount += 1 + n += 1 + wcount += 1 + # If the graph remains connected, increase the window size. + if nx.is_connected(G): + window += 1 + # Otherwise, undo the changes from the previous window and decrease + # the window size. + else: + while swapped: + (u, v, x, y) = swapped.pop() + G.add_edge(u, v) + G.add_edge(x, y) + G.remove_edge(u, x) + G.remove_edge(v, y) + swapcount -= 1 + window = math.ceil(window / 2) + return swapcount diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py new file mode 100644 index 0000000000000000000000000000000000000000..67131b2d05026317b496d06e6b382836c8c26367 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_asteroidal.py @@ -0,0 +1,23 @@ +import networkx as nx + + +def test_is_at_free(): + is_at_free = nx.asteroidal.is_at_free + + cycle = nx.cycle_graph(6) + assert not is_at_free(cycle) + + path = nx.path_graph(6) + assert is_at_free(path) + + small_graph = nx.complete_graph(2) + assert is_at_free(small_graph) + + petersen = nx.petersen_graph() + assert not is_at_free(petersen) + + clique = nx.complete_graph(6) + assert is_at_free(clique) + + line_clique = nx.line_graph(clique) + assert not is_at_free(line_clique) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py new file mode 100644 index 0000000000000000000000000000000000000000..856be465556941fe6f2bfc2c8bab6d4b508cf999 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_boundary.py @@ -0,0 +1,154 @@ +"""Unit tests for the :mod:`networkx.algorithms.boundary` module.""" + +from itertools import combinations + +import pytest + +import networkx as nx +from networkx import convert_node_labels_to_integers as cnlti +from networkx.utils import edges_equal + + +class TestNodeBoundary: + """Unit tests for the :func:`~networkx.node_boundary` function.""" + + def test_null_graph(self): + """Tests that the null graph has empty node boundaries.""" + null = nx.null_graph() + assert nx.node_boundary(null, []) == set() + assert nx.node_boundary(null, [], []) == set() + assert nx.node_boundary(null, [1, 2, 3]) == set() + assert nx.node_boundary(null, [1, 2, 3], [4, 5, 6]) == set() + assert nx.node_boundary(null, [1, 2, 3], [3, 4, 5]) == set() + + def test_path_graph(self): + P10 = cnlti(nx.path_graph(10), first_label=1) + assert nx.node_boundary(P10, []) == set() + assert nx.node_boundary(P10, [], []) == set() + assert nx.node_boundary(P10, [1, 2, 3]) == {4} + assert nx.node_boundary(P10, [4, 5, 6]) == {3, 7} + assert nx.node_boundary(P10, [3, 4, 5, 6, 7]) == {2, 8} + assert nx.node_boundary(P10, [8, 9, 10]) == {7} + assert nx.node_boundary(P10, [4, 5, 6], [9, 10]) == set() + + def test_complete_graph(self): + K10 = cnlti(nx.complete_graph(10), first_label=1) + assert nx.node_boundary(K10, []) == set() + assert nx.node_boundary(K10, [], []) == set() + assert nx.node_boundary(K10, [1, 2, 3]) == {4, 5, 6, 7, 8, 9, 10} + assert nx.node_boundary(K10, [4, 5, 6]) == {1, 2, 3, 7, 8, 9, 10} + assert nx.node_boundary(K10, [3, 4, 5, 6, 7]) == {1, 2, 8, 9, 10} + assert nx.node_boundary(K10, [4, 5, 6], []) == set() + assert nx.node_boundary(K10, K10) == set() + assert nx.node_boundary(K10, [1, 2, 3], [3, 4, 5]) == {4, 5} + + def test_petersen(self): + """Check boundaries in the petersen graph + + cheeger(G,k)=min(|bdy(S)|/|S| for |S|=k, 0>> list(cycles("abc")) + [('a', 'b', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b')] + + """ + n = len(seq) + cycled_seq = cycle(seq) + for x in seq: + yield tuple(islice(cycled_seq, n)) + next(cycled_seq) + + +def cyclic_equals(seq1, seq2): + """Decide whether two sequences are equal up to cyclic permutations. + + For example:: + + >>> cyclic_equals("xyz", "zxy") + True + >>> cyclic_equals("xyz", "zyx") + False + + """ + # Cast seq2 to a tuple since `cycles()` yields tuples. + seq2 = tuple(seq2) + return any(x == tuple(seq2) for x in cycles(seq1)) + + +class TestChainDecomposition: + """Unit tests for the chain decomposition function.""" + + def assertContainsChain(self, chain, expected): + # A cycle could be expressed in two different orientations, one + # forward and one backward, so we need to check for cyclic + # equality in both orientations. + reversed_chain = list(reversed([tuple(reversed(e)) for e in chain])) + for candidate in expected: + if cyclic_equals(chain, candidate): + break + if cyclic_equals(reversed_chain, candidate): + break + else: + self.fail("chain not found") + + def test_decomposition(self): + edges = [ + # DFS tree edges. + (1, 2), + (2, 3), + (3, 4), + (3, 5), + (5, 6), + (6, 7), + (7, 8), + (5, 9), + (9, 10), + # Nontree edges. + (1, 3), + (1, 4), + (2, 5), + (5, 10), + (6, 8), + ] + G = nx.Graph(edges) + expected = [ + [(1, 3), (3, 2), (2, 1)], + [(1, 4), (4, 3)], + [(2, 5), (5, 3)], + [(5, 10), (10, 9), (9, 5)], + [(6, 8), (8, 7), (7, 6)], + ] + chains = list(nx.chain_decomposition(G, root=1)) + assert len(chains) == len(expected) + + def test_barbell_graph(self): + # The (3, 0) barbell graph has two triangles joined by a single edge. + G = nx.barbell_graph(3, 0) + chains = list(nx.chain_decomposition(G, root=0)) + expected = [[(0, 1), (1, 2), (2, 0)], [(3, 4), (4, 5), (5, 3)]] + assert len(chains) == len(expected) + for chain in chains: + self.assertContainsChain(chain, expected) + + def test_disconnected_graph(self): + """Test for a graph with multiple connected components.""" + G = nx.barbell_graph(3, 0) + H = nx.barbell_graph(3, 0) + mapping = dict(zip(range(6), "abcdef")) + nx.relabel_nodes(H, mapping, copy=False) + G = nx.union(G, H) + chains = list(nx.chain_decomposition(G)) + expected = [ + [(0, 1), (1, 2), (2, 0)], + [(3, 4), (4, 5), (5, 3)], + [("a", "b"), ("b", "c"), ("c", "a")], + [("d", "e"), ("e", "f"), ("f", "d")], + ] + assert len(chains) == len(expected) + for chain in chains: + self.assertContainsChain(chain, expected) + + def test_disconnected_graph_root_node(self): + """Test for a single component of a disconnected graph.""" + G = nx.barbell_graph(3, 0) + H = nx.barbell_graph(3, 0) + mapping = dict(zip(range(6), "abcdef")) + nx.relabel_nodes(H, mapping, copy=False) + G = nx.union(G, H) + chains = list(nx.chain_decomposition(G, root="a")) + expected = [ + [("a", "b"), ("b", "c"), ("c", "a")], + [("d", "e"), ("e", "f"), ("f", "d")], + ] + assert len(chains) == len(expected) + for chain in chains: + self.assertContainsChain(chain, expected) + + def test_chain_decomposition_root_not_in_G(self): + """Test chain decomposition when root is not in graph""" + G = nx.Graph() + G.add_nodes_from([1, 2, 3]) + with pytest.raises(nx.NodeNotFound): + nx.has_bridges(G, root=6) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py new file mode 100644 index 0000000000000000000000000000000000000000..148b22f2632d722522483b556f11285a8e823126 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_chordal.py @@ -0,0 +1,129 @@ +import pytest + +import networkx as nx + + +class TestMCS: + @classmethod + def setup_class(cls): + # simple graph + connected_chordal_G = nx.Graph() + connected_chordal_G.add_edges_from( + [ + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 6), + ] + ) + cls.connected_chordal_G = connected_chordal_G + + chordal_G = nx.Graph() + chordal_G.add_edges_from( + [ + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 6), + (7, 8), + ] + ) + chordal_G.add_node(9) + cls.chordal_G = chordal_G + + non_chordal_G = nx.Graph() + non_chordal_G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5), (3, 4), (3, 5)]) + cls.non_chordal_G = non_chordal_G + + self_loop_G = nx.Graph() + self_loop_G.add_edges_from([(1, 1)]) + cls.self_loop_G = self_loop_G + + @pytest.mark.parametrize("G", (nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph())) + def test_is_chordal_not_implemented(self, G): + with pytest.raises(nx.NetworkXNotImplemented): + nx.is_chordal(G) + + def test_is_chordal(self): + assert not nx.is_chordal(self.non_chordal_G) + assert nx.is_chordal(self.chordal_G) + assert nx.is_chordal(self.connected_chordal_G) + assert nx.is_chordal(nx.Graph()) + assert nx.is_chordal(nx.complete_graph(3)) + assert nx.is_chordal(nx.cycle_graph(3)) + assert not nx.is_chordal(nx.cycle_graph(5)) + assert nx.is_chordal(self.self_loop_G) + + def test_induced_nodes(self): + G = nx.generators.classic.path_graph(10) + Induced_nodes = nx.find_induced_nodes(G, 1, 9, 2) + assert Induced_nodes == {1, 2, 3, 4, 5, 6, 7, 8, 9} + pytest.raises( + nx.NetworkXTreewidthBoundExceeded, nx.find_induced_nodes, G, 1, 9, 1 + ) + Induced_nodes = nx.find_induced_nodes(self.chordal_G, 1, 6) + assert Induced_nodes == {1, 2, 4, 6} + pytest.raises(nx.NetworkXError, nx.find_induced_nodes, self.non_chordal_G, 1, 5) + + def test_graph_treewidth(self): + with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"): + nx.chordal_graph_treewidth(self.non_chordal_G) + + def test_chordal_find_cliques(self): + cliques = { + frozenset([9]), + frozenset([7, 8]), + frozenset([1, 2, 3]), + frozenset([2, 3, 4]), + frozenset([3, 4, 5, 6]), + } + assert set(nx.chordal_graph_cliques(self.chordal_G)) == cliques + with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"): + set(nx.chordal_graph_cliques(self.non_chordal_G)) + with pytest.raises(nx.NetworkXError, match="Input graph is not chordal"): + set(nx.chordal_graph_cliques(self.self_loop_G)) + + def test_chordal_find_cliques_path(self): + G = nx.path_graph(10) + cliqueset = nx.chordal_graph_cliques(G) + for u, v in G.edges(): + assert frozenset([u, v]) in cliqueset or frozenset([v, u]) in cliqueset + + def test_chordal_find_cliquesCC(self): + cliques = {frozenset([1, 2, 3]), frozenset([2, 3, 4]), frozenset([3, 4, 5, 6])} + cgc = nx.chordal_graph_cliques + assert set(cgc(self.connected_chordal_G)) == cliques + + def test_complete_to_chordal_graph(self): + fgrg = nx.fast_gnp_random_graph + test_graphs = [ + nx.barbell_graph(6, 2), + nx.cycle_graph(15), + nx.wheel_graph(20), + nx.grid_graph([10, 4]), + nx.ladder_graph(15), + nx.star_graph(5), + nx.bull_graph(), + fgrg(20, 0.3, seed=1), + ] + for G in test_graphs: + H, a = nx.complete_to_chordal_graph(G) + assert nx.is_chordal(H) + assert len(a) == H.number_of_nodes() + if nx.is_chordal(G): + assert G.number_of_edges() == H.number_of_edges() + assert set(a.values()) == {0} + else: + assert len(set(a.values())) == H.number_of_nodes() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py new file mode 100644 index 0000000000000000000000000000000000000000..118b3093cf3fd12ffe458983b37549faf78c83c3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_clique.py @@ -0,0 +1,300 @@ +import pytest + +import networkx as nx +from networkx import convert_node_labels_to_integers as cnlti + + +class TestCliques: + def setup_method(self): + z = [3, 4, 3, 4, 2, 4, 2, 1, 1, 1, 1] + self.G = cnlti(nx.generators.havel_hakimi_graph(z), first_label=1) + self.cl = list(nx.find_cliques(self.G)) + H = nx.complete_graph(6) + H = nx.relabel_nodes(H, {i: i + 1 for i in range(6)}) + H.remove_edges_from([(2, 6), (2, 5), (2, 4), (1, 3), (5, 3)]) + self.H = H + + def test_find_cliques1(self): + cl = list(nx.find_cliques(self.G)) + rcl = nx.find_cliques_recursive(self.G) + expected = [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]] + assert sorted(map(sorted, cl)) == sorted(map(sorted, rcl)) + assert sorted(map(sorted, cl)) == sorted(map(sorted, expected)) + + def test_selfloops(self): + self.G.add_edge(1, 1) + cl = list(nx.find_cliques(self.G)) + rcl = list(nx.find_cliques_recursive(self.G)) + assert set(map(frozenset, cl)) == set(map(frozenset, rcl)) + answer = [{2, 6, 1, 3}, {2, 6, 4}, {5, 4, 7}, {8, 9}, {10, 11}] + assert len(answer) == len(cl) + assert all(set(c) in answer for c in cl) + + def test_find_cliques2(self): + hcl = list(nx.find_cliques(self.H)) + assert sorted(map(sorted, hcl)) == [[1, 2], [1, 4, 5, 6], [2, 3], [3, 4, 6]] + + def test_find_cliques3(self): + # all cliques are [[2, 6, 1, 3], [2, 6, 4], [5, 4, 7], [8, 9], [10, 11]] + + cl = list(nx.find_cliques(self.G, [2])) + rcl = nx.find_cliques_recursive(self.G, [2]) + expected = [[2, 6, 1, 3], [2, 6, 4]] + assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected)) + assert sorted(map(sorted, cl)) == sorted(map(sorted, expected)) + + cl = list(nx.find_cliques(self.G, [2, 3])) + rcl = nx.find_cliques_recursive(self.G, [2, 3]) + expected = [[2, 6, 1, 3]] + assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected)) + assert sorted(map(sorted, cl)) == sorted(map(sorted, expected)) + + cl = list(nx.find_cliques(self.G, [2, 6, 4])) + rcl = nx.find_cliques_recursive(self.G, [2, 6, 4]) + expected = [[2, 6, 4]] + assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected)) + assert sorted(map(sorted, cl)) == sorted(map(sorted, expected)) + + cl = list(nx.find_cliques(self.G, [2, 6, 4])) + rcl = nx.find_cliques_recursive(self.G, [2, 6, 4]) + expected = [[2, 6, 4]] + assert sorted(map(sorted, rcl)) == sorted(map(sorted, expected)) + assert sorted(map(sorted, cl)) == sorted(map(sorted, expected)) + + with pytest.raises(ValueError): + list(nx.find_cliques(self.G, [2, 6, 4, 1])) + + with pytest.raises(ValueError): + list(nx.find_cliques_recursive(self.G, [2, 6, 4, 1])) + + def test_find_cliques_directed(self): + G = nx.path_graph(4, create_using=nx.DiGraph) + msg = "not implemented for directed" + with pytest.raises(nx.NetworkXNotImplemented, match=msg): + list(nx.find_cliques(G)) + + with pytest.raises(nx.NetworkXNotImplemented, match=msg): + list(nx.find_cliques_recursive(G)) + + def test_number_of_cliques(self): + G = self.G + assert nx.number_of_cliques(G, 1) == 1 + assert list(nx.number_of_cliques(G, [1]).values()) == [1] + assert list(nx.number_of_cliques(G, [1, 2]).values()) == [1, 2] + assert nx.number_of_cliques(G, [1, 2]) == {1: 1, 2: 2} + assert nx.number_of_cliques(G, 2) == 2 + assert nx.number_of_cliques(G) == { + 1: 1, + 2: 2, + 3: 1, + 4: 2, + 5: 1, + 6: 2, + 7: 1, + 8: 1, + 9: 1, + 10: 1, + 11: 1, + } + assert nx.number_of_cliques(G, nodes=list(G)) == { + 1: 1, + 2: 2, + 3: 1, + 4: 2, + 5: 1, + 6: 2, + 7: 1, + 8: 1, + 9: 1, + 10: 1, + 11: 1, + } + assert nx.number_of_cliques(G, nodes=[2, 3, 4]) == {2: 2, 3: 1, 4: 2} + assert nx.number_of_cliques(G, cliques=self.cl) == { + 1: 1, + 2: 2, + 3: 1, + 4: 2, + 5: 1, + 6: 2, + 7: 1, + 8: 1, + 9: 1, + 10: 1, + 11: 1, + } + assert nx.number_of_cliques(G, list(G), cliques=self.cl) == { + 1: 1, + 2: 2, + 3: 1, + 4: 2, + 5: 1, + 6: 2, + 7: 1, + 8: 1, + 9: 1, + 10: 1, + 11: 1, + } + + def test_node_clique_number(self): + G = self.G + assert nx.node_clique_number(G, 1) == 4 + assert list(nx.node_clique_number(G, [1]).values()) == [4] + assert list(nx.node_clique_number(G, [1, 2]).values()) == [4, 4] + assert nx.node_clique_number(G, [1, 2]) == {1: 4, 2: 4} + assert nx.node_clique_number(G, 1) == 4 + assert nx.node_clique_number(G) == { + 1: 4, + 2: 4, + 3: 4, + 4: 3, + 5: 3, + 6: 4, + 7: 3, + 8: 2, + 9: 2, + 10: 2, + 11: 2, + } + assert nx.node_clique_number(G, cliques=self.cl) == { + 1: 4, + 2: 4, + 3: 4, + 4: 3, + 5: 3, + 6: 4, + 7: 3, + 8: 2, + 9: 2, + 10: 2, + 11: 2, + } + assert nx.node_clique_number(G, [1, 2], cliques=self.cl) == {1: 4, 2: 4} + assert nx.node_clique_number(G, 1, cliques=self.cl) == 4 + + def test_make_clique_bipartite(self): + G = self.G + B = nx.make_clique_bipartite(G) + assert sorted(B) == [-5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + # Project onto the nodes of the original graph. + H = nx.projected_graph(B, range(1, 12)) + assert H.adj == G.adj + # Project onto the nodes representing the cliques. + H1 = nx.projected_graph(B, range(-5, 0)) + # Relabel the negative numbers as positive ones. + H1 = nx.relabel_nodes(H1, {-v: v for v in range(1, 6)}) + assert sorted(H1) == [1, 2, 3, 4, 5] + + def test_make_max_clique_graph(self): + """Tests that the maximal clique graph is the same as the bipartite + clique graph after being projected onto the nodes representing the + cliques. + + """ + G = self.G + B = nx.make_clique_bipartite(G) + # Project onto the nodes representing the cliques. + H1 = nx.projected_graph(B, range(-5, 0)) + # Relabel the negative numbers as nonnegative ones, starting at + # 0. + H1 = nx.relabel_nodes(H1, {-v: v - 1 for v in range(1, 6)}) + H2 = nx.make_max_clique_graph(G) + assert H1.adj == H2.adj + + def test_directed(self): + with pytest.raises(nx.NetworkXNotImplemented): + next(nx.find_cliques(nx.DiGraph())) + + def test_find_cliques_trivial(self): + G = nx.Graph() + assert sorted(nx.find_cliques(G)) == [] + assert sorted(nx.find_cliques_recursive(G)) == [] + + def test_make_max_clique_graph_create_using(self): + G = nx.Graph([(1, 2), (3, 1), (4, 1), (5, 6)]) + E = nx.Graph([(0, 1), (0, 2), (1, 2)]) + E.add_node(3) + assert nx.is_isomorphic(nx.make_max_clique_graph(G, create_using=nx.Graph), E) + + +class TestEnumerateAllCliques: + def test_paper_figure_4(self): + # Same graph as given in Fig. 4 of paper enumerate_all_cliques is + # based on. + # http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=1559964&isnumber=33129 + G = nx.Graph() + edges_fig_4 = [ + ("a", "b"), + ("a", "c"), + ("a", "d"), + ("a", "e"), + ("b", "c"), + ("b", "d"), + ("b", "e"), + ("c", "d"), + ("c", "e"), + ("d", "e"), + ("f", "b"), + ("f", "c"), + ("f", "g"), + ("g", "f"), + ("g", "c"), + ("g", "d"), + ("g", "e"), + ] + G.add_edges_from(edges_fig_4) + + cliques = list(nx.enumerate_all_cliques(G)) + clique_sizes = list(map(len, cliques)) + assert sorted(clique_sizes) == clique_sizes + + expected_cliques = [ + ["a"], + ["b"], + ["c"], + ["d"], + ["e"], + ["f"], + ["g"], + ["a", "b"], + ["a", "b", "d"], + ["a", "b", "d", "e"], + ["a", "b", "e"], + ["a", "c"], + ["a", "c", "d"], + ["a", "c", "d", "e"], + ["a", "c", "e"], + ["a", "d"], + ["a", "d", "e"], + ["a", "e"], + ["b", "c"], + ["b", "c", "d"], + ["b", "c", "d", "e"], + ["b", "c", "e"], + ["b", "c", "f"], + ["b", "d"], + ["b", "d", "e"], + ["b", "e"], + ["b", "f"], + ["c", "d"], + ["c", "d", "e"], + ["c", "d", "e", "g"], + ["c", "d", "g"], + ["c", "e"], + ["c", "e", "g"], + ["c", "f"], + ["c", "f", "g"], + ["c", "g"], + ["d", "e"], + ["d", "e", "g"], + ["d", "g"], + ["e", "g"], + ["f", "g"], + ["a", "b", "c"], + ["a", "b", "c", "d"], + ["a", "b", "c", "d", "e"], + ["a", "b", "c", "e"], + ] + + assert sorted(map(sorted, cliques)) == sorted(map(sorted, expected_cliques)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..f4741b420af781cec73c86e917af6ad68fa091a7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cluster.py @@ -0,0 +1,678 @@ +import pytest + +import networkx as nx + + +def test_square_clustering_adjacent_squares(): + G = nx.Graph([(1, 2), (1, 3), (2, 4), (3, 4), (3, 5), (4, 6), (5, 6)]) + # Corner nodes: C_4 == 0.5, central face nodes: C_4 = 1 / 3 + expected = {1: 0.5, 2: 0.5, 3: 1 / 3, 4: 1 / 3, 5: 0.5, 6: 0.5} + assert nx.square_clustering(G) == expected + + +def test_square_clustering_2d_grid(): + G = nx.grid_2d_graph(3, 3) + # Central node: 4 squares out of 20 potential + expected = { + (0, 0): 1 / 3, + (0, 1): 0.25, + (0, 2): 1 / 3, + (1, 0): 0.25, + (1, 1): 0.2, + (1, 2): 0.25, + (2, 0): 1 / 3, + (2, 1): 0.25, + (2, 2): 1 / 3, + } + assert nx.square_clustering(G) == expected + + +def test_square_clustering_multiple_squares_non_complete(): + """An example where all nodes are part of all squares, but not every node + is connected to every other.""" + G = nx.Graph([(0, 1), (0, 2), (1, 3), (2, 3), (1, 4), (2, 4), (1, 5), (2, 5)]) + expected = {n: 1 for n in G} + assert nx.square_clustering(G) == expected + + +class TestTriangles: + def test_empty(self): + G = nx.Graph() + assert list(nx.triangles(G).values()) == [] + + def test_path(self): + G = nx.path_graph(10) + assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert nx.triangles(G) == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + + def test_cubical(self): + G = nx.cubical_graph() + assert list(nx.triangles(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0] + assert nx.triangles(G, 1) == 0 + assert list(nx.triangles(G, [1, 2]).values()) == [0, 0] + assert nx.triangles(G, 1) == 0 + assert nx.triangles(G, [1, 2]) == {1: 0, 2: 0} + + def test_k5(self): + G = nx.complete_graph(5) + assert list(nx.triangles(G).values()) == [6, 6, 6, 6, 6] + assert sum(nx.triangles(G).values()) / 3 == 10 + assert nx.triangles(G, 1) == 6 + G.remove_edge(1, 2) + assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5] + assert nx.triangles(G, 1) == 3 + G.add_edge(3, 3) # ignore self-edges + assert list(nx.triangles(G).values()) == [5, 3, 3, 5, 5] + assert nx.triangles(G, 3) == 5 + + +def test_all_triangles_non_integer_nodes(): + G = nx.Graph() + G.add_edges_from( + [ + ("a", "b"), + ("b", "c"), + ("c", "a"), # triangle: a-b-c + ] + ) + expected = {frozenset({"a", "b", "c"})} + assert {frozenset(t) for t in nx.all_triangles(G)} == expected + + +def test_all_triangles_overlapping(): + G = nx.Graph() + G.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 0), # triangle: 0-1-2 + (0, 2), + (2, 3), + (3, 0), # triangle: 0-2-3 + ] + ) + expected = {frozenset({0, 1, 2}), frozenset({0, 2, 3})} + assert {frozenset(t) for t in nx.all_triangles(G)} == expected + + +def test_all_triangles_subset(): + G = nx.Graph() + G.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 0), # triangle: 0-1-2 + (2, 3), + (3, 4), + (4, 2), # triangle: 2-3-4 + ] + ) + assert {frozenset(t) for t in nx.all_triangles(G, nbunch=[0, 1])} == { + frozenset({0, 1, 2}) + } + + +def test_all_triangles_subset_empty(): + G = nx.Graph() + G.add_edges_from( + [ + (0, 1), + (1, 2), + (2, 0), # triangle: 0-1-2 + (2, 3), + (3, 4), + (4, 2), # triangle: 2-3-4 + (5, 2), + ] + ) + assert list(nx.all_triangles(G, nbunch=[5])) == [] + + +def test_all_triangles_no_triangles(): + G = nx.path_graph(4) + assert list(nx.all_triangles(G)) == [] + + +def test_all_triangles_complete_graph_exact(): + G = nx.complete_graph(4) + + expected = { + frozenset({0, 1, 2}), + frozenset({0, 1, 3}), + frozenset({0, 2, 3}), + frozenset({1, 2, 3}), + } + + assert {frozenset(t) for t in nx.all_triangles(G)} == expected + + +def test_all_triangles_directed_graph(): + G = nx.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 0)]) + with pytest.raises(nx.NetworkXNotImplemented): + list(nx.all_triangles(G)) + + +@pytest.mark.parametrize("graph_type", [nx.Graph, nx.MultiGraph]) +def test_all_triangles_multiedges(graph_type): + G = graph_type() + G.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 2)]) + assert {frozenset(t) for t in nx.all_triangles(G)} == {frozenset({0, 1, 2})} + + +class TestDirectedClustering: + def test_clustering(self): + G = nx.DiGraph() + assert list(nx.clustering(G).values()) == [] + assert nx.clustering(G) == {} + + def test_path(self): + G = nx.path_graph(10, create_using=nx.DiGraph()) + assert list(nx.clustering(G).values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.clustering(G) == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + assert nx.clustering(G, 0) == 0 + + def test_k5(self): + G = nx.complete_graph(5, create_using=nx.DiGraph()) + assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1] + assert nx.average_clustering(G) == 1 + G.remove_edge(1, 2) + assert list(nx.clustering(G).values()) == [ + 11 / 12, + 1, + 1, + 11 / 12, + 11 / 12, + ] + assert nx.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12} + G.remove_edge(2, 1) + assert list(nx.clustering(G).values()) == [ + 5 / 6, + 1, + 1, + 5 / 6, + 5 / 6, + ] + assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337} + assert nx.clustering(G, 4) == 5 / 6 + + def test_triangle_and_edge(self): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(0, 4) + assert nx.clustering(G)[0] == 1 / 6 + + +class TestDirectedWeightedClustering: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + + def test_clustering(self): + G = nx.DiGraph() + assert list(nx.clustering(G, weight="weight").values()) == [] + assert nx.clustering(G) == {} + + def test_path(self): + G = nx.path_graph(10, create_using=nx.DiGraph()) + assert list(nx.clustering(G, weight="weight").values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.clustering(G, weight="weight") == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + + def test_k5(self): + G = nx.complete_graph(5, create_using=nx.DiGraph()) + assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1] + assert nx.average_clustering(G, weight="weight") == 1 + G.remove_edge(1, 2) + assert list(nx.clustering(G, weight="weight").values()) == [ + 11 / 12, + 1, + 1, + 11 / 12, + 11 / 12, + ] + assert nx.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12} + G.remove_edge(2, 1) + assert list(nx.clustering(G, weight="weight").values()) == [ + 5 / 6, + 1, + 1, + 5 / 6, + 5 / 6, + ] + assert nx.clustering(G, [1, 4], weight="weight") == { + 1: 1, + 4: 0.83333333333333337, + } + + def test_triangle_and_edge(self): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(0, 4, weight=2) + assert nx.clustering(G)[0] == 1 / 6 + # Relaxed comparisons to allow graphblas-algorithms to pass tests + np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 12) + np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 12) + + +class TestWeightedClustering: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + + def test_clustering(self): + G = nx.Graph() + assert list(nx.clustering(G, weight="weight").values()) == [] + assert nx.clustering(G) == {} + + def test_path(self): + G = nx.path_graph(10) + assert list(nx.clustering(G, weight="weight").values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.clustering(G, weight="weight") == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + + def test_cubical(self): + G = nx.cubical_graph() + assert list(nx.clustering(G, weight="weight").values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.clustering(G, 1) == 0 + assert list(nx.clustering(G, [1, 2], weight="weight").values()) == [0, 0] + assert nx.clustering(G, 1, weight="weight") == 0 + assert nx.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0} + + def test_k5(self): + G = nx.complete_graph(5) + assert list(nx.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1] + assert nx.average_clustering(G, weight="weight") == 1 + G.remove_edge(1, 2) + assert list(nx.clustering(G, weight="weight").values()) == [ + 5 / 6, + 1, + 1, + 5 / 6, + 5 / 6, + ] + assert nx.clustering(G, [1, 4], weight="weight") == { + 1: 1, + 4: 0.83333333333333337, + } + + def test_triangle_and_edge(self): + G = nx.cycle_graph(3) + G.add_edge(0, 4, weight=2) + assert nx.clustering(G)[0] == 1 / 3 + np.testing.assert_allclose(nx.clustering(G, weight="weight")[0], 1 / 6) + np.testing.assert_allclose(nx.clustering(G, 0, weight="weight"), 1 / 6) + + def test_triangle_and_signed_edge(self): + G = nx.cycle_graph(3) + G.add_edge(0, 1, weight=-1) + G.add_edge(3, 0, weight=0) + assert nx.clustering(G)[0] == 1 / 3 + assert nx.clustering(G, weight="weight")[0] == -1 / 3 + + +class TestClustering: + @classmethod + def setup_class(cls): + pytest.importorskip("numpy") + + def test_clustering(self): + G = nx.Graph() + assert list(nx.clustering(G).values()) == [] + assert nx.clustering(G) == {} + + def test_path(self): + G = nx.path_graph(10) + assert list(nx.clustering(G).values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.clustering(G) == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + + def test_cubical(self): + G = nx.cubical_graph() + assert list(nx.clustering(G).values()) == [0, 0, 0, 0, 0, 0, 0, 0] + assert nx.clustering(G, 1) == 0 + assert list(nx.clustering(G, [1, 2]).values()) == [0, 0] + assert nx.clustering(G, 1) == 0 + assert nx.clustering(G, [1, 2]) == {1: 0, 2: 0} + + def test_k5(self): + G = nx.complete_graph(5) + assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1] + assert nx.average_clustering(G) == 1 + G.remove_edge(1, 2) + assert list(nx.clustering(G).values()) == [ + 5 / 6, + 1, + 1, + 5 / 6, + 5 / 6, + ] + assert nx.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337} + + def test_k5_signed(self): + G = nx.complete_graph(5) + assert list(nx.clustering(G).values()) == [1, 1, 1, 1, 1] + assert nx.average_clustering(G) == 1 + G.remove_edge(1, 2) + G.add_edge(0, 1, weight=-1) + assert list(nx.clustering(G, weight="weight").values()) == [ + 1 / 6, + -1 / 3, + 1, + 3 / 6, + 3 / 6, + ] + + +class TestTransitivity: + def test_transitivity(self): + G = nx.Graph() + assert nx.transitivity(G) == 0 + + def test_path(self): + G = nx.path_graph(10) + assert nx.transitivity(G) == 0 + + def test_cubical(self): + G = nx.cubical_graph() + assert nx.transitivity(G) == 0 + + def test_k5(self): + G = nx.complete_graph(5) + assert nx.transitivity(G) == 1 + G.remove_edge(1, 2) + assert nx.transitivity(G) == 0.875 + + +class TestSquareClustering: + def test_clustering(self): + G = nx.Graph() + assert list(nx.square_clustering(G).values()) == [] + assert nx.square_clustering(G) == {} + + def test_path(self): + G = nx.path_graph(10) + assert list(nx.square_clustering(G).values()) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + assert nx.square_clustering(G) == { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + } + + def test_cubical(self): + G = nx.cubical_graph() + assert list(nx.square_clustering(G).values()) == [ + 1 / 3, + 1 / 3, + 1 / 3, + 1 / 3, + 1 / 3, + 1 / 3, + 1 / 3, + 1 / 3, + ] + assert list(nx.square_clustering(G, [1, 2]).values()) == [1 / 3, 1 / 3] + assert nx.square_clustering(G, [1])[1] == 1 / 3 + assert nx.square_clustering(G, 1) == 1 / 3 + assert nx.square_clustering(G, [1, 2]) == {1: 1 / 3, 2: 1 / 3} + + def test_k5(self): + G = nx.complete_graph(5) + assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1] + + def test_bipartite_k5(self): + G = nx.complete_bipartite_graph(5, 5) + assert list(nx.square_clustering(G).values()) == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + + def test_lind_square_clustering(self): + """Test C4 for figure 1 Lind et al (2005)""" + G = nx.Graph( + [ + (1, 2), + (1, 3), + (1, 6), + (1, 7), + (2, 4), + (2, 5), + (3, 4), + (3, 5), + (6, 7), + (7, 8), + (6, 8), + (7, 9), + (7, 10), + (6, 11), + (6, 12), + (2, 13), + (2, 14), + (3, 15), + (3, 16), + ] + ) + G1 = G.subgraph([1, 2, 3, 4, 5, 13, 14, 15, 16]) + G2 = G.subgraph([1, 6, 7, 8, 9, 10, 11, 12]) + assert nx.square_clustering(G, [1])[1] == 3 / 43 + assert nx.square_clustering(G1, [1])[1] == 2 / 6 + assert nx.square_clustering(G2, [1])[1] == 1 / 5 + + def test_peng_square_clustering(self): + """Test eq2 for figure 1 Peng et al (2008)""" + # Example graph from figure 1b + G = nx.Graph([(1, 2), (1, 3), (2, 4), (3, 4), (3, 5), (3, 6)]) + # From table 1, row 2 + expected = {1: 1 / 3, 2: 1, 3: 0.2, 4: 1 / 3, 5: 0, 6: 0} + assert nx.square_clustering(G) == expected + + def test_self_loops_square_clustering(self): + G = nx.path_graph(5) + assert nx.square_clustering(G) == {0: 0, 1: 0, 2: 0, 3: 0, 4: 0} + G.add_edges_from([(0, 0), (1, 1), (2, 2)]) + assert nx.square_clustering(G) == {0: 0, 1: 0, 2: 0, 3: 0, 4: 0} + + +class TestAverageClustering: + @classmethod + def setup_class(cls): + pytest.importorskip("numpy") + + def test_empty(self): + G = nx.Graph() + with pytest.raises(ZeroDivisionError): + nx.average_clustering(G) + + def test_average_clustering(self): + G = nx.cycle_graph(3) + G.add_edge(2, 3) + assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 4 + assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4 + assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3 + assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3 + assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3 + assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2 + + def test_average_clustering_signed(self): + G = nx.cycle_graph(3) + G.add_edge(2, 3) + G.add_edge(0, 1, weight=-1) + assert nx.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4 + assert ( + nx.average_clustering(G, weight="weight", count_zeros=True) + == (-1 - 1 - 1 / 3) / 4 + ) + assert ( + nx.average_clustering(G, weight="weight", count_zeros=False) + == (-1 - 1 - 1 / 3) / 3 + ) + + +class TestDirectedAverageClustering: + @classmethod + def setup_class(cls): + pytest.importorskip("numpy") + + def test_empty(self): + G = nx.DiGraph() + with pytest.raises(ZeroDivisionError): + nx.average_clustering(G) + + def test_average_clustering(self): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(2, 3) + assert nx.average_clustering(G) == (1 + 1 + 1 / 3) / 8 + assert nx.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8 + assert nx.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6 + assert nx.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6 + assert nx.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6 + assert nx.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4 + + +class TestGeneralizedDegree: + def test_generalized_degree(self): + G = nx.Graph() + assert nx.generalized_degree(G) == {} + + def test_path(self): + G = nx.path_graph(5) + assert nx.generalized_degree(G, 0) == {0: 1} + assert nx.generalized_degree(G, 1) == {0: 2} + + def test_cubical(self): + G = nx.cubical_graph() + assert nx.generalized_degree(G, 0) == {0: 3} + + def test_k5(self): + G = nx.complete_graph(5) + assert nx.generalized_degree(G, 0) == {3: 4} + G.remove_edge(0, 1) + assert nx.generalized_degree(G, 0) == {2: 3} + assert nx.generalized_degree(G, [1, 2]) == {1: {2: 3}, 2: {2: 2, 3: 2}} + assert nx.generalized_degree(G) == { + 0: {2: 3}, + 1: {2: 3}, + 2: {2: 2, 3: 2}, + 3: {2: 2, 3: 2}, + 4: {2: 2, 3: 2}, + } diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py new file mode 100644 index 0000000000000000000000000000000000000000..0f447094548415c089710b9b62ac4d73a27efeb5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_communicability.py @@ -0,0 +1,80 @@ +from collections import defaultdict + +import pytest + +pytest.importorskip("numpy") +pytest.importorskip("scipy") + +import networkx as nx +from networkx.algorithms.communicability_alg import communicability, communicability_exp + + +class TestCommunicability: + def test_communicability(self): + answer = { + 0: {0: 1.5430806348152435, 1: 1.1752011936438012}, + 1: {0: 1.1752011936438012, 1: 1.5430806348152435}, + } + # answer={(0, 0): 1.5430806348152435, + # (0, 1): 1.1752011936438012, + # (1, 0): 1.1752011936438012, + # (1, 1): 1.5430806348152435} + + result = communicability(nx.path_graph(2)) + for k1, val in result.items(): + for k2 in val: + assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7) + + def test_communicability2(self): + answer_orig = { + ("1", "1"): 1.6445956054135658, + ("1", "Albert"): 0.7430186221096251, + ("1", "Aric"): 0.7430186221096251, + ("1", "Dan"): 1.6208126320442937, + ("1", "Franck"): 0.42639707170035257, + ("Albert", "1"): 0.7430186221096251, + ("Albert", "Albert"): 2.4368257358712189, + ("Albert", "Aric"): 1.4368257358712191, + ("Albert", "Dan"): 2.0472097037446453, + ("Albert", "Franck"): 1.8340111678944691, + ("Aric", "1"): 0.7430186221096251, + ("Aric", "Albert"): 1.4368257358712191, + ("Aric", "Aric"): 2.4368257358712193, + ("Aric", "Dan"): 2.0472097037446457, + ("Aric", "Franck"): 1.8340111678944691, + ("Dan", "1"): 1.6208126320442937, + ("Dan", "Albert"): 2.0472097037446453, + ("Dan", "Aric"): 2.0472097037446457, + ("Dan", "Dan"): 3.1306328496328168, + ("Dan", "Franck"): 1.4860372442192515, + ("Franck", "1"): 0.42639707170035257, + ("Franck", "Albert"): 1.8340111678944691, + ("Franck", "Aric"): 1.8340111678944691, + ("Franck", "Dan"): 1.4860372442192515, + ("Franck", "Franck"): 2.3876142275231915, + } + + answer = defaultdict(dict) + for (k1, k2), v in answer_orig.items(): + answer[k1][k2] = v + + G1 = nx.Graph( + [ + ("Franck", "Aric"), + ("Aric", "Dan"), + ("Dan", "Albert"), + ("Albert", "Franck"), + ("Dan", "1"), + ("Franck", "Albert"), + ] + ) + + result = communicability(G1) + for k1, val in result.items(): + for k2 in val: + assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7) + + result = communicability_exp(G1) + for k1, val in result.items(): + for k2 in val: + assert answer[k1][k2] == pytest.approx(result[k1][k2], abs=1e-7) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py new file mode 100644 index 0000000000000000000000000000000000000000..7cbaf759be2ae91cd053629f73353e33bd3a5ee5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_core.py @@ -0,0 +1,266 @@ +import pytest + +import networkx as nx +from networkx.utils import nodes_equal + + +class TestCore: + @classmethod + def setup_class(cls): + # G is the example graph in Figure 1 from Batagelj and + # Zaversnik's paper titled An O(m) Algorithm for Cores + # Decomposition of Networks, 2003, + # http://arXiv.org/abs/cs/0310049. With nodes labeled as + # shown, the 3-core is given by nodes 1-8, the 2-core by nodes + # 9-16, the 1-core by nodes 17-20 and node 21 is in the + # 0-core. + t1 = nx.convert_node_labels_to_integers(nx.tetrahedral_graph(), 1) + t2 = nx.convert_node_labels_to_integers(t1, 5) + G = nx.union(t1, t2) + G.add_edges_from( + [ + (3, 7), + (2, 11), + (11, 5), + (11, 12), + (5, 12), + (12, 19), + (12, 18), + (3, 9), + (7, 9), + (7, 10), + (9, 10), + (9, 20), + (17, 13), + (13, 14), + (14, 15), + (15, 16), + (16, 13), + ] + ) + G.add_node(21) + cls.G = G + + # Create the graph H resulting from the degree sequence + # [0, 1, 2, 2, 2, 2, 3] when using the Havel-Hakimi algorithm. + + degseq = [0, 1, 2, 2, 2, 2, 3] + H = nx.havel_hakimi_graph(degseq) + mapping = {6: 0, 0: 1, 4: 3, 5: 6, 3: 4, 1: 2, 2: 5} + cls.H = nx.relabel_nodes(H, mapping) + + def test_trivial(self): + """Empty graph""" + G = nx.Graph() + assert nx.core_number(G) == {} + + def test_core_number(self): + core = nx.core_number(self.G) + nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(4)] + assert nodes_equal(nodes_by_core[0], [21]) + assert nodes_equal(nodes_by_core[1], [17, 18, 19, 20]) + assert nodes_equal(nodes_by_core[2], [9, 10, 11, 12, 13, 14, 15, 16]) + assert nodes_equal(nodes_by_core[3], [1, 2, 3, 4, 5, 6, 7, 8]) + + def test_core_number2(self): + core = nx.core_number(self.H) + nodes_by_core = [sorted(n for n in core if core[n] == val) for val in range(3)] + assert nodes_equal(nodes_by_core[0], [0]) + assert nodes_equal(nodes_by_core[1], [1, 3]) + assert nodes_equal(nodes_by_core[2], [2, 4, 5, 6]) + + def test_core_number_multigraph(self): + G = nx.complete_graph(3) + G = nx.MultiGraph(G) + G.add_edge(1, 2) + with pytest.raises( + nx.NetworkXNotImplemented, match="not implemented for multigraph type" + ): + nx.core_number(G) + + def test_core_number_self_loop(self): + G = nx.cycle_graph(3) + G.add_edge(0, 0) + with pytest.raises( + nx.NetworkXNotImplemented, match="Input graph has self loops" + ): + nx.core_number(G) + + def test_directed_core_number(self): + """core number had a bug for directed graphs found in issue #1959""" + # small example where too timid edge removal can make cn[2] = 3 + G = nx.DiGraph() + edges = [(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)] + G.add_edges_from(edges) + assert nx.core_number(G) == {1: 2, 2: 2, 3: 2, 4: 2} + # small example where too aggressive edge removal can make cn[2] = 2 + more_edges = [(1, 5), (3, 5), (4, 5), (3, 6), (4, 6), (5, 6)] + G.add_edges_from(more_edges) + assert nx.core_number(G) == {1: 3, 2: 3, 3: 3, 4: 3, 5: 3, 6: 3} + + def test_main_core(self): + main_core_subgraph = nx.k_core(self.H) + assert sorted(main_core_subgraph.nodes()) == [2, 4, 5, 6] + + def test_k_core(self): + # k=0 + k_core_subgraph = nx.k_core(self.H, k=0) + assert sorted(k_core_subgraph.nodes()) == sorted(self.H.nodes()) + # k=1 + k_core_subgraph = nx.k_core(self.H, k=1) + assert sorted(k_core_subgraph.nodes()) == [1, 2, 3, 4, 5, 6] + # k = 2 + k_core_subgraph = nx.k_core(self.H, k=2) + assert sorted(k_core_subgraph.nodes()) == [2, 4, 5, 6] + + def test_k_core_multigraph(self): + core_number = nx.core_number(self.H) + H = nx.MultiGraph(self.H) + with pytest.raises(nx.NetworkXNotImplemented): + nx.k_core(H, k=0, core_number=core_number) + + def test_main_crust(self): + main_crust_subgraph = nx.k_crust(self.H) + assert sorted(main_crust_subgraph.nodes()) == [0, 1, 3] + + def test_k_crust(self): + # k = 0 + k_crust_subgraph = nx.k_crust(self.H, k=2) + assert sorted(k_crust_subgraph.nodes()) == sorted(self.H.nodes()) + # k=1 + k_crust_subgraph = nx.k_crust(self.H, k=1) + assert sorted(k_crust_subgraph.nodes()) == [0, 1, 3] + # k=2 + k_crust_subgraph = nx.k_crust(self.H, k=0) + assert sorted(k_crust_subgraph.nodes()) == [0] + + def test_k_crust_multigraph(self): + core_number = nx.core_number(self.H) + H = nx.MultiGraph(self.H) + with pytest.raises(nx.NetworkXNotImplemented): + nx.k_crust(H, k=0, core_number=core_number) + + def test_main_shell(self): + main_shell_subgraph = nx.k_shell(self.H) + assert sorted(main_shell_subgraph.nodes()) == [2, 4, 5, 6] + + def test_k_shell(self): + # k=0 + k_shell_subgraph = nx.k_shell(self.H, k=2) + assert sorted(k_shell_subgraph.nodes()) == [2, 4, 5, 6] + # k=1 + k_shell_subgraph = nx.k_shell(self.H, k=1) + assert sorted(k_shell_subgraph.nodes()) == [1, 3] + # k=2 + k_shell_subgraph = nx.k_shell(self.H, k=0) + assert sorted(k_shell_subgraph.nodes()) == [0] + + def test_k_shell_multigraph(self): + core_number = nx.core_number(self.H) + H = nx.MultiGraph(self.H) + with pytest.raises(nx.NetworkXNotImplemented): + nx.k_shell(H, k=0, core_number=core_number) + + def test_k_corona(self): + # k=0 + k_corona_subgraph = nx.k_corona(self.H, k=2) + assert sorted(k_corona_subgraph.nodes()) == [2, 4, 5, 6] + # k=1 + k_corona_subgraph = nx.k_corona(self.H, k=1) + assert sorted(k_corona_subgraph.nodes()) == [1] + # k=2 + k_corona_subgraph = nx.k_corona(self.H, k=0) + assert sorted(k_corona_subgraph.nodes()) == [0] + + def test_k_corona_multigraph(self): + core_number = nx.core_number(self.H) + H = nx.MultiGraph(self.H) + with pytest.raises(nx.NetworkXNotImplemented): + nx.k_corona(H, k=0, core_number=core_number) + + def test_k_truss(self): + # k=-1 + k_truss_subgraph = nx.k_truss(self.G, -1) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21)) + # k=0 + k_truss_subgraph = nx.k_truss(self.G, 0) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21)) + # k=1 + k_truss_subgraph = nx.k_truss(self.G, 1) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21)) + # k=2 + k_truss_subgraph = nx.k_truss(self.G, 2) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 21)) + # k=3 + k_truss_subgraph = nx.k_truss(self.G, 3) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 13)) + + k_truss_subgraph = nx.k_truss(self.G, 4) + assert sorted(k_truss_subgraph.nodes()) == list(range(1, 9)) + + k_truss_subgraph = nx.k_truss(self.G, 5) + assert sorted(k_truss_subgraph.nodes()) == [] + + def test_k_truss_digraph(self): + G = nx.complete_graph(3) + G = nx.DiGraph(G) + G.add_edge(2, 1) + with pytest.raises( + nx.NetworkXNotImplemented, match="not implemented for directed type" + ): + nx.k_truss(G, k=1) + + def test_k_truss_multigraph(self): + G = nx.complete_graph(3) + G = nx.MultiGraph(G) + G.add_edge(1, 2) + with pytest.raises( + nx.NetworkXNotImplemented, match="not implemented for multigraph type" + ): + nx.k_truss(G, k=1) + + def test_k_truss_self_loop(self): + G = nx.cycle_graph(3) + G.add_edge(0, 0) + with pytest.raises( + nx.NetworkXNotImplemented, match="Input graph has self loops" + ): + nx.k_truss(G, k=1) + + def test_onion_layers(self): + layers = nx.onion_layers(self.G) + nodes_by_layer = [ + sorted(n for n in layers if layers[n] == val) for val in range(1, 7) + ] + assert nodes_equal(nodes_by_layer[0], [21]) + assert nodes_equal(nodes_by_layer[1], [17, 18, 19, 20]) + assert nodes_equal(nodes_by_layer[2], [10, 12, 13, 14, 15, 16]) + assert nodes_equal(nodes_by_layer[3], [9, 11]) + assert nodes_equal(nodes_by_layer[4], [1, 2, 4, 5, 6, 8]) + assert nodes_equal(nodes_by_layer[5], [3, 7]) + + def test_onion_digraph(self): + G = nx.complete_graph(3) + G = nx.DiGraph(G) + G.add_edge(2, 1) + with pytest.raises( + nx.NetworkXNotImplemented, match="not implemented for directed type" + ): + nx.onion_layers(G) + + def test_onion_multigraph(self): + G = nx.complete_graph(3) + G = nx.MultiGraph(G) + G.add_edge(1, 2) + with pytest.raises( + nx.NetworkXNotImplemented, match="not implemented for multigraph type" + ): + nx.onion_layers(G) + + def test_onion_self_loop(self): + G = nx.cycle_graph(3) + G.add_edge(0, 0) + with pytest.raises( + nx.NetworkXNotImplemented, match="Input graph contains self loops" + ): + nx.onion_layers(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py new file mode 100644 index 0000000000000000000000000000000000000000..b2f97a866b0e09c199c2edb9f40f20986caa8fbc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_covering.py @@ -0,0 +1,85 @@ +import pytest + +import networkx as nx + + +class TestMinEdgeCover: + """Tests for :func:`networkx.algorithms.min_edge_cover`""" + + def test_empty_graph(self): + G = nx.Graph() + assert nx.min_edge_cover(G) == set() + + def test_graph_with_loop(self): + G = nx.Graph() + G.add_edge(0, 0) + assert nx.min_edge_cover(G) == {(0, 0)} + + def test_graph_with_isolated_v(self): + G = nx.Graph() + G.add_node(1) + with pytest.raises( + nx.NetworkXException, + match="Graph has a node with no edge incident on it, so no edge cover exists.", + ): + nx.min_edge_cover(G) + + def test_graph_single_edge(self): + G = nx.Graph([(0, 1)]) + assert nx.min_edge_cover(G) in ({(0, 1)}, {(1, 0)}) + + def test_graph_two_edge_path(self): + G = nx.path_graph(3) + min_cover = nx.min_edge_cover(G) + assert len(min_cover) == 2 + for u, v in G.edges: + assert (u, v) in min_cover or (v, u) in min_cover + + def test_bipartite_explicit(self): + G = nx.Graph() + G.add_nodes_from([1, 2, 3, 4], bipartite=0) + G.add_nodes_from(["a", "b", "c"], bipartite=1) + G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")]) + # Use bipartite method by prescribing the algorithm + min_cover = nx.min_edge_cover( + G, nx.algorithms.bipartite.matching.eppstein_matching + ) + assert nx.is_edge_cover(G, min_cover) + assert len(min_cover) == 8 + # Use the default method which is not specialized for bipartite + min_cover2 = nx.min_edge_cover(G) + assert nx.is_edge_cover(G, min_cover2) + assert len(min_cover2) == 4 + + def test_complete_graph_even(self): + G = nx.complete_graph(10) + min_cover = nx.min_edge_cover(G) + assert nx.is_edge_cover(G, min_cover) + assert len(min_cover) == 5 + + def test_complete_graph_odd(self): + G = nx.complete_graph(11) + min_cover = nx.min_edge_cover(G) + assert nx.is_edge_cover(G, min_cover) + assert len(min_cover) == 6 + + +class TestIsEdgeCover: + """Tests for :func:`networkx.algorithms.is_edge_cover`""" + + def test_empty_graph(self): + G = nx.Graph() + assert nx.is_edge_cover(G, set()) + + def test_graph_with_loop(self): + G = nx.Graph() + G.add_edge(1, 1) + assert nx.is_edge_cover(G, {(1, 1)}) + + def test_graph_single_edge(self): + G = nx.Graph() + G.add_edge(0, 1) + assert nx.is_edge_cover(G, {(0, 0), (1, 1)}) + assert nx.is_edge_cover(G, {(0, 1), (1, 0)}) + assert nx.is_edge_cover(G, {(0, 1)}) + assert not nx.is_edge_cover(G, {(0, 0)}) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py new file mode 100644 index 0000000000000000000000000000000000000000..923efa502acc623650f36ff41e72884e5e508bc9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cuts.py @@ -0,0 +1,171 @@ +"""Unit tests for the :mod:`networkx.algorithms.cuts` module.""" + +import networkx as nx + + +class TestCutSize: + """Unit tests for the :func:`~networkx.cut_size` function.""" + + def test_symmetric(self): + """Tests that the cut size is symmetric.""" + G = nx.barbell_graph(3, 0) + S = {0, 1, 4} + T = {2, 3, 5} + assert nx.cut_size(G, S, T) == 4 + assert nx.cut_size(G, T, S) == 4 + + def test_single_edge(self): + """Tests for a cut of a single edge.""" + G = nx.barbell_graph(3, 0) + S = {0, 1, 2} + T = {3, 4, 5} + assert nx.cut_size(G, S, T) == 1 + assert nx.cut_size(G, T, S) == 1 + + def test_directed(self): + """Tests that each directed edge is counted once in the cut.""" + G = nx.barbell_graph(3, 0).to_directed() + S = {0, 1, 2} + T = {3, 4, 5} + assert nx.cut_size(G, S, T) == 2 + assert nx.cut_size(G, T, S) == 2 + + def test_directed_symmetric(self): + """Tests that a cut in a directed graph is symmetric.""" + G = nx.barbell_graph(3, 0).to_directed() + S = {0, 1, 4} + T = {2, 3, 5} + assert nx.cut_size(G, S, T) == 8 + assert nx.cut_size(G, T, S) == 8 + + def test_multigraph(self): + """Tests that parallel edges are each counted for a cut.""" + G = nx.MultiGraph(["ab", "ab"]) + assert nx.cut_size(G, {"a"}, {"b"}) == 2 + + +class TestVolume: + """Unit tests for the :func:`~networkx.volume` function.""" + + def test_graph(self): + G = nx.cycle_graph(4) + assert nx.volume(G, {0, 1}) == 4 + + def test_digraph(self): + G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0)]) + assert nx.volume(G, {0, 1}) == 2 + + def test_multigraph(self): + edges = list(nx.cycle_graph(4).edges()) + G = nx.MultiGraph(edges * 2) + assert nx.volume(G, {0, 1}) == 8 + + def test_multidigraph(self): + edges = [(0, 1), (1, 2), (2, 3), (3, 0)] + G = nx.MultiDiGraph(edges * 2) + assert nx.volume(G, {0, 1}) == 4 + + def test_barbell(self): + G = nx.barbell_graph(3, 0) + assert nx.volume(G, {0, 1, 2}) == 7 + assert nx.volume(G, {3, 4, 5}) == 7 + + +class TestNormalizedCutSize: + """Unit tests for the :func:`~networkx.normalized_cut_size` function.""" + + def test_graph(self): + G = nx.path_graph(4) + S = {1, 2} + T = set(G) - S + size = nx.normalized_cut_size(G, S, T) + # The cut looks like this: o-{-o--o-}-o + expected = 2 * ((1 / 4) + (1 / 2)) + assert expected == size + # Test with no input T + assert expected == nx.normalized_cut_size(G, S) + + def test_directed(self): + G = nx.DiGraph([(0, 1), (1, 2), (2, 3)]) + S = {1, 2} + T = set(G) - S + size = nx.normalized_cut_size(G, S, T) + # The cut looks like this: o-{->o-->o-}->o + expected = 2 * ((1 / 2) + (1 / 1)) + assert expected == size + # Test with no input T + assert expected == nx.normalized_cut_size(G, S) + + +class TestConductance: + """Unit tests for the :func:`~networkx.conductance` function.""" + + def test_graph(self): + G = nx.barbell_graph(5, 0) + # Consider the singleton sets containing the "bridge" nodes. + # There is only one cut edge, and each set has volume five. + S = {4} + T = {5} + conductance = nx.conductance(G, S, T) + expected = 1 / 5 + assert expected == conductance + # Test with no input T + G2 = nx.barbell_graph(3, 0) + # There is only one cut edge, and each set has volume seven. + S2 = {0, 1, 2} + assert nx.conductance(G2, S2) == 1 / 7 + + +class TestEdgeExpansion: + """Unit tests for the :func:`~networkx.edge_expansion` function.""" + + def test_graph(self): + G = nx.barbell_graph(5, 0) + S = set(range(5)) + T = set(G) - S + expansion = nx.edge_expansion(G, S, T) + expected = 1 / 5 + assert expected == expansion + # Test with no input T + assert expected == nx.edge_expansion(G, S) + + +class TestNodeExpansion: + """Unit tests for the :func:`~networkx.node_expansion` function.""" + + def test_graph(self): + G = nx.path_graph(8) + S = {3, 4, 5} + expansion = nx.node_expansion(G, S) + # The neighborhood of S has cardinality five, and S has + # cardinality three. + expected = 5 / 3 + assert expected == expansion + + +class TestBoundaryExpansion: + """Unit tests for the :func:`~networkx.boundary_expansion` function.""" + + def test_graph(self): + G = nx.complete_graph(10) + S = set(range(4)) + expansion = nx.boundary_expansion(G, S) + # The node boundary of S has cardinality six, and S has + # cardinality three. + expected = 6 / 4 + assert expected == expansion + + +class TestMixingExpansion: + """Unit tests for the :func:`~networkx.mixing_expansion` function.""" + + def test_graph(self): + G = nx.barbell_graph(5, 0) + S = set(range(5)) + T = set(G) - S + expansion = nx.mixing_expansion(G, S, T) + # There is one cut edge, and the total number of edges in the + # graph is twice the total number of edges in a clique of size + # five, plus one more for the bridge. + expected = 1 / (2 * (5 * 4 + 1)) + assert expected == expansion diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py new file mode 100644 index 0000000000000000000000000000000000000000..1b43929aae00be120f7fb2cd2780cd7d4ad20b03 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_cycles.py @@ -0,0 +1,984 @@ +import random +from itertools import chain, islice, tee +from math import inf + +import pytest + +import networkx as nx +from networkx.algorithms.traversal.edgedfs import FORWARD, REVERSE + + +def check_independent(basis): + if len(basis) == 0: + return + + np = pytest.importorskip("numpy") + sp = pytest.importorskip("scipy") # Required by incidence_matrix + + H = nx.Graph() + for b in basis: + nx.add_cycle(H, b) + inc = nx.incidence_matrix(H, oriented=True) + rank = np.linalg.matrix_rank(inc.toarray(), tol=None, hermitian=False) + assert inc.shape[1] - rank == len(basis) + + +class TestCycles: + @classmethod + def setup_class(cls): + G = nx.Graph() + nx.add_cycle(G, [0, 1, 2, 3]) + nx.add_cycle(G, [0, 3, 4, 5]) + nx.add_cycle(G, [0, 1, 6, 7, 8]) + G.add_edge(8, 9) + cls.G = G + + def is_cyclic_permutation(self, a, b): + n = len(a) + if len(b) != n: + return False + l = a + a + return any(l[i : i + n] == b for i in range(n)) + + def test_cycle_basis(self): + G = self.G + cy = nx.cycle_basis(G, 0) + sort_cy = sorted(sorted(c) for c in cy) + assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] + cy = nx.cycle_basis(G, 1) + sort_cy = sorted(sorted(c) for c in cy) + assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] + cy = nx.cycle_basis(G, 9) + sort_cy = sorted(sorted(c) for c in cy) + assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5]] + # test disconnected graphs + nx.add_cycle(G, "ABC") + cy = nx.cycle_basis(G, 9) + sort_cy = sorted(sorted(c) for c in cy[:-1]) + [sorted(cy[-1])] + assert sort_cy == [[0, 1, 2, 3], [0, 1, 6, 7, 8], [0, 3, 4, 5], ["A", "B", "C"]] + + def test_cycle_basis2(self): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.DiGraph() + cy = nx.cycle_basis(G, 0) + + def test_cycle_basis3(self): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.MultiGraph() + cy = nx.cycle_basis(G, 0) + + def test_cycle_basis_ordered(self): + # see gh-6654 replace sets with (ordered) dicts + G = nx.cycle_graph(5) + G.update(nx.cycle_graph(range(3, 8))) + cbG = nx.cycle_basis(G) + + perm = {1: 0, 0: 1} # switch 0 and 1 + H = nx.relabel_nodes(G, perm) + cbH = [[perm.get(n, n) for n in cyc] for cyc in nx.cycle_basis(H)] + assert cbG == cbH + + def test_cycle_basis_self_loop(self): + """Tests the function for graphs with self loops""" + G = nx.Graph() + nx.add_cycle(G, [0, 1, 2, 3]) + nx.add_cycle(G, [0, 0, 6, 2]) + cy = nx.cycle_basis(G) + sort_cy = sorted(sorted(c) for c in cy) + assert sort_cy == [[0], [0, 1, 2], [0, 2, 3], [0, 2, 6]] + + def test_simple_cycles(self): + edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] + G = nx.DiGraph(edges) + cc = sorted(nx.simple_cycles(G)) + ca = [[0], [0, 1, 2], [0, 2], [1, 2], [2]] + assert len(cc) == len(ca) + for c in cc: + assert any(self.is_cyclic_permutation(c, rc) for rc in ca) + + def test_simple_cycles_singleton(self): + G = nx.Graph([(0, 0)]) # self-loop + assert list(nx.simple_cycles(G)) == [[0]] + + def test_unsortable(self): + # this test ensures that graphs whose nodes without an intrinsic + # ordering do not cause issues + G = nx.DiGraph() + nx.add_cycle(G, ["a", 1]) + c = list(nx.simple_cycles(G)) + assert len(c) == 1 + + def test_simple_cycles_small(self): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3]) + c = sorted(nx.simple_cycles(G)) + assert len(c) == 1 + assert self.is_cyclic_permutation(c[0], [1, 2, 3]) + nx.add_cycle(G, [10, 20, 30]) + cc = sorted(nx.simple_cycles(G)) + assert len(cc) == 2 + ca = [[1, 2, 3], [10, 20, 30]] + for c in cc: + assert any(self.is_cyclic_permutation(c, rc) for rc in ca) + + def test_simple_cycles_empty(self): + G = nx.DiGraph() + assert list(nx.simple_cycles(G)) == [] + + def worst_case_graph(self, k): + # see figure 1 in Johnson's paper + # this graph has exactly 3k simple cycles + G = nx.DiGraph() + for n in range(2, k + 2): + G.add_edge(1, n) + G.add_edge(n, k + 2) + G.add_edge(2 * k + 1, 1) + for n in range(k + 2, 2 * k + 2): + G.add_edge(n, 2 * k + 2) + G.add_edge(n, n + 1) + G.add_edge(2 * k + 3, k + 2) + for n in range(2 * k + 3, 3 * k + 3): + G.add_edge(2 * k + 2, n) + G.add_edge(n, 3 * k + 3) + G.add_edge(3 * k + 3, 2 * k + 2) + return G + + def test_worst_case_graph(self): + # see figure 1 in Johnson's paper + for k in range(3, 10): + G = self.worst_case_graph(k) + l = len(list(nx.simple_cycles(G))) + assert l == 3 * k + + def test_recursive_simple_and_not(self): + for k in range(2, 10): + G = self.worst_case_graph(k) + cc = sorted(nx.simple_cycles(G)) + rcc = sorted(nx.recursive_simple_cycles(G)) + assert len(cc) == len(rcc) + for c in cc: + assert any(self.is_cyclic_permutation(c, r) for r in rcc) + for rc in rcc: + assert any(self.is_cyclic_permutation(rc, c) for c in cc) + + def test_simple_graph_with_reported_bug(self): + G = nx.DiGraph() + edges = [ + (0, 2), + (0, 3), + (1, 0), + (1, 3), + (2, 1), + (2, 4), + (3, 2), + (3, 4), + (4, 0), + (4, 1), + (4, 5), + (5, 0), + (5, 1), + (5, 2), + (5, 3), + ] + G.add_edges_from(edges) + cc = sorted(nx.simple_cycles(G)) + assert len(cc) == 26 + rcc = sorted(nx.recursive_simple_cycles(G)) + assert len(cc) == len(rcc) + for c in cc: + assert any(self.is_cyclic_permutation(c, rc) for rc in rcc) + for rc in rcc: + assert any(self.is_cyclic_permutation(rc, c) for c in cc) + + +def pairwise(iterable): + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def cycle_edges(c): + return pairwise(chain(c, islice(c, 1))) + + +def directed_cycle_edgeset(c): + return frozenset(cycle_edges(c)) + + +def undirected_cycle_edgeset(c): + if len(c) == 1: + return frozenset(cycle_edges(c)) + return frozenset(map(frozenset, cycle_edges(c))) + + +def multigraph_cycle_edgeset(c): + if len(c) <= 2: + return frozenset(cycle_edges(c)) + else: + return frozenset(map(frozenset, cycle_edges(c))) + + +class TestCycleEnumeration: + @staticmethod + def K(n): + return nx.complete_graph(n) + + @staticmethod + def D(n): + return nx.complete_graph(n).to_directed() + + @staticmethod + def edgeset_function(g): + if g.is_directed(): + return directed_cycle_edgeset + elif g.is_multigraph(): + return multigraph_cycle_edgeset + else: + return undirected_cycle_edgeset + + def check_cycle(self, g, c, es, cache, source, original_c, length_bound, chordless): + if length_bound is not None and len(c) > length_bound: + raise RuntimeError( + f"computed cycle {original_c} exceeds length bound {length_bound}" + ) + if source == "computed": + if es in cache: + raise RuntimeError( + f"computed cycle {original_c} has already been found!" + ) + else: + cache[es] = tuple(original_c) + else: + if es in cache: + cache.pop(es) + else: + raise RuntimeError(f"expected cycle {original_c} was not computed") + + if not all(g.has_edge(*e) for e in es): + raise RuntimeError( + f"{source} claimed cycle {original_c} is not a cycle of g" + ) + if chordless and len(g.subgraph(c).edges) > len(c): + raise RuntimeError(f"{source} cycle {original_c} is not chordless") + + def check_cycle_algorithm( + self, + g, + expected_cycles, + length_bound=None, + chordless=False, + algorithm=None, + ): + if algorithm is None: + algorithm = nx.chordless_cycles if chordless else nx.simple_cycles + + # note: we shuffle the labels of g to rule out accidentally-correct + # behavior which occurred during the development of chordless cycle + # enumeration algorithms + + relabel = list(range(len(g))) + rng = random.Random(42) + rng.shuffle(relabel) + label = dict(zip(g, relabel)) + unlabel = dict(zip(relabel, g)) + h = nx.relabel_nodes(g, label, copy=True) + + edgeset = self.edgeset_function(h) + + params = {} + if length_bound is not None: + params["length_bound"] = length_bound + + cycle_cache = {} + for c in algorithm(h, **params): + original_c = [unlabel[x] for x in c] + es = edgeset(c) + self.check_cycle( + h, c, es, cycle_cache, "computed", original_c, length_bound, chordless + ) + + if isinstance(expected_cycles, int): + if len(cycle_cache) != expected_cycles: + raise RuntimeError( + f"expected {expected_cycles} cycles, got {len(cycle_cache)}" + ) + return + for original_c in expected_cycles: + c = [label[x] for x in original_c] + es = edgeset(c) + self.check_cycle( + h, c, es, cycle_cache, "expected", original_c, length_bound, chordless + ) + + if len(cycle_cache): + for c in cycle_cache.values(): + raise RuntimeError( + f"computed cycle {c} is valid but not in the expected cycle set!" + ) + + def check_cycle_enumeration_integer_sequence( + self, + g_family, + cycle_counts, + length_bound=None, + chordless=False, + algorithm=None, + ): + for g, num_cycles in zip(g_family, cycle_counts): + self.check_cycle_algorithm( + g, + num_cycles, + length_bound=length_bound, + chordless=chordless, + algorithm=algorithm, + ) + + def test_directed_chordless_cycle_digons(self): + g = nx.DiGraph() + nx.add_cycle(g, range(5)) + nx.add_cycle(g, range(5)[::-1]) + g.add_edge(0, 0) + expected_cycles = [(0,), (1, 2), (2, 3), (3, 4)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=2) + + expected_cycles = [c for c in expected_cycles if len(c) < 2] + self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=1) + + def test_chordless_cycles_multigraph_self_loops(self): + G = nx.MultiGraph([(1, 1), (2, 2), (1, 2), (1, 2)]) + expected_cycles = [[1], [2]] + self.check_cycle_algorithm(G, expected_cycles, chordless=True) + + G.add_edges_from([(2, 3), (3, 4), (3, 4), (1, 3)]) + expected_cycles = [[1], [2], [3, 4]] + self.check_cycle_algorithm(G, expected_cycles, chordless=True) + + def test_directed_chordless_cycle_undirected(self): + g = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (5, 0), (5, 1), (0, 2)]) + expected_cycles = [(0, 2, 3, 4, 5), (1, 2, 3, 4, 5)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + g = nx.DiGraph() + nx.add_cycle(g, range(5)) + nx.add_cycle(g, range(4, 9)) + g.add_edge(7, 3) + expected_cycles = [(0, 1, 2, 3, 4), (3, 4, 5, 6, 7), (4, 5, 6, 7, 8)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + g.add_edge(3, 7) + expected_cycles = [(0, 1, 2, 3, 4), (3, 7), (4, 5, 6, 7, 8)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + expected_cycles = [(3, 7)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True, length_bound=4) + + g.remove_edge(7, 3) + expected_cycles = [(0, 1, 2, 3, 4), (4, 5, 6, 7, 8)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + g = nx.DiGraph((i, j) for i in range(10) for j in range(i)) + expected_cycles = [] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + def test_chordless_cycles_directed(self): + G = nx.DiGraph() + nx.add_cycle(G, range(5)) + nx.add_cycle(G, range(4, 12)) + expected = [[*range(5)], [*range(4, 12)]] + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + G.add_edge(7, 3) + expected.append([*range(3, 8)]) + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + G.add_edge(3, 7) + expected[-1] = [7, 3] + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + expected.pop() + G.remove_edge(7, 3) + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + def test_directed_chordless_cycle_diclique(self): + g_family = [self.D(n) for n in range(10)] + expected_cycles = [(n * n - n) // 2 for n in range(10)] + self.check_cycle_enumeration_integer_sequence( + g_family, expected_cycles, chordless=True + ) + + expected_cycles = [(n * n - n) // 2 for n in range(10)] + self.check_cycle_enumeration_integer_sequence( + g_family, expected_cycles, length_bound=2 + ) + + def test_directed_chordless_loop_blockade(self): + g = nx.DiGraph((i, i) for i in range(10)) + nx.add_cycle(g, range(10)) + expected_cycles = [(i,) for i in range(10)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + self.check_cycle_algorithm(g, expected_cycles, length_bound=1) + + g = nx.MultiDiGraph(g) + g.add_edges_from((i, i) for i in range(0, 10, 2)) + expected_cycles = [(i,) for i in range(1, 10, 2)] + self.check_cycle_algorithm(g, expected_cycles, chordless=True) + + def test_simple_cycles_notable_clique_sequences(self): + # A000292: Number of labeled graphs on n+3 nodes that are triangles. + g_family = [self.K(n) for n in range(2, 12)] + expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, length_bound=3 + ) + + def triangles(g, **kwargs): + yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 3) + + # directed complete graphs have twice as many triangles thanks to reversal + g_family = [self.D(n) for n in range(2, 12)] + expected = [2 * e for e in expected] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, length_bound=3, algorithm=triangles + ) + + def four_cycles(g, **kwargs): + yield from (c for c in nx.simple_cycles(g, **kwargs) if len(c) == 4) + + # A050534: the number of 4-cycles in the complete graph K_{n+1} + expected = [0, 0, 0, 3, 15, 45, 105, 210, 378, 630, 990] + g_family = [self.K(n) for n in range(1, 12)] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, length_bound=4, algorithm=four_cycles + ) + + # directed complete graphs have twice as many 4-cycles thanks to reversal + expected = [2 * e for e in expected] + g_family = [self.D(n) for n in range(1, 15)] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, length_bound=4, algorithm=four_cycles + ) + + # A006231: the number of elementary circuits in a complete directed graph with n nodes + expected = [0, 1, 5, 20, 84, 409, 2365] + g_family = [self.D(n) for n in range(1, 8)] + self.check_cycle_enumeration_integer_sequence(g_family, expected) + + # A002807: Number of cycles in the complete graph on n nodes K_{n}. + expected = [0, 0, 0, 1, 7, 37, 197, 1172] + g_family = [self.K(n) for n in range(8)] + self.check_cycle_enumeration_integer_sequence(g_family, expected) + + def test_directed_chordless_cycle_parallel_multiedges(self): + g = nx.MultiGraph() + + nx.add_cycle(g, range(5)) + expected = [[*range(5)]] + self.check_cycle_algorithm(g, expected, chordless=True) + + nx.add_cycle(g, range(5)) + expected = [*cycle_edges(range(5))] + self.check_cycle_algorithm(g, expected, chordless=True) + + nx.add_cycle(g, range(5)) + expected = [] + self.check_cycle_algorithm(g, expected, chordless=True) + + g = nx.MultiDiGraph() + + nx.add_cycle(g, range(5)) + expected = [[*range(5)]] + self.check_cycle_algorithm(g, expected, chordless=True) + + nx.add_cycle(g, range(5)) + self.check_cycle_algorithm(g, [], chordless=True) + + nx.add_cycle(g, range(5)) + self.check_cycle_algorithm(g, [], chordless=True) + + g = nx.MultiDiGraph() + + nx.add_cycle(g, range(5)) + nx.add_cycle(g, range(5)[::-1]) + expected = [*cycle_edges(range(5))] + self.check_cycle_algorithm(g, expected, chordless=True) + + nx.add_cycle(g, range(5)) + self.check_cycle_algorithm(g, [], chordless=True) + + def test_chordless_cycles_graph(self): + G = nx.Graph() + nx.add_cycle(G, range(5)) + nx.add_cycle(G, range(4, 12)) + expected = [[*range(5)], [*range(4, 12)]] + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + G.add_edge(7, 3) + expected.append([*range(3, 8)]) + expected.append([4, 3, 7, 8, 9, 10, 11]) + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 5], length_bound=5, chordless=True + ) + + def test_chordless_cycles_giant_hamiltonian(self): + # ... o - e - o - e - o ... # o = odd, e = even + # ... ---/ \-----/ \--- ... # <-- "long" edges + # + # each long edge belongs to exactly one triangle, and one giant cycle + # of length n/2. The remaining edges each belong to a triangle + + n = 1000 + assert n % 2 == 0 + G = nx.Graph() + for v in range(n): + if not v % 2: + G.add_edge(v, (v + 2) % n) + G.add_edge(v, (v + 1) % n) + + expected = [[*range(0, n, 2)]] + [ + [x % n for x in range(i, i + 3)] for i in range(0, n, 2) + ] + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True + ) + + # ... o -> e -> o -> e -> o ... # o = odd, e = even + # ... <---/ \---<---/ \---< ... # <-- "long" edges + # + # this time, we orient the short and long edges in opposition + # the cycle structure of this graph is the same, but we need to reverse + # the long one in our representation. Also, we need to drop the size + # because our partitioning algorithm uses strongly connected components + # instead of separating graphs by their strong articulation points + + n = 100 + assert n % 2 == 0 + G = nx.DiGraph() + for v in range(n): + G.add_edge(v, (v + 1) % n) + if not v % 2: + G.add_edge((v + 2) % n, v) + + expected = [[*range(n - 2, -2, -2)]] + [ + [x % n for x in range(i, i + 3)] for i in range(0, n, 2) + ] + self.check_cycle_algorithm(G, expected, chordless=True) + self.check_cycle_algorithm( + G, [c for c in expected if len(c) <= 3], length_bound=3, chordless=True + ) + + def test_simple_cycles_acyclic_tournament(self): + n = 10 + G = nx.DiGraph((x, y) for x in range(n) for y in range(x)) + self.check_cycle_algorithm(G, []) + self.check_cycle_algorithm(G, [], chordless=True) + + for k in range(n + 1): + self.check_cycle_algorithm(G, [], length_bound=k) + self.check_cycle_algorithm(G, [], length_bound=k, chordless=True) + + def test_simple_cycles_graph(self): + testG = nx.cycle_graph(8) + cyc1 = tuple(range(8)) + self.check_cycle_algorithm(testG, [cyc1]) + + testG.add_edge(4, -1) + nx.add_path(testG, [3, -2, -3, -4]) + self.check_cycle_algorithm(testG, [cyc1]) + + testG.update(nx.cycle_graph(range(8, 16))) + cyc2 = tuple(range(8, 16)) + self.check_cycle_algorithm(testG, [cyc1, cyc2]) + + testG.update(nx.cycle_graph(range(4, 12))) + cyc3 = tuple(range(4, 12)) + expected = { + (0, 1, 2, 3, 4, 5, 6, 7), # cyc1 + (8, 9, 10, 11, 12, 13, 14, 15), # cyc2 + (4, 5, 6, 7, 8, 9, 10, 11), # cyc3 + (4, 5, 6, 7, 8, 15, 14, 13, 12, 11), # cyc2 + cyc3 + (0, 1, 2, 3, 4, 11, 10, 9, 8, 7), # cyc1 + cyc3 + (0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7), # cyc1 + cyc2 + cyc3 + } + self.check_cycle_algorithm(testG, expected) + assert len(expected) == (2**3 - 1) - 1 # 1 disjoint comb: cyc1 + cyc2 + + # Basis size = 5 (2 loops overlapping gives 5 small loops + # E + # / \ Note: A-F = 10-15 + # 1-2-3-4-5 + # / | | \ cyc1=012DAB -- left + # 0 D F 6 cyc2=234E -- top + # \ | | / cyc3=45678F -- right + # B-A-9-8-7 cyc4=89AC -- bottom + # \ / cyc5=234F89AD -- middle + # C + # + # combinations of 5 basis elements: 2^5 - 1 (one includes no cycles) + # + # disjoint combs: (11 total) not simple cycles + # Any pair not including cyc5 => choose(4, 2) = 6 + # Any triple not including cyc5 => choose(4, 3) = 4 + # Any quad not including cyc5 => choose(4, 4) = 1 + # + # we expect 31 - 11 = 20 simple cycles + # + testG = nx.cycle_graph(12) + testG.update(nx.cycle_graph([12, 10, 13, 2, 14, 4, 15, 8]).edges) + expected = (2**5 - 1) - 11 # 11 disjoint combinations + self.check_cycle_algorithm(testG, expected) + + def test_simple_cycles_bounded(self): + # iteratively construct a cluster of nested cycles running in the same direction + # there should be one cycle of every length + d = nx.DiGraph() + expected = [] + for n in range(10): + nx.add_cycle(d, range(n)) + expected.append(n) + for k, e in enumerate(expected): + self.check_cycle_algorithm(d, e, length_bound=k) + + # iteratively construct a path of undirected cycles, connected at articulation + # points. there should be one cycle of every length except 2: no digons + g = nx.Graph() + top = 0 + expected = [] + for n in range(10): + expected.append(n if n < 2 else n - 1) + if n == 2: + # no digons in undirected graphs + continue + nx.add_cycle(g, range(top, top + n)) + top += n + for k, e in enumerate(expected): + self.check_cycle_algorithm(g, e, length_bound=k) + + def test_simple_cycles_bound_corner_cases(self): + G = nx.cycle_graph(4) + DG = nx.cycle_graph(4, create_using=nx.DiGraph) + assert list(nx.simple_cycles(G, length_bound=0)) == [] + assert list(nx.simple_cycles(DG, length_bound=0)) == [] + assert list(nx.chordless_cycles(G, length_bound=0)) == [] + assert list(nx.chordless_cycles(DG, length_bound=0)) == [] + + def test_simple_cycles_bound_error(self): + with pytest.raises(ValueError): + G = nx.DiGraph() + for c in nx.simple_cycles(G, -1): + assert False + + with pytest.raises(ValueError): + G = nx.Graph() + for c in nx.simple_cycles(G, -1): + assert False + + with pytest.raises(ValueError): + G = nx.Graph() + for c in nx.chordless_cycles(G, -1): + assert False + + with pytest.raises(ValueError): + G = nx.DiGraph() + for c in nx.chordless_cycles(G, -1): + assert False + + def test_chordless_cycles_clique(self): + g_family = [self.K(n) for n in range(2, 15)] + expected = [0, 1, 4, 10, 20, 35, 56, 84, 120, 165, 220, 286, 364] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, chordless=True + ) + + # directed cliques have as many digons as undirected graphs have edges + expected = [(n * n - n) // 2 for n in range(15)] + g_family = [self.D(n) for n in range(15)] + self.check_cycle_enumeration_integer_sequence( + g_family, expected, chordless=True + ) + + +# These tests might fail with hash randomization since they depend on +# edge_dfs. For more information, see the comments in: +# networkx/algorithms/traversal/tests/test_edgedfs.py + + +class TestFindCycle: + @classmethod + def setup_class(cls): + cls.nodes = [0, 1, 2, 3] + cls.edges = [(-1, 0), (0, 1), (1, 0), (1, 0), (2, 1), (3, 1)] + + def test_graph_nocycle(self): + G = nx.Graph(self.edges) + pytest.raises(nx.exception.NetworkXNoCycle, nx.find_cycle, G, self.nodes) + + def test_graph_cycle(self): + G = nx.Graph(self.edges) + G.add_edge(2, 0) + x = list(nx.find_cycle(G, self.nodes)) + x_ = [(0, 1), (1, 2), (2, 0)] + assert x == x_ + + def test_graph_orientation_none(self): + G = nx.Graph(self.edges) + G.add_edge(2, 0) + x = list(nx.find_cycle(G, self.nodes, orientation=None)) + x_ = [(0, 1), (1, 2), (2, 0)] + assert x == x_ + + def test_graph_orientation_original(self): + G = nx.Graph(self.edges) + G.add_edge(2, 0) + x = list(nx.find_cycle(G, self.nodes, orientation="original")) + x_ = [(0, 1, FORWARD), (1, 2, FORWARD), (2, 0, FORWARD)] + assert x == x_ + + def test_digraph(self): + G = nx.DiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes)) + x_ = [(0, 1), (1, 0)] + assert x == x_ + + def test_digraph_orientation_none(self): + G = nx.DiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes, orientation=None)) + x_ = [(0, 1), (1, 0)] + assert x == x_ + + def test_digraph_orientation_original(self): + G = nx.DiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes, orientation="original")) + x_ = [(0, 1, FORWARD), (1, 0, FORWARD)] + assert x == x_ + + def test_multigraph(self): + G = nx.MultiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes)) + x_ = [(0, 1, 0), (1, 0, 1)] # or (1, 0, 2) + # Hash randomization...could be any edge. + assert x[0] == x_[0] + assert x[1][:2] == x_[1][:2] + + def test_multidigraph(self): + G = nx.MultiDiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes)) + x_ = [(0, 1, 0), (1, 0, 0)] # (1, 0, 1) + assert x[0] == x_[0] + assert x[1][:2] == x_[1][:2] + + def test_digraph_ignore(self): + G = nx.DiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes, orientation="ignore")) + x_ = [(0, 1, FORWARD), (1, 0, FORWARD)] + assert x == x_ + + def test_digraph_reverse(self): + G = nx.DiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes, orientation="reverse")) + x_ = [(1, 0, REVERSE), (0, 1, REVERSE)] + assert x == x_ + + def test_multidigraph_ignore(self): + G = nx.MultiDiGraph(self.edges) + x = list(nx.find_cycle(G, self.nodes, orientation="ignore")) + x_ = [(0, 1, 0, FORWARD), (1, 0, 0, FORWARD)] # or (1, 0, 1, 1) + assert x[0] == x_[0] + assert x[1][:2] == x_[1][:2] + assert x[1][3] == x_[1][3] + + def test_multidigraph_ignore2(self): + # Loop traversed an edge while ignoring its orientation. + G = nx.MultiDiGraph([(0, 1), (1, 2), (1, 2)]) + x = list(nx.find_cycle(G, [0, 1, 2], orientation="ignore")) + x_ = [(1, 2, 0, FORWARD), (1, 2, 1, REVERSE)] + assert x == x_ + + def test_multidigraph_original(self): + # Node 2 doesn't need to be searched again from visited from 4. + # The goal here is to cover the case when 2 to be researched from 4, + # when 4 is visited from the first time (so we must make sure that 4 + # is not visited from 2, and hence, we respect the edge orientation). + G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 3), (4, 2)]) + pytest.raises( + nx.exception.NetworkXNoCycle, + nx.find_cycle, + G, + [0, 1, 2, 3, 4], + orientation="original", + ) + + def test_dag(self): + G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) + pytest.raises( + nx.exception.NetworkXNoCycle, nx.find_cycle, G, orientation="original" + ) + x = list(nx.find_cycle(G, orientation="ignore")) + assert x == [(0, 1, FORWARD), (1, 2, FORWARD), (0, 2, REVERSE)] + + def test_prev_explored(self): + # https://github.com/networkx/networkx/issues/2323 + + G = nx.DiGraph() + G.add_edges_from([(1, 0), (2, 0), (1, 2), (2, 1)]) + pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0) + x = list(nx.find_cycle(G, 1)) + x_ = [(1, 2), (2, 1)] + assert x == x_ + + x = list(nx.find_cycle(G, 2)) + x_ = [(2, 1), (1, 2)] + assert x == x_ + + x = list(nx.find_cycle(G)) + x_ = [(1, 2), (2, 1)] + assert x == x_ + + def test_no_cycle(self): + # https://github.com/networkx/networkx/issues/2439 + + G = nx.DiGraph() + G.add_edges_from([(1, 2), (2, 0), (3, 1), (3, 2)]) + pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G, source=0) + pytest.raises(nx.NetworkXNoCycle, nx.find_cycle, G) + + +def assert_basis_equal(a, b): + assert sorted(a) == sorted(b) + + +class TestMinimumCycleBasis: + @classmethod + def setup_class(cls): + T = nx.Graph() + nx.add_cycle(T, [1, 2, 3, 4], weight=1) + T.add_edge(2, 4, weight=5) + cls.diamond_graph = T + + def test_unweighted_diamond(self): + mcb = nx.minimum_cycle_basis(self.diamond_graph) + assert_basis_equal(mcb, [[2, 4, 1], [3, 4, 2]]) + + def test_weighted_diamond(self): + mcb = nx.minimum_cycle_basis(self.diamond_graph, weight="weight") + assert_basis_equal(mcb, [[2, 4, 1], [4, 3, 2, 1]]) + + def test_dimensionality(self): + # checks |MCB|=|E|-|V|+|NC| + ntrial = 10 + for seed in range(1234, 1234 + ntrial): + rg = nx.erdos_renyi_graph(10, 0.3, seed=seed) + nnodes = rg.number_of_nodes() + nedges = rg.number_of_edges() + ncomp = nx.number_connected_components(rg) + + mcb = nx.minimum_cycle_basis(rg) + assert len(mcb) == nedges - nnodes + ncomp + check_independent(mcb) + + def test_complete_graph(self): + cg = nx.complete_graph(5) + mcb = nx.minimum_cycle_basis(cg) + assert all(len(cycle) == 3 for cycle in mcb) + check_independent(mcb) + + def test_tree_graph(self): + tg = nx.balanced_tree(3, 3) + assert not nx.minimum_cycle_basis(tg) + + def test_petersen_graph(self): + G = nx.petersen_graph() + mcb = list(nx.minimum_cycle_basis(G)) + expected = [ + [4, 9, 7, 5, 0], + [1, 2, 3, 4, 0], + [1, 6, 8, 5, 0], + [4, 3, 8, 5, 0], + [1, 6, 9, 4, 0], + [1, 2, 7, 5, 0], + ] + assert len(mcb) == len(expected) + assert all(c in expected for c in mcb) + + # check that order of the nodes is a path + for c in mcb: + assert all(G.has_edge(u, v) for u, v in nx.utils.pairwise(c, cyclic=True)) + # check independence of the basis + check_independent(mcb) + + def test_gh6787_variable_weighted_complete_graph(self): + N = 8 + cg = nx.complete_graph(N) + cg.add_weighted_edges_from([(u, v, 9) for u, v in cg.edges]) + cg.add_weighted_edges_from([(u, v, 1) for u, v in nx.cycle_graph(N).edges]) + mcb = nx.minimum_cycle_basis(cg, weight="weight") + check_independent(mcb) + + def test_gh6787_and_edge_attribute_names(self): + G = nx.cycle_graph(4) + G.add_weighted_edges_from([(0, 2, 10), (1, 3, 10)], weight="dist") + expected = [[1, 3, 0], [3, 2, 1, 0], [1, 2, 0]] + mcb = list(nx.minimum_cycle_basis(G, weight="dist")) + assert len(mcb) == len(expected) + assert all(c in expected for c in mcb) + + # test not using a weight with weight attributes + expected = [[1, 3, 0], [1, 2, 0], [3, 2, 0]] + mcb = list(nx.minimum_cycle_basis(G)) + assert len(mcb) == len(expected) + assert all(c in expected for c in mcb) + + +class TestGirth: + @pytest.mark.parametrize( + ("G", "expected"), + ( + (nx.chvatal_graph(), 4), + (nx.tutte_graph(), 4), + (nx.petersen_graph(), 5), + (nx.heawood_graph(), 6), + (nx.pappus_graph(), 6), + (nx.random_labeled_tree(10, seed=42), inf), + (nx.empty_graph(10), inf), + (nx.Graph(chain(cycle_edges(range(5)), cycle_edges(range(6, 10)))), 4), + ( + nx.Graph( + [ + (0, 6), + (0, 8), + (0, 9), + (1, 8), + (2, 8), + (2, 9), + (4, 9), + (5, 9), + (6, 8), + (6, 9), + (7, 8), + ] + ), + 3, + ), + ), + ) + def test_girth(self, G, expected): + assert nx.girth(G) == expected diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py new file mode 100644 index 0000000000000000000000000000000000000000..f7608295afa2e8e20116e5e3cc0b655b0f2a23d6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_d_separation.py @@ -0,0 +1,340 @@ +from itertools import combinations + +import pytest + +import networkx as nx + + +def path_graph(): + """Return a path graph of length three.""" + G = nx.path_graph(3, create_using=nx.DiGraph) + G.graph["name"] = "path" + nx.freeze(G) + return G + + +def fork_graph(): + """Return a three node fork graph.""" + G = nx.DiGraph(name="fork") + G.add_edges_from([(0, 1), (0, 2)]) + nx.freeze(G) + return G + + +def collider_graph(): + """Return a collider/v-structure graph with three nodes.""" + G = nx.DiGraph(name="collider") + G.add_edges_from([(0, 2), (1, 2)]) + nx.freeze(G) + return G + + +def naive_bayes_graph(): + """Return a simply Naive Bayes PGM graph.""" + G = nx.DiGraph(name="naive_bayes") + G.add_edges_from([(0, 1), (0, 2), (0, 3), (0, 4)]) + nx.freeze(G) + return G + + +def asia_graph(): + """Return the 'Asia' PGM graph.""" + G = nx.DiGraph(name="asia") + G.add_edges_from( + [ + ("asia", "tuberculosis"), + ("smoking", "cancer"), + ("smoking", "bronchitis"), + ("tuberculosis", "either"), + ("cancer", "either"), + ("either", "xray"), + ("either", "dyspnea"), + ("bronchitis", "dyspnea"), + ] + ) + nx.freeze(G) + return G + + +@pytest.fixture(name="path_graph") +def path_graph_fixture(): + return path_graph() + + +@pytest.fixture(name="fork_graph") +def fork_graph_fixture(): + return fork_graph() + + +@pytest.fixture(name="collider_graph") +def collider_graph_fixture(): + return collider_graph() + + +@pytest.fixture(name="naive_bayes_graph") +def naive_bayes_graph_fixture(): + return naive_bayes_graph() + + +@pytest.fixture(name="asia_graph") +def asia_graph_fixture(): + return asia_graph() + + +@pytest.fixture() +def large_collider_graph(): + edge_list = [("A", "B"), ("C", "B"), ("B", "D"), ("D", "E"), ("B", "F"), ("G", "E")] + G = nx.DiGraph(edge_list) + return G + + +@pytest.fixture() +def chain_and_fork_graph(): + edge_list = [("A", "B"), ("B", "C"), ("B", "D"), ("D", "C")] + G = nx.DiGraph(edge_list) + return G + + +@pytest.fixture() +def no_separating_set_graph(): + edge_list = [("A", "B")] + G = nx.DiGraph(edge_list) + return G + + +@pytest.fixture() +def large_no_separating_set_graph(): + edge_list = [("A", "B"), ("C", "A"), ("C", "B")] + G = nx.DiGraph(edge_list) + return G + + +@pytest.fixture() +def collider_trek_graph(): + edge_list = [("A", "B"), ("C", "B"), ("C", "D")] + G = nx.DiGraph(edge_list) + return G + + +@pytest.mark.parametrize( + "graph", + [path_graph(), fork_graph(), collider_graph(), naive_bayes_graph(), asia_graph()], +) +def test_markov_condition(graph): + """Test that the Markov condition holds for each PGM graph.""" + for node in graph.nodes: + parents = set(graph.predecessors(node)) + non_descendants = graph.nodes - nx.descendants(graph, node) - {node} - parents + assert nx.is_d_separator(graph, {node}, non_descendants, parents) + + +def test_path_graph_dsep(path_graph): + """Example-based test of d-separation for path_graph.""" + assert nx.is_d_separator(path_graph, {0}, {2}, {1}) + assert not nx.is_d_separator(path_graph, {0}, {2}, set()) + + +def test_fork_graph_dsep(fork_graph): + """Example-based test of d-separation for fork_graph.""" + assert nx.is_d_separator(fork_graph, {1}, {2}, {0}) + assert not nx.is_d_separator(fork_graph, {1}, {2}, set()) + + +def test_collider_graph_dsep(collider_graph): + """Example-based test of d-separation for collider_graph.""" + assert nx.is_d_separator(collider_graph, {0}, {1}, set()) + assert not nx.is_d_separator(collider_graph, {0}, {1}, {2}) + + +def test_naive_bayes_dsep(naive_bayes_graph): + """Example-based test of d-separation for naive_bayes_graph.""" + for u, v in combinations(range(1, 5), 2): + assert nx.is_d_separator(naive_bayes_graph, {u}, {v}, {0}) + assert not nx.is_d_separator(naive_bayes_graph, {u}, {v}, set()) + + +def test_asia_graph_dsep(asia_graph): + """Example-based test of d-separation for asia_graph.""" + assert nx.is_d_separator( + asia_graph, {"asia", "smoking"}, {"dyspnea", "xray"}, {"bronchitis", "either"} + ) + assert nx.is_d_separator( + asia_graph, {"tuberculosis", "cancer"}, {"bronchitis"}, {"smoking", "xray"} + ) + + +def test_undirected_graphs_are_not_supported(): + """ + Test that undirected graphs are not supported. + + d-separation and its related algorithms do not apply in + the case of undirected graphs. + """ + g = nx.path_graph(3, nx.Graph) + with pytest.raises(nx.NetworkXNotImplemented): + nx.is_d_separator(g, {0}, {1}, {2}) + with pytest.raises(nx.NetworkXNotImplemented): + nx.is_minimal_d_separator(g, {0}, {1}, {2}) + with pytest.raises(nx.NetworkXNotImplemented): + nx.find_minimal_d_separator(g, {0}, {1}) + + +def test_cyclic_graphs_raise_error(): + """ + Test that cycle graphs should cause erroring. + + This is because PGMs assume a directed acyclic graph. + """ + g = nx.cycle_graph(3, nx.DiGraph) + with pytest.raises(nx.NetworkXError): + nx.is_d_separator(g, {0}, {1}, {2}) + with pytest.raises(nx.NetworkXError): + nx.find_minimal_d_separator(g, {0}, {1}) + with pytest.raises(nx.NetworkXError): + nx.is_minimal_d_separator(g, {0}, {1}, {2}) + + +def test_invalid_nodes_raise_error(asia_graph): + """ + Test that graphs that have invalid nodes passed in raise errors. + """ + # Check both set and node arguments + with pytest.raises(nx.NodeNotFound): + nx.is_d_separator(asia_graph, {0}, {1}, {2}) + with pytest.raises(nx.NodeNotFound): + nx.is_d_separator(asia_graph, 0, 1, 2) + with pytest.raises(nx.NodeNotFound): + nx.is_minimal_d_separator(asia_graph, {0}, {1}, {2}) + with pytest.raises(nx.NodeNotFound): + nx.is_minimal_d_separator(asia_graph, 0, 1, 2) + with pytest.raises(nx.NodeNotFound): + nx.find_minimal_d_separator(asia_graph, {0}, {1}) + with pytest.raises(nx.NodeNotFound): + nx.find_minimal_d_separator(asia_graph, 0, 1) + + +def test_nondisjoint_node_sets_raise_error(collider_graph): + """ + Test that error is raised when node sets aren't disjoint. + """ + with pytest.raises(nx.NetworkXError): + nx.is_d_separator(collider_graph, 0, 1, 0) + with pytest.raises(nx.NetworkXError): + nx.is_d_separator(collider_graph, 0, 2, 0) + with pytest.raises(nx.NetworkXError): + nx.is_d_separator(collider_graph, 0, 0, 1) + with pytest.raises(nx.NetworkXError): + nx.is_d_separator(collider_graph, 1, 0, 0) + with pytest.raises(nx.NetworkXError): + nx.find_minimal_d_separator(collider_graph, 0, 0) + with pytest.raises(nx.NetworkXError): + nx.find_minimal_d_separator(collider_graph, 0, 1, included=0) + with pytest.raises(nx.NetworkXError): + nx.find_minimal_d_separator(collider_graph, 1, 0, included=0) + with pytest.raises(nx.NetworkXError): + nx.is_minimal_d_separator(collider_graph, 0, 0, set()) + with pytest.raises(nx.NetworkXError): + nx.is_minimal_d_separator(collider_graph, 0, 1, set(), included=0) + with pytest.raises(nx.NetworkXError): + nx.is_minimal_d_separator(collider_graph, 1, 0, set(), included=0) + + +def test_is_minimal_d_separator( + large_collider_graph, + chain_and_fork_graph, + no_separating_set_graph, + large_no_separating_set_graph, + collider_trek_graph, +): + # Case 1: + # create a graph A -> B <- C + # B -> D -> E; + # B -> F; + # G -> E; + assert not nx.is_d_separator(large_collider_graph, {"B"}, {"E"}, set()) + + # minimal set of the corresponding graph + # for B and E should be (D,) + Zmin = nx.find_minimal_d_separator(large_collider_graph, "B", "E") + # check that the minimal d-separator is a d-separating set + assert nx.is_d_separator(large_collider_graph, "B", "E", Zmin) + # the minimal separating set should also pass the test for minimality + assert nx.is_minimal_d_separator(large_collider_graph, "B", "E", Zmin) + # function should also work with set arguments + assert nx.is_minimal_d_separator(large_collider_graph, {"A", "B"}, {"G", "E"}, Zmin) + assert Zmin == {"D"} + + # Case 2: + # create a graph A -> B -> C + # B -> D -> C; + assert not nx.is_d_separator(chain_and_fork_graph, {"A"}, {"C"}, set()) + Zmin = nx.find_minimal_d_separator(chain_and_fork_graph, "A", "C") + + # the minimal separating set should pass the test for minimality + assert nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Zmin) + assert Zmin == {"B"} + Znotmin = Zmin.union({"D"}) + assert not nx.is_minimal_d_separator(chain_and_fork_graph, "A", "C", Znotmin) + + # Case 3: + # create a graph A -> B + + # there is no m-separating set between A and B at all, so + # no minimal m-separating set can exist + assert not nx.is_d_separator(no_separating_set_graph, {"A"}, {"B"}, set()) + assert nx.find_minimal_d_separator(no_separating_set_graph, "A", "B") is None + + # Case 4: + # create a graph A -> B with A <- C -> B + + # there is no m-separating set between A and B at all, so + # no minimal m-separating set can exist + # however, the algorithm will initially propose C as a + # minimal (but invalid) separating set + assert not nx.is_d_separator(large_no_separating_set_graph, {"A"}, {"B"}, {"C"}) + assert nx.find_minimal_d_separator(large_no_separating_set_graph, "A", "B") is None + + # Test `included` and `excluded` args + # create graph A -> B <- C -> D + assert nx.find_minimal_d_separator(collider_trek_graph, "A", "D", included="B") == { + "B", + "C", + } + assert ( + nx.find_minimal_d_separator( + collider_trek_graph, "A", "D", included="B", restricted="B" + ) + is None + ) + + +def test_is_minimal_d_separator_checks_dsep(): + """Test that is_minimal_d_separator checks for d-separation as well.""" + g = nx.DiGraph() + g.add_edges_from( + [ + ("A", "B"), + ("A", "E"), + ("B", "C"), + ("B", "D"), + ("D", "C"), + ("D", "F"), + ("E", "D"), + ("E", "F"), + ] + ) + + assert not nx.is_d_separator(g, {"C"}, {"F"}, {"D"}) + + # since {'D'} and {} are not d-separators, we return false + assert not nx.is_minimal_d_separator(g, "C", "F", {"D"}) + assert not nx.is_minimal_d_separator(g, "C", "F", set()) + + +def test__reachable(large_collider_graph): + reachable = nx.algorithms.d_separation._reachable + g = large_collider_graph + x = {"F", "D"} + ancestors = {"A", "B", "C", "D", "F"} + assert reachable(g, x, ancestors, {"B"}) == {"B", "F", "D"} + assert reachable(g, x, ancestors, set()) == ancestors diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py new file mode 100644 index 0000000000000000000000000000000000000000..0c76be8119c44096703dbd276a3275a4c33bc5b6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dag.py @@ -0,0 +1,835 @@ +from collections import deque +from itertools import combinations, permutations + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, pairwise + + +# Recipe from the itertools documentation. +def _consume(iterator): + "Consume the iterator entirely." + # Feed the entire iterator into a zero-length deque. + deque(iterator, maxlen=0) + + +class TestDagLongestPath: + """Unit tests computing the longest path in a directed acyclic graph.""" + + def test_empty(self): + G = nx.DiGraph() + assert nx.dag_longest_path(G) == [] + + def test_unweighted1(self): + edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (3, 7)] + G = nx.DiGraph(edges) + assert nx.dag_longest_path(G) == [1, 2, 3, 5, 6] + + def test_unweighted2(self): + edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)] + G = nx.DiGraph(edges) + assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5] + + def test_weighted(self): + G = nx.DiGraph() + edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)] + G.add_weighted_edges_from(edges) + assert nx.dag_longest_path(G) == [2, 3, 5] + + def test_undirected_not_implemented(self): + G = nx.Graph() + pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path, G) + + def test_unorderable_nodes(self): + """Tests that computing the longest path does not depend on + nodes being orderable. + + For more information, see issue #1989. + + """ + # Create the directed path graph on four nodes in a diamond shape, + # with nodes represented as (unorderable) Python objects. + nodes = [object() for n in range(4)] + G = nx.DiGraph() + G.add_edge(nodes[0], nodes[1]) + G.add_edge(nodes[0], nodes[2]) + G.add_edge(nodes[2], nodes[3]) + G.add_edge(nodes[1], nodes[3]) + + # this will raise NotImplementedError when nodes need to be ordered + nx.dag_longest_path(G) + + def test_multigraph_unweighted(self): + edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)] + G = nx.MultiDiGraph(edges) + assert nx.dag_longest_path(G) == [1, 2, 3, 4, 5] + + def test_multigraph_weighted(self): + G = nx.MultiDiGraph() + edges = [ + (1, 2, 2), + (2, 3, 2), + (1, 3, 1), + (1, 3, 5), + (1, 3, 2), + ] + G.add_weighted_edges_from(edges) + assert nx.dag_longest_path(G) == [1, 3] + + def test_multigraph_weighted_default_weight(self): + G = nx.MultiDiGraph([(1, 2), (2, 3)]) # Unweighted edges + G.add_weighted_edges_from([(1, 3, 1), (1, 3, 5), (1, 3, 2)]) + + # Default value for default weight is 1 + assert nx.dag_longest_path(G) == [1, 3] + assert nx.dag_longest_path(G, default_weight=3) == [1, 2, 3] + + +class TestDagLongestPathLength: + """Unit tests for computing the length of a longest path in a + directed acyclic graph. + + """ + + def test_unweighted(self): + edges = [(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)] + G = nx.DiGraph(edges) + assert nx.dag_longest_path_length(G) == 4 + + edges = [(1, 2), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)] + G = nx.DiGraph(edges) + assert nx.dag_longest_path_length(G) == 4 + + # test degenerate graphs + G = nx.DiGraph() + G.add_node(1) + assert nx.dag_longest_path_length(G) == 0 + + def test_undirected_not_implemented(self): + G = nx.Graph() + pytest.raises(nx.NetworkXNotImplemented, nx.dag_longest_path_length, G) + + def test_weighted(self): + edges = [(1, 2, -5), (2, 3, 1), (3, 4, 1), (4, 5, 0), (3, 5, 4), (1, 6, 2)] + G = nx.DiGraph() + G.add_weighted_edges_from(edges) + assert nx.dag_longest_path_length(G) == 5 + + def test_multigraph_unweighted(self): + edges = [(1, 2), (2, 3), (2, 3), (3, 4), (4, 5), (1, 3), (1, 5), (3, 5)] + G = nx.MultiDiGraph(edges) + assert nx.dag_longest_path_length(G) == 4 + + def test_multigraph_weighted(self): + G = nx.MultiDiGraph() + edges = [ + (1, 2, 2), + (2, 3, 2), + (1, 3, 1), + (1, 3, 5), + (1, 3, 2), + ] + G.add_weighted_edges_from(edges) + assert nx.dag_longest_path_length(G) == 5 + + +class TestDAG: + @classmethod + def setup_class(cls): + pass + + def test_topological_sort1(self): + DG = nx.DiGraph([(1, 2), (1, 3), (2, 3)]) + + for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]: + assert tuple(algorithm(DG)) == (1, 2, 3) + + DG.add_edge(3, 2) + + for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]: + pytest.raises(nx.NetworkXUnfeasible, _consume, algorithm(DG)) + + DG.remove_edge(2, 3) + + for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]: + assert tuple(algorithm(DG)) == (1, 3, 2) + + DG.remove_edge(3, 2) + + assert tuple(nx.topological_sort(DG)) in {(1, 2, 3), (1, 3, 2)} + assert tuple(nx.lexicographical_topological_sort(DG)) == (1, 2, 3) + + def test_is_directed_acyclic_graph(self): + G = nx.generators.complete_graph(2) + assert not nx.is_directed_acyclic_graph(G) + assert not nx.is_directed_acyclic_graph(G.to_directed()) + assert not nx.is_directed_acyclic_graph(nx.Graph([(3, 4), (4, 5)])) + assert nx.is_directed_acyclic_graph(nx.DiGraph([(3, 4), (4, 5)])) + + def test_topological_sort2(self): + DG = nx.DiGraph( + { + 1: [2], + 2: [3], + 3: [4], + 4: [5], + 5: [1], + 11: [12], + 12: [13], + 13: [14], + 14: [15], + } + ) + pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG)) + + assert not nx.is_directed_acyclic_graph(DG) + + DG.remove_edge(1, 2) + _consume(nx.topological_sort(DG)) + assert nx.is_directed_acyclic_graph(DG) + + def test_topological_sort3(self): + DG = nx.DiGraph() + DG.add_edges_from([(1, i) for i in range(2, 5)]) + DG.add_edges_from([(2, i) for i in range(5, 9)]) + DG.add_edges_from([(6, i) for i in range(9, 12)]) + DG.add_edges_from([(4, i) for i in range(12, 15)]) + + def validate(order): + assert isinstance(order, list) + assert set(order) == set(DG) + for u, v in combinations(order, 2): + assert not nx.has_path(DG, v, u) + + validate(list(nx.topological_sort(DG))) + + DG.add_edge(14, 1) + pytest.raises(nx.NetworkXUnfeasible, _consume, nx.topological_sort(DG)) + + def test_topological_sort4(self): + G = nx.Graph() + G.add_edge(1, 2) + # Only directed graphs can be topologically sorted. + pytest.raises(nx.NetworkXError, _consume, nx.topological_sort(G)) + + def test_topological_sort5(self): + G = nx.DiGraph() + G.add_edge(0, 1) + assert list(nx.topological_sort(G)) == [0, 1] + + def test_topological_sort6(self): + for algorithm in [nx.topological_sort, nx.lexicographical_topological_sort]: + + def runtime_error(): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + first = True + for x in algorithm(DG): + if first: + first = False + DG.add_edge(5 - x, 5) + + def unfeasible_error(): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + first = True + for x in algorithm(DG): + if first: + first = False + DG.remove_node(4) + + def runtime_error2(): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + first = True + for x in algorithm(DG): + if first: + first = False + DG.remove_node(2) + + pytest.raises(RuntimeError, runtime_error) + pytest.raises(RuntimeError, runtime_error2) + pytest.raises(nx.NetworkXUnfeasible, unfeasible_error) + + def test_all_topological_sorts_1(self): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 5)]) + assert list(nx.all_topological_sorts(DG)) == [[1, 2, 3, 4, 5]] + + def test_all_topological_sorts_2(self): + DG = nx.DiGraph([(1, 3), (2, 1), (2, 4), (4, 3), (4, 5)]) + assert sorted(nx.all_topological_sorts(DG)) == [ + [2, 1, 4, 3, 5], + [2, 1, 4, 5, 3], + [2, 4, 1, 3, 5], + [2, 4, 1, 5, 3], + [2, 4, 5, 1, 3], + ] + + def test_all_topological_sorts_3(self): + def unfeasible(): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 4), (4, 2), (4, 5)]) + # convert to list to execute generator + list(nx.all_topological_sorts(DG)) + + def not_implemented(): + G = nx.Graph([(1, 2), (2, 3)]) + # convert to list to execute generator + list(nx.all_topological_sorts(G)) + + def not_implemented_2(): + G = nx.MultiGraph([(1, 2), (1, 2), (2, 3)]) + list(nx.all_topological_sorts(G)) + + pytest.raises(nx.NetworkXUnfeasible, unfeasible) + pytest.raises(nx.NetworkXNotImplemented, not_implemented) + pytest.raises(nx.NetworkXNotImplemented, not_implemented_2) + + def test_all_topological_sorts_4(self): + DG = nx.DiGraph() + for i in range(7): + DG.add_node(i) + assert sorted(map(list, permutations(DG.nodes))) == sorted( + nx.all_topological_sorts(DG) + ) + + def test_all_topological_sorts_multigraph_1(self): + DG = nx.MultiDiGraph([(1, 2), (1, 2), (2, 3), (3, 4), (3, 5), (3, 5), (3, 5)]) + assert sorted(nx.all_topological_sorts(DG)) == sorted( + [[1, 2, 3, 4, 5], [1, 2, 3, 5, 4]] + ) + + def test_all_topological_sorts_multigraph_2(self): + N = 9 + edges = [] + for i in range(1, N): + edges.extend([(i, i + 1)] * i) + DG = nx.MultiDiGraph(edges) + assert list(nx.all_topological_sorts(DG)) == [list(range(1, N + 1))] + + def test_ancestors(self): + G = nx.DiGraph() + ancestors = nx.algorithms.dag.ancestors + G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)]) + assert ancestors(G, 6) == {1, 2, 4, 5} + assert ancestors(G, 3) == {1, 4} + assert ancestors(G, 1) == set() + pytest.raises(nx.NetworkXError, ancestors, G, 8) + + def test_descendants(self): + G = nx.DiGraph() + descendants = nx.algorithms.dag.descendants + G.add_edges_from([(1, 2), (1, 3), (4, 2), (4, 3), (4, 5), (2, 6), (5, 6)]) + assert descendants(G, 1) == {2, 3, 6} + assert descendants(G, 4) == {2, 3, 5, 6} + assert descendants(G, 3) == set() + pytest.raises(nx.NetworkXError, descendants, G, 8) + + def test_transitive_closure(self): + G = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + assert edges_equal(nx.transitive_closure(G).edges(), solution, directed=True) + G = nx.DiGraph([(1, 2), (2, 3), (2, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)] + assert edges_equal(nx.transitive_closure(G).edges(), solution, directed=True) + G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + solution = [(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal( + sorted(nx.transitive_closure(G).edges()), soln, directed=True + ) + + G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution) + + G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + assert edges_equal(sorted(nx.transitive_closure(G).edges()), solution) + + G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + assert edges_equal( + sorted(nx.transitive_closure(G).edges()), solution, directed=True + ) + + # test if edge data is copied + G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)]) + H = nx.transitive_closure(G) + for u, v in G.edges(): + assert G.get_edge_data(u, v) == H.get_edge_data(u, v) + + k = 10 + G = nx.DiGraph((i, i + 1, {"f": "b", "weight": i}) for i in range(k)) + H = nx.transitive_closure(G) + for u, v in G.edges(): + assert G.get_edge_data(u, v) == H.get_edge_data(u, v) + + G = nx.Graph() + with pytest.raises(nx.NetworkXError): + nx.transitive_closure(G, reflexive="wrong input") + + def test_reflexive_transitive_closure(self): + G = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal(nx.transitive_closure(G).edges(), solution, directed=True) + assert edges_equal( + nx.transitive_closure(G, False).edges(), solution, directed=True + ) + assert edges_equal(nx.transitive_closure(G, True).edges(), soln, directed=True) + assert edges_equal( + nx.transitive_closure(G, None).edges(), solution, directed=True + ) + + G = nx.DiGraph([(1, 2), (2, 3), (2, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal(nx.transitive_closure(G).edges(), solution, directed=True) + assert edges_equal( + nx.transitive_closure(G, False).edges(), solution, directed=True + ) + assert edges_equal(nx.transitive_closure(G, True).edges(), soln, directed=True) + assert edges_equal( + nx.transitive_closure(G, None).edges(), solution, directed=True + ) + + G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + solution = sorted([(1, 2), (2, 1), (2, 3), (3, 2), (1, 3), (3, 1)]) + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal( + sorted(nx.transitive_closure(G).edges()), soln, directed=True + ) + assert edges_equal( + sorted(nx.transitive_closure(G, False).edges()), soln, directed=True + ) + assert edges_equal( + sorted(nx.transitive_closure(G, None).edges()), solution, directed=True + ) + assert edges_equal( + sorted(nx.transitive_closure(G, True).edges()), soln, directed=True + ) + + G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal(nx.transitive_closure(G).edges(), solution) + assert edges_equal(nx.transitive_closure(G, False).edges(), solution) + assert edges_equal(nx.transitive_closure(G, True).edges(), soln) + assert edges_equal(nx.transitive_closure(G, None).edges(), solution) + + G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal(nx.transitive_closure(G).edges(), solution) + assert edges_equal(nx.transitive_closure(G, False).edges(), solution) + assert edges_equal(nx.transitive_closure(G, True).edges(), soln) + assert edges_equal(nx.transitive_closure(G, None).edges(), solution) + + G = nx.MultiDiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + soln = sorted(solution + [(n, n) for n in G]) + assert edges_equal(nx.transitive_closure(G).edges(), solution, directed=True) + assert edges_equal( + nx.transitive_closure(G, False).edges(), solution, directed=True + ) + assert edges_equal(nx.transitive_closure(G, True).edges(), soln, directed=True) + assert edges_equal( + nx.transitive_closure(G, None).edges(), solution, directed=True + ) + + def test_transitive_closure_dag(self): + G = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + transitive_closure = nx.algorithms.dag.transitive_closure_dag + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + assert edges_equal(transitive_closure(G).edges(), solution, directed=True) + G = nx.DiGraph([(1, 2), (2, 3), (2, 4)]) + solution = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)] + assert edges_equal(transitive_closure(G).edges(), solution, directed=True) + G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + pytest.raises(nx.NetworkXNotImplemented, transitive_closure, G) + + # test if edge data is copied + G = nx.DiGraph([(1, 2, {"a": 3}), (2, 3, {"b": 0}), (3, 4)]) + H = transitive_closure(G) + for u, v in G.edges(): + assert G.get_edge_data(u, v) == H.get_edge_data(u, v) + + k = 10 + G = nx.DiGraph((i, i + 1, {"foo": "bar", "weight": i}) for i in range(k)) + H = transitive_closure(G) + for u, v in G.edges(): + assert G.get_edge_data(u, v) == H.get_edge_data(u, v) + + def test_transitive_reduction(self): + G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]) + transitive_reduction = nx.algorithms.dag.transitive_reduction + solution = [(1, 2), (2, 3), (3, 4)] + assert edges_equal(transitive_reduction(G).edges(), solution, directed=True) + G = nx.DiGraph([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4)]) + transitive_reduction = nx.algorithms.dag.transitive_reduction + solution = [(1, 2), (2, 3), (2, 4)] + assert edges_equal(transitive_reduction(G).edges(), solution, directed=True) + G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + pytest.raises(nx.NetworkXNotImplemented, transitive_reduction, G) + + def _check_antichains(self, solution, result): + sol = [frozenset(a) for a in solution] + res = [frozenset(a) for a in result] + assert set(sol) == set(res) + + def test_antichains(self): + antichains = nx.algorithms.dag.antichains + G = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + solution = [[], [4], [3], [2], [1]] + self._check_antichains(list(antichains(G)), solution) + G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (3, 5), (5, 6), (5, 7)]) + solution = [ + [], + [4], + [7], + [7, 4], + [6], + [6, 4], + [6, 7], + [6, 7, 4], + [5], + [5, 4], + [3], + [3, 4], + [2], + [1], + ] + self._check_antichains(list(antichains(G)), solution) + G = nx.DiGraph([(1, 2), (1, 3), (3, 4), (3, 5), (5, 6)]) + solution = [ + [], + [6], + [5], + [4], + [4, 6], + [4, 5], + [3], + [2], + [2, 6], + [2, 5], + [2, 4], + [2, 4, 6], + [2, 4, 5], + [2, 3], + [1], + ] + self._check_antichains(list(antichains(G)), solution) + G = nx.DiGraph({0: [1, 2], 1: [4], 2: [3], 3: [4]}) + solution = [[], [4], [3], [2], [1], [1, 3], [1, 2], [0]] + self._check_antichains(list(antichains(G)), solution) + G = nx.DiGraph() + self._check_antichains(list(antichains(G)), [[]]) + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2]) + solution = [[], [0], [1], [1, 0], [2], [2, 0], [2, 1], [2, 1, 0]] + self._check_antichains(list(antichains(G)), solution) + + def f(x): + return list(antichains(x)) + + G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + pytest.raises(nx.NetworkXNotImplemented, f, G) + G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + pytest.raises(nx.NetworkXUnfeasible, f, G) + + def test_lexicographical_topological_sort(self): + G = nx.DiGraph([(1, 2), (2, 3), (1, 4), (1, 5), (2, 6)]) + assert list(nx.lexicographical_topological_sort(G)) == [1, 2, 3, 4, 5, 6] + assert list(nx.lexicographical_topological_sort(G, key=lambda x: x)) == [ + 1, + 2, + 3, + 4, + 5, + 6, + ] + assert list(nx.lexicographical_topological_sort(G, key=lambda x: -x)) == [ + 1, + 5, + 4, + 2, + 6, + 3, + ] + + def test_lexicographical_topological_sort2(self): + """ + Check the case of two or more nodes with same key value. + Want to avoid exception raised due to comparing nodes directly. + See Issue #3493 + """ + + class Test_Node: + def __init__(self, n): + self.label = n + self.priority = 1 + + def __repr__(self): + return f"Node({self.label})" + + def sorting_key(node): + return node.priority + + test_nodes = [Test_Node(n) for n in range(4)] + G = nx.DiGraph() + edges = [(0, 1), (0, 2), (0, 3), (2, 3)] + G.add_edges_from((test_nodes[a], test_nodes[b]) for a, b in edges) + + sorting = list(nx.lexicographical_topological_sort(G, key=sorting_key)) + assert sorting == test_nodes + + +def test_topological_generations(): + G = nx.DiGraph( + {1: [2, 3], 2: [4, 5], 3: [7], 4: [], 5: [6, 7], 6: [], 7: []} + ).reverse() + # order within each generation is inconsequential + generations = [sorted(gen) for gen in nx.topological_generations(G)] + expected = [[4, 6, 7], [3, 5], [2], [1]] + assert generations == expected + + MG = nx.MultiDiGraph(G.edges) + MG.add_edge(2, 1) + generations = [sorted(gen) for gen in nx.topological_generations(MG)] + assert generations == expected + + +def test_topological_generations_empty(): + G = nx.DiGraph() + assert list(nx.topological_generations(G)) == [] + + +def test_topological_generations_cycle(): + G = nx.DiGraph([[2, 1], [3, 1], [1, 2]]) + with pytest.raises(nx.NetworkXUnfeasible): + list(nx.topological_generations(G)) + + +def test_is_aperiodic_cycle(): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3, 4]) + assert not nx.is_aperiodic(G) + + +def test_is_aperiodic_cycle2(): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3, 4]) + nx.add_cycle(G, [3, 4, 5, 6, 7]) + assert nx.is_aperiodic(G) + + +def test_is_aperiodic_cycle3(): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3, 4]) + nx.add_cycle(G, [3, 4, 5, 6]) + assert not nx.is_aperiodic(G) + + +def test_is_aperiodic_cycle4(): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3, 4]) + G.add_edge(1, 3) + assert nx.is_aperiodic(G) + + +def test_is_aperiodic_selfloop(): + G = nx.DiGraph() + nx.add_cycle(G, [1, 2, 3, 4]) + G.add_edge(1, 1) + assert nx.is_aperiodic(G) + + +def test_is_aperiodic_null_graph_raises(): + G = nx.DiGraph() + pytest.raises(nx.NetworkXPointlessConcept, nx.is_aperiodic, G) + + +def test_is_aperiodic_undirected_raises(): + G = nx.Graph([(1, 2), (2, 3), (3, 1)]) + pytest.raises(nx.NetworkXError, nx.is_aperiodic, G) + + +def test_is_aperiodic_disconnected_raises(): + G = nx.DiGraph() + nx.add_cycle(G, [0, 1, 2]) + G.add_edge(3, 3) + pytest.raises(nx.NetworkXError, nx.is_aperiodic, G) + + +def test_is_aperiodic_weakly_connected_raises(): + G = nx.DiGraph([(1, 2), (2, 3)]) + pytest.raises(nx.NetworkXError, nx.is_aperiodic, G) + + +def test_is_aperiodic_empty_graph(): + G = nx.empty_graph(create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes."): + nx.is_aperiodic(G) + + +def test_is_aperiodic_bipartite(): + # Bipartite graph + G = nx.DiGraph(nx.davis_southern_women_graph()) + assert not nx.is_aperiodic(G) + + +def test_is_aperiodic_single_node(): + G = nx.DiGraph() + G.add_node(0) + assert not nx.is_aperiodic(G) + G.add_edge(0, 0) + assert nx.is_aperiodic(G) + + +class TestDagToBranching: + """Unit tests for the :func:`networkx.dag_to_branching` function.""" + + def test_single_root(self): + """Tests that a directed acyclic graph with a single degree + zero node produces an arborescence. + + """ + G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)]) + B = nx.dag_to_branching(G) + expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4)]) + assert nx.is_arborescence(B) + assert nx.is_isomorphic(B, expected) + + def test_multiple_roots(self): + """Tests that a directed acyclic graph with multiple degree zero + nodes creates an arborescence with multiple (weakly) connected + components. + + """ + G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3), (5, 2)]) + B = nx.dag_to_branching(G) + expected = nx.DiGraph([(0, 1), (1, 3), (0, 2), (2, 4), (5, 6), (6, 7)]) + assert nx.is_branching(B) + assert not nx.is_arborescence(B) + assert nx.is_isomorphic(B, expected) + + # # Attributes are not copied by this function. If they were, this would + # # be a good test to uncomment. + # def test_copy_attributes(self): + # """Tests that node attributes are copied in the branching.""" + # G = nx.DiGraph([(0, 1), (0, 2), (1, 3), (2, 3)]) + # for v in G: + # G.node[v]['label'] = str(v) + # B = nx.dag_to_branching(G) + # # Determine the root node of the branching. + # root = next(v for v, d in B.in_degree() if d == 0) + # assert_equal(B.node[root]['label'], '0') + # children = B[root] + # # Get the left and right children, nodes 1 and 2, respectively. + # left, right = sorted(children, key=lambda v: B.node[v]['label']) + # assert_equal(B.node[left]['label'], '1') + # assert_equal(B.node[right]['label'], '2') + # # Get the left grandchild. + # children = B[left] + # assert_equal(len(children), 1) + # left_grandchild = arbitrary_element(children) + # assert_equal(B.node[left_grandchild]['label'], '3') + # # Get the right grandchild. + # children = B[right] + # assert_equal(len(children), 1) + # right_grandchild = arbitrary_element(children) + # assert_equal(B.node[right_grandchild]['label'], '3') + + def test_already_arborescence(self): + """Tests that a directed acyclic graph that is already an + arborescence produces an isomorphic arborescence as output. + + """ + A = nx.balanced_tree(2, 2, create_using=nx.DiGraph()) + B = nx.dag_to_branching(A) + assert nx.is_isomorphic(A, B) + + def test_already_branching(self): + """Tests that a directed acyclic graph that is already a + branching produces an isomorphic branching as output. + + """ + T1 = nx.balanced_tree(2, 2, create_using=nx.DiGraph()) + T2 = nx.balanced_tree(2, 2, create_using=nx.DiGraph()) + G = nx.disjoint_union(T1, T2) + B = nx.dag_to_branching(G) + assert nx.is_isomorphic(G, B) + + def test_not_acyclic(self): + """Tests that a non-acyclic graph causes an exception.""" + with pytest.raises(nx.HasACycle): + G = nx.DiGraph(pairwise("abc", cyclic=True)) + nx.dag_to_branching(G) + + def test_undirected(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.dag_to_branching(nx.Graph()) + + def test_multigraph(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.dag_to_branching(nx.MultiGraph()) + + def test_multidigraph(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.dag_to_branching(nx.MultiDiGraph()) + + +def test_ancestors_descendants_undirected(): + """Regression test to ensure ancestors and descendants work as expected on + undirected graphs.""" + G = nx.path_graph(5) + nx.ancestors(G, 2) == nx.descendants(G, 2) == {0, 1, 3, 4} + + +def test_v_structures_raise(): + G = nx.Graph() + with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"): + nx.dag.v_structures(G) + + +@pytest.mark.parametrize( + ("edgelist", "expected"), + ( + ( + [(0, 1), (0, 2), (3, 2)], + {(0, 2, 3)}, + ), + ( + [("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")], + {("A", "B", "C")}, + ), + ([(0, 1), (2, 1), (0, 2)], set()), # adjacent parents case: see gh-7385 + ), +) +def test_v_structures(edgelist, expected): + G = nx.DiGraph(edgelist) + v_structs = set(nx.dag.v_structures(G)) + assert v_structs == expected + + +def test_colliders_raise(): + G = nx.Graph() + with pytest.raises(nx.NetworkXNotImplemented, match="for undirected type"): + nx.dag.colliders(G) + + +@pytest.mark.parametrize( + ("edgelist", "expected"), + ( + ( + [(0, 1), (0, 2), (3, 2)], + {(0, 2, 3)}, + ), + ( + [("A", "B"), ("C", "B"), ("D", "G"), ("D", "E"), ("G", "E")], + {("A", "B", "C"), ("D", "E", "G")}, + ), + ), +) +def test_colliders(edgelist, expected): + G = nx.DiGraph(edgelist) + colliders = set(nx.dag.colliders(G)) + assert colliders == expected diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py new file mode 100644 index 0000000000000000000000000000000000000000..1668fefdf4bb68985bc58de5658e149adeef32bd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_measures.py @@ -0,0 +1,831 @@ +import itertools +import math +from random import Random + +import pytest + +import networkx as nx +from networkx import convert_node_labels_to_integers as cnlti +from networkx.algorithms.distance_measures import _extrema_bounding + + +def test__extrema_bounding_invalid_compute_kwarg(): + G = nx.path_graph(3) + with pytest.raises(ValueError, match="compute must be one of"): + _extrema_bounding(G, compute="spam") + + +class TestDistance: + def setup_method(self): + self.G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted") + + @pytest.mark.parametrize("seed", list(range(10))) + @pytest.mark.parametrize("n", list(range(10, 20))) + @pytest.mark.parametrize("prob", [x / 10 for x in range(0, 10, 2)]) + def test_use_bounds_on_off_consistency(self, seed, n, prob): + """Test for consistency of distance metrics when using usebounds=True. + + We validate consistency for `networkx.diameter`, `networkx.radius`, `networkx.periphery` + and `networkx.center` when passing `usebounds=True`. Expectation is that method + returns the same result whether we pass usebounds=True or not. + + For this we generate random connected graphs and validate method returns the same. + """ + metrics = [nx.diameter, nx.radius, nx.periphery, nx.center] + max_weight = [5, 10, 1000] + rng = Random(seed) + # we compose it with a random tree to ensure graph is connected + G = nx.compose( + nx.random_labeled_tree(n, seed=rng), + nx.erdos_renyi_graph(n, prob, seed=rng), + ) + for metric in metrics: + # checking unweighted case + assert metric(G) == metric(G, usebounds=True) + for w in max_weight: + for u, v in G.edges(): + G[u][v]["w"] = rng.randint(0, w) + # checking weighted case + assert metric(G, weight="w") == metric(G, weight="w", usebounds=True) + + def test_eccentricity(self): + assert nx.eccentricity(self.G, 1) == 6 + e = nx.eccentricity(self.G) + assert e[1] == 6 + + sp = dict(nx.shortest_path_length(self.G)) + e = nx.eccentricity(self.G, sp=sp) + assert e[1] == 6 + + e = nx.eccentricity(self.G, v=1) + assert e == 6 + + # This behavior changed in version 1.8 (ticket #739) + e = nx.eccentricity(self.G, v=[1, 1]) + assert e[1] == 6 + e = nx.eccentricity(self.G, v=[1, 2]) + assert e[1] == 6 + + # test against graph with one node + G = nx.path_graph(1) + e = nx.eccentricity(G) + assert e[0] == 0 + e = nx.eccentricity(G, v=0) + assert e == 0 + pytest.raises(nx.NetworkXError, nx.eccentricity, G, 1) + + # test against empty graph + G = nx.empty_graph() + e = nx.eccentricity(G) + assert e == {} + + def test_diameter(self): + assert nx.diameter(self.G) == 6 + + def test_harmonic_diameter(self): + assert nx.harmonic_diameter(self.G) == pytest.approx(2.0477815699658715) + assert nx.harmonic_diameter(nx.star_graph(3)) == pytest.approx(1.333333) + + def test_harmonic_diameter_empty(self): + assert math.isnan(nx.harmonic_diameter(nx.empty_graph())) + + def test_harmonic_diameter_single_node(self): + assert math.isnan(nx.harmonic_diameter(nx.empty_graph(1))) + + def test_harmonic_diameter_discrete(self): + assert math.isinf(nx.harmonic_diameter(nx.empty_graph(3))) + + def test_harmonic_diameter_not_strongly_connected(self): + DG = nx.DiGraph() + DG.add_edge(0, 1) + assert nx.harmonic_diameter(DG) == 2 + + def test_harmonic_diameter_weighted_paths(self): + G = nx.star_graph(3) + # check defaults + G.add_weighted_edges_from([(*e, 1) for i, e in enumerate(G.edges)], "weight") + assert nx.harmonic_diameter(G) == pytest.approx(1.333333) + assert nx.harmonic_diameter(G, weight="weight") == pytest.approx(1.333333) + + # check impact of weights and alternate weight name + G.add_weighted_edges_from([(*e, i) for i, e in enumerate(G.edges)], "dist") + assert nx.harmonic_diameter(G, weight="dist") == pytest.approx(1.8) + + def test_radius(self): + assert nx.radius(self.G) == 4 + + def test_periphery(self): + assert set(nx.periphery(self.G)) == {1, 4, 13, 16} + + def test_center_simple_tree(self): + G = nx.Graph([(1, 2), (1, 3), (2, 4), (2, 5)]) + assert nx.center(G) == [1, 2] + + @pytest.mark.parametrize("r", range(2, 5)) + @pytest.mark.parametrize("h", range(1, 5)) + def test_center_balanced_tree(self, r, h): + G = nx.balanced_tree(r, h) + assert nx.center(G) == [0] + + def test_center(self): + assert set(nx.center(self.G)) == {6, 7, 10, 11} + + @pytest.mark.parametrize("n", [1, 2, 99, 100]) + def test_center_path_graphs(self, n): + G = nx.path_graph(n) + expected = {(n - 1) // 2, math.ceil((n - 1) / 2)} + assert set(nx.center(G)) == expected + + def test_bound_diameter(self): + assert nx.diameter(self.G, usebounds=True) == 6 + + def test_bound_radius(self): + assert nx.radius(self.G, usebounds=True) == 4 + + def test_bound_periphery(self): + result = {1, 4, 13, 16} + assert set(nx.periphery(self.G, usebounds=True)) == result + + def test_bound_center(self): + result = {6, 7, 10, 11} + assert set(nx.center(self.G, usebounds=True)) == result + + def test_radius_exception(self): + G = nx.Graph() + G.add_edge(1, 2) + G.add_edge(3, 4) + pytest.raises(nx.NetworkXError, nx.diameter, G) + + def test_eccentricity_infinite(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph([(1, 2), (3, 4)]) + e = nx.eccentricity(G) + + def test_eccentricity_undirected_not_connected(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph([(1, 2), (3, 4)]) + e = nx.eccentricity(G, sp=1) + + def test_eccentricity_directed_weakly_connected(self): + with pytest.raises(nx.NetworkXError): + DG = nx.DiGraph([(1, 2), (1, 3)]) + nx.eccentricity(DG) + + +class TestWeightedDistance: + def setup_method(self): + G = nx.Graph() + G.add_edge(0, 1, weight=0.6, cost=0.6, high_cost=6) + G.add_edge(0, 2, weight=0.2, cost=0.2, high_cost=2) + G.add_edge(2, 3, weight=0.1, cost=0.1, high_cost=1) + G.add_edge(2, 4, weight=0.7, cost=0.7, high_cost=7) + G.add_edge(2, 5, weight=0.9, cost=0.9, high_cost=9) + G.add_edge(1, 5, weight=0.3, cost=0.3, high_cost=3) + self.G = G + self.weight_fn = lambda v, u, e: 2 + + def test_eccentricity_weight_None(self): + assert nx.eccentricity(self.G, 1, weight=None) == 3 + e = nx.eccentricity(self.G, weight=None) + assert e[1] == 3 + + e = nx.eccentricity(self.G, v=1, weight=None) + assert e == 3 + + # This behavior changed in version 1.8 (ticket #739) + e = nx.eccentricity(self.G, v=[1, 1], weight=None) + assert e[1] == 3 + e = nx.eccentricity(self.G, v=[1, 2], weight=None) + assert e[1] == 3 + + def test_eccentricity_weight_attr(self): + assert nx.eccentricity(self.G, 1, weight="weight") == 1.5 + e = nx.eccentricity(self.G, weight="weight") + assert ( + e + == nx.eccentricity(self.G, weight="cost") + != nx.eccentricity(self.G, weight="high_cost") + ) + assert e[1] == 1.5 + + e = nx.eccentricity(self.G, v=1, weight="weight") + assert e == 1.5 + + # This behavior changed in version 1.8 (ticket #739) + e = nx.eccentricity(self.G, v=[1, 1], weight="weight") + assert e[1] == 1.5 + e = nx.eccentricity(self.G, v=[1, 2], weight="weight") + assert e[1] == 1.5 + + def test_eccentricity_weight_fn(self): + assert nx.eccentricity(self.G, 1, weight=self.weight_fn) == 6 + e = nx.eccentricity(self.G, weight=self.weight_fn) + assert e[1] == 6 + + e = nx.eccentricity(self.G, v=1, weight=self.weight_fn) + assert e == 6 + + # This behavior changed in version 1.8 (ticket #739) + e = nx.eccentricity(self.G, v=[1, 1], weight=self.weight_fn) + assert e[1] == 6 + e = nx.eccentricity(self.G, v=[1, 2], weight=self.weight_fn) + assert e[1] == 6 + + def test_diameter_weight_None(self): + assert nx.diameter(self.G, weight=None) == 3 + + def test_diameter_weight_attr(self): + assert ( + nx.diameter(self.G, weight="weight") + == nx.diameter(self.G, weight="cost") + == 1.6 + != nx.diameter(self.G, weight="high_cost") + ) + + def test_diameter_weight_fn(self): + assert nx.diameter(self.G, weight=self.weight_fn) == 6 + + def test_radius_weight_None(self): + assert pytest.approx(nx.radius(self.G, weight=None)) == 2 + + def test_radius_weight_attr(self): + assert ( + pytest.approx(nx.radius(self.G, weight="weight")) + == pytest.approx(nx.radius(self.G, weight="cost")) + == 0.9 + != nx.radius(self.G, weight="high_cost") + ) + + def test_radius_weight_fn(self): + assert nx.radius(self.G, weight=self.weight_fn) == 4 + + def test_periphery_weight_None(self): + for v in set(nx.periphery(self.G, weight=None)): + assert nx.eccentricity(self.G, v, weight=None) == nx.diameter( + self.G, weight=None + ) + + def test_periphery_weight_attr(self): + periphery = set(nx.periphery(self.G, weight="weight")) + assert ( + periphery + == set(nx.periphery(self.G, weight="cost")) + == set(nx.periphery(self.G, weight="high_cost")) + ) + for v in periphery: + assert ( + nx.eccentricity(self.G, v, weight="high_cost") + != nx.eccentricity(self.G, v, weight="weight") + == nx.eccentricity(self.G, v, weight="cost") + == nx.diameter(self.G, weight="weight") + == nx.diameter(self.G, weight="cost") + != nx.diameter(self.G, weight="high_cost") + ) + assert nx.eccentricity(self.G, v, weight="high_cost") == nx.diameter( + self.G, weight="high_cost" + ) + + def test_periphery_weight_fn(self): + for v in set(nx.periphery(self.G, weight=self.weight_fn)): + assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.diameter( + self.G, weight=self.weight_fn + ) + + def test_center_weight_None(self): + for v in set(nx.center(self.G, weight=None)): + assert pytest.approx(nx.eccentricity(self.G, v, weight=None)) == nx.radius( + self.G, weight=None + ) + + def test_center_weight_attr(self): + center = set(nx.center(self.G, weight="weight")) + assert ( + center + == set(nx.center(self.G, weight="cost")) + != set(nx.center(self.G, weight="high_cost")) + ) + for v in center: + assert ( + nx.eccentricity(self.G, v, weight="high_cost") + != pytest.approx(nx.eccentricity(self.G, v, weight="weight")) + == pytest.approx(nx.eccentricity(self.G, v, weight="cost")) + == nx.radius(self.G, weight="weight") + == nx.radius(self.G, weight="cost") + != nx.radius(self.G, weight="high_cost") + ) + assert nx.eccentricity(self.G, v, weight="high_cost") == nx.radius( + self.G, weight="high_cost" + ) + + def test_center_weight_fn(self): + for v in set(nx.center(self.G, weight=self.weight_fn)): + assert nx.eccentricity(self.G, v, weight=self.weight_fn) == nx.radius( + self.G, weight=self.weight_fn + ) + + def test_bound_diameter_weight_None(self): + assert nx.diameter(self.G, usebounds=True, weight=None) == 3 + + def test_bound_diameter_weight_attr(self): + assert ( + nx.diameter(self.G, usebounds=True, weight="high_cost") + != nx.diameter(self.G, usebounds=True, weight="weight") + == nx.diameter(self.G, usebounds=True, weight="cost") + == 1.6 + != nx.diameter(self.G, usebounds=True, weight="high_cost") + ) + assert nx.diameter(self.G, usebounds=True, weight="high_cost") == nx.diameter( + self.G, usebounds=True, weight="high_cost" + ) + + def test_bound_diameter_weight_fn(self): + assert nx.diameter(self.G, usebounds=True, weight=self.weight_fn) == 6 + + def test_bound_radius_weight_None(self): + assert pytest.approx(nx.radius(self.G, usebounds=True, weight=None)) == 2 + + def test_bound_radius_weight_attr(self): + assert ( + nx.radius(self.G, usebounds=True, weight="high_cost") + != pytest.approx(nx.radius(self.G, usebounds=True, weight="weight")) + == pytest.approx(nx.radius(self.G, usebounds=True, weight="cost")) + == 0.9 + != nx.radius(self.G, usebounds=True, weight="high_cost") + ) + assert nx.radius(self.G, usebounds=True, weight="high_cost") == nx.radius( + self.G, usebounds=True, weight="high_cost" + ) + + def test_bound_radius_weight_fn(self): + assert nx.radius(self.G, usebounds=True, weight=self.weight_fn) == 4 + + def test_bound_periphery_weight_None(self): + result = {1, 3, 4} + assert set(nx.periphery(self.G, usebounds=True, weight=None)) == result + + def test_bound_periphery_weight_attr(self): + result = {4, 5} + assert ( + set(nx.periphery(self.G, usebounds=True, weight="weight")) + == set(nx.periphery(self.G, usebounds=True, weight="cost")) + == result + ) + + def test_bound_periphery_weight_fn(self): + result = {1, 3, 4} + assert ( + set(nx.periphery(self.G, usebounds=True, weight=self.weight_fn)) == result + ) + + def test_bound_center_weight_None(self): + result = {0, 2, 5} + assert set(nx.center(self.G, usebounds=True, weight=None)) == result + + def test_bound_center_weight_attr(self): + result = {0} + assert ( + set(nx.center(self.G, usebounds=True, weight="weight")) + == set(nx.center(self.G, usebounds=True, weight="cost")) + == result + ) + + def test_bound_center_weight_fn(self): + result = {0, 2, 5} + assert set(nx.center(self.G, usebounds=True, weight=self.weight_fn)) == result + + +class TestResistanceDistance: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + sp = pytest.importorskip("scipy") + + def setup_method(self): + G = nx.Graph() + G.add_edge(1, 2, weight=2) + G.add_edge(2, 3, weight=4) + G.add_edge(3, 4, weight=1) + G.add_edge(1, 4, weight=3) + self.G = G + + def test_resistance_distance_directed_graph(self): + G = nx.DiGraph() + with pytest.raises(nx.NetworkXNotImplemented): + nx.resistance_distance(G) + + def test_resistance_distance_empty(self): + G = nx.Graph() + with pytest.raises(nx.NetworkXError): + nx.resistance_distance(G) + + def test_resistance_distance_not_connected(self): + with pytest.raises(nx.NetworkXError): + self.G.add_node(5) + nx.resistance_distance(self.G, 1, 5) + + def test_resistance_distance_nodeA_not_in_graph(self): + with pytest.raises(nx.NetworkXError): + nx.resistance_distance(self.G, 9, 1) + + def test_resistance_distance_nodeB_not_in_graph(self): + with pytest.raises(nx.NetworkXError): + nx.resistance_distance(self.G, 1, 9) + + def test_resistance_distance(self): + rd = nx.resistance_distance(self.G, 1, 3, "weight", True) + test_data = 1 / (1 / (2 + 4) + 1 / (1 + 3)) + assert round(rd, 5) == round(test_data, 5) + + def test_resistance_distance_noinv(self): + rd = nx.resistance_distance(self.G, 1, 3, "weight", False) + test_data = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1 + 1 / 3)) + assert round(rd, 5) == round(test_data, 5) + + def test_resistance_distance_no_weight(self): + rd = nx.resistance_distance(self.G, 1, 3) + assert round(rd, 5) == 1 + + def test_resistance_distance_neg_weight(self): + self.G[2][3]["weight"] = -4 + rd = nx.resistance_distance(self.G, 1, 3, "weight", True) + test_data = 1 / (1 / (2 + -4) + 1 / (1 + 3)) + assert round(rd, 5) == round(test_data, 5) + + def test_multigraph(self): + G = nx.MultiGraph() + G.add_edge(1, 2, weight=2) + G.add_edge(2, 3, weight=4) + G.add_edge(3, 4, weight=1) + G.add_edge(1, 4, weight=3) + rd = nx.resistance_distance(G, 1, 3, "weight", True) + assert np.isclose(rd, 1 / (1 / (2 + 4) + 1 / (1 + 3))) + + def test_resistance_distance_div0(self): + with pytest.raises(ZeroDivisionError): + self.G[1][2]["weight"] = 0 + nx.resistance_distance(self.G, 1, 3, "weight") + + def test_resistance_distance_same_node(self): + assert nx.resistance_distance(self.G, 1, 1) == 0 + + def test_resistance_distance_only_nodeA(self): + rd = nx.resistance_distance(self.G, nodeA=1) + test_data = {} + test_data[1] = 0 + test_data[2] = 0.75 + test_data[3] = 1 + test_data[4] = 0.75 + assert isinstance(rd, dict) + assert sorted(rd.keys()) == sorted(test_data.keys()) + for key in rd: + assert np.isclose(rd[key], test_data[key]) + + def test_resistance_distance_only_nodeB(self): + rd = nx.resistance_distance(self.G, nodeB=1) + test_data = {} + test_data[1] = 0 + test_data[2] = 0.75 + test_data[3] = 1 + test_data[4] = 0.75 + assert isinstance(rd, dict) + assert sorted(rd.keys()) == sorted(test_data.keys()) + for key in rd: + assert np.isclose(rd[key], test_data[key]) + + def test_resistance_distance_all(self): + rd = nx.resistance_distance(self.G) + assert isinstance(rd, dict) + assert round(rd[1][3], 5) == 1 + + +class TestEffectiveGraphResistance: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + sp = pytest.importorskip("scipy") + + def setup_method(self): + G = nx.Graph() + G.add_edge(1, 2, weight=2) + G.add_edge(1, 3, weight=1) + G.add_edge(2, 3, weight=4) + self.G = G + + def test_effective_graph_resistance_directed_graph(self): + G = nx.DiGraph() + with pytest.raises(nx.NetworkXNotImplemented): + nx.effective_graph_resistance(G) + + def test_effective_graph_resistance_empty(self): + G = nx.Graph() + with pytest.raises(nx.NetworkXError): + nx.effective_graph_resistance(G) + + def test_effective_graph_resistance_not_connected(self): + G = nx.Graph([(1, 2), (3, 4)]) + RG = nx.effective_graph_resistance(G) + assert np.isinf(RG) + + def test_effective_graph_resistance(self): + RG = nx.effective_graph_resistance(self.G, "weight", True) + rd12 = 1 / (1 / (1 + 4) + 1 / 2) + rd13 = 1 / (1 / (1 + 2) + 1 / 4) + rd23 = 1 / (1 / (2 + 4) + 1 / 1) + assert np.isclose(RG, rd12 + rd13 + rd23) + + def test_effective_graph_resistance_noinv(self): + RG = nx.effective_graph_resistance(self.G, "weight", False) + rd12 = 1 / (1 / (1 / 1 + 1 / 4) + 1 / (1 / 2)) + rd13 = 1 / (1 / (1 / 1 + 1 / 2) + 1 / (1 / 4)) + rd23 = 1 / (1 / (1 / 2 + 1 / 4) + 1 / (1 / 1)) + assert np.isclose(RG, rd12 + rd13 + rd23) + + def test_effective_graph_resistance_no_weight(self): + RG = nx.effective_graph_resistance(self.G) + assert np.isclose(RG, 2) + + def test_effective_graph_resistance_neg_weight(self): + self.G[2][3]["weight"] = -4 + RG = nx.effective_graph_resistance(self.G, "weight", True) + rd12 = 1 / (1 / (1 + -4) + 1 / 2) + rd13 = 1 / (1 / (1 + 2) + 1 / (-4)) + rd23 = 1 / (1 / (2 + -4) + 1 / 1) + assert np.isclose(RG, rd12 + rd13 + rd23) + + def test_effective_graph_resistance_multigraph(self): + G = nx.MultiGraph() + G.add_edge(1, 2, weight=2) + G.add_edge(1, 3, weight=1) + G.add_edge(2, 3, weight=1) + G.add_edge(2, 3, weight=3) + RG = nx.effective_graph_resistance(G, "weight", True) + edge23 = 1 / (1 / 1 + 1 / 3) + rd12 = 1 / (1 / (1 + edge23) + 1 / 2) + rd13 = 1 / (1 / (1 + 2) + 1 / edge23) + rd23 = 1 / (1 / (2 + edge23) + 1 / 1) + assert np.isclose(RG, rd12 + rd13 + rd23) + + def test_effective_graph_resistance_div0(self): + with pytest.raises(ZeroDivisionError): + self.G[1][2]["weight"] = 0 + nx.effective_graph_resistance(self.G, "weight") + + def test_effective_graph_resistance_complete_graph(self): + N = 10 + G = nx.complete_graph(N) + RG = nx.effective_graph_resistance(G) + assert np.isclose(RG, N - 1) + + def test_effective_graph_resistance_path_graph(self): + N = 10 + G = nx.path_graph(N) + RG = nx.effective_graph_resistance(G) + assert np.isclose(RG, (N - 1) * N * (N + 1) // 6) + + +class TestBarycenter: + """Test :func:`networkx.algorithms.distance_measures.barycenter`.""" + + def barycenter_as_subgraph(self, g, **kwargs): + """Return the subgraph induced on the barycenter of g""" + b = nx.barycenter(g, **kwargs) + assert isinstance(b, list) + assert set(b) <= set(g) + return g.subgraph(b) + + def test_must_be_connected(self): + pytest.raises(nx.NetworkXNoPath, nx.barycenter, nx.empty_graph(5)) + + def test_sp_kwarg(self): + # Complete graph K_5. Normally it works... + K_5 = nx.complete_graph(5) + sp = dict(nx.shortest_path_length(K_5)) + assert nx.barycenter(K_5, sp=sp) == list(K_5) + + # ...but not with the weight argument + for u, v, data in K_5.edges.data(): + data["weight"] = 1 + pytest.raises(ValueError, nx.barycenter, K_5, sp=sp, weight="weight") + + # ...and a corrupted sp can make it seem like K_5 is disconnected + del sp[0][1] + pytest.raises(nx.NetworkXNoPath, nx.barycenter, K_5, sp=sp) + + def test_trees(self): + """The barycenter of a tree is a single vertex or an edge. + + See [West01]_, p. 78. + """ + prng = Random(0xDEADBEEF) + for i in range(50): + RT = nx.random_labeled_tree(prng.randint(1, 75), seed=prng) + b = self.barycenter_as_subgraph(RT) + if len(b) == 2: + assert b.size() == 1 + else: + assert len(b) == 1 + assert b.size() == 0 + + def test_this_one_specific_tree(self): + """Test the tree pictured at the bottom of [West01]_, p. 78.""" + g = nx.Graph( + { + "a": ["b"], + "b": ["a", "x"], + "x": ["b", "y"], + "y": ["x", "z"], + "z": ["y", 0, 1, 2, 3, 4], + 0: ["z"], + 1: ["z"], + 2: ["z"], + 3: ["z"], + 4: ["z"], + } + ) + b = self.barycenter_as_subgraph(g, attr="barycentricity") + assert list(b) == ["z"] + assert not b.edges + expected_barycentricity = { + 0: 23, + 1: 23, + 2: 23, + 3: 23, + 4: 23, + "a": 35, + "b": 27, + "x": 21, + "y": 17, + "z": 15, + } + for node, barycentricity in expected_barycentricity.items(): + assert g.nodes[node]["barycentricity"] == barycentricity + + # Doubling weights should do nothing but double the barycentricities + for edge in g.edges: + g.edges[edge]["weight"] = 2 + b = self.barycenter_as_subgraph(g, weight="weight", attr="barycentricity2") + assert list(b) == ["z"] + assert not b.edges + for node, barycentricity in expected_barycentricity.items(): + assert g.nodes[node]["barycentricity2"] == barycentricity * 2 + + +class TestKemenyConstant: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + sp = pytest.importorskip("scipy") + + def setup_method(self): + G = nx.Graph() + w12 = 2 + w13 = 3 + w23 = 4 + G.add_edge(1, 2, weight=w12) + G.add_edge(1, 3, weight=w13) + G.add_edge(2, 3, weight=w23) + self.G = G + + def test_kemeny_constant_directed(self): + G = nx.DiGraph() + G.add_edge(1, 2) + G.add_edge(1, 3) + G.add_edge(2, 3) + with pytest.raises(nx.NetworkXNotImplemented): + nx.kemeny_constant(G) + + def test_kemeny_constant_not_connected(self): + self.G.add_node(5) + with pytest.raises(nx.NetworkXError): + nx.kemeny_constant(self.G) + + def test_kemeny_constant_no_nodes(self): + G = nx.Graph() + with pytest.raises(nx.NetworkXError): + nx.kemeny_constant(G) + + def test_kemeny_constant_negative_weight(self): + G = nx.Graph() + w12 = 2 + w13 = 3 + w23 = -10 + G.add_edge(1, 2, weight=w12) + G.add_edge(1, 3, weight=w13) + G.add_edge(2, 3, weight=w23) + with pytest.raises(nx.NetworkXError): + nx.kemeny_constant(G, weight="weight") + + def test_kemeny_constant(self): + K = nx.kemeny_constant(self.G, weight="weight") + w12 = 2 + w13 = 3 + w23 = 4 + test_data = ( + 3 + / 2 + * (w12 + w13) + * (w12 + w23) + * (w13 + w23) + / ( + w12**2 * (w13 + w23) + + w13**2 * (w12 + w23) + + w23**2 * (w12 + w13) + + 3 * w12 * w13 * w23 + ) + ) + assert np.isclose(K, test_data) + + def test_kemeny_constant_no_weight(self): + K = nx.kemeny_constant(self.G) + assert np.isclose(K, 4 / 3) + + def test_kemeny_constant_multigraph(self): + G = nx.MultiGraph() + w12_1 = 2 + w12_2 = 1 + w13 = 3 + w23 = 4 + G.add_edge(1, 2, weight=w12_1) + G.add_edge(1, 2, weight=w12_2) + G.add_edge(1, 3, weight=w13) + G.add_edge(2, 3, weight=w23) + K = nx.kemeny_constant(G, weight="weight") + w12 = w12_1 + w12_2 + test_data = ( + 3 + / 2 + * (w12 + w13) + * (w12 + w23) + * (w13 + w23) + / ( + w12**2 * (w13 + w23) + + w13**2 * (w12 + w23) + + w23**2 * (w12 + w13) + + 3 * w12 * w13 * w23 + ) + ) + assert np.isclose(K, test_data) + + def test_kemeny_constant_weight0(self): + G = nx.Graph() + w12 = 0 + w13 = 3 + w23 = 4 + G.add_edge(1, 2, weight=w12) + G.add_edge(1, 3, weight=w13) + G.add_edge(2, 3, weight=w23) + K = nx.kemeny_constant(G, weight="weight") + test_data = ( + 3 + / 2 + * (w12 + w13) + * (w12 + w23) + * (w13 + w23) + / ( + w12**2 * (w13 + w23) + + w13**2 * (w12 + w23) + + w23**2 * (w12 + w13) + + 3 * w12 * w13 * w23 + ) + ) + assert np.isclose(K, test_data) + + def test_kemeny_constant_selfloop(self): + G = nx.Graph() + w11 = 1 + w12 = 2 + w13 = 3 + w23 = 4 + G.add_edge(1, 1, weight=w11) + G.add_edge(1, 2, weight=w12) + G.add_edge(1, 3, weight=w13) + G.add_edge(2, 3, weight=w23) + K = nx.kemeny_constant(G, weight="weight") + test_data = ( + (2 * w11 + 3 * w12 + 3 * w13) + * (w12 + w23) + * (w13 + w23) + / ( + (w12 * w13 + w12 * w23 + w13 * w23) + * (w11 + 2 * w12 + 2 * w13 + 2 * w23) + ) + ) + assert np.isclose(K, test_data) + + def test_kemeny_constant_complete_bipartite_graph(self): + # Theorem 1 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912 + n1 = 5 + n2 = 4 + G = nx.complete_bipartite_graph(n1, n2) + K = nx.kemeny_constant(G) + assert np.isclose(K, n1 + n2 - 3 / 2) + + def test_kemeny_constant_path_graph(self): + # Theorem 2 in https://www.sciencedirect.com/science/article/pii/S0166218X20302912 + n = 10 + G = nx.path_graph(n) + K = nx.kemeny_constant(G) + assert np.isclose(K, n**2 / 3 - 2 * n / 3 + 1 / 2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py new file mode 100644 index 0000000000000000000000000000000000000000..545fb6dee6a915230971cf4b5a141e47adc2cc15 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_distance_regular.py @@ -0,0 +1,85 @@ +import pytest + +import networkx as nx +from networkx import is_strongly_regular + + +@pytest.mark.parametrize( + "f", (nx.is_distance_regular, nx.intersection_array, nx.is_strongly_regular) +) +@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph)) +def test_raises_on_directed_and_multigraphs(f, graph_constructor): + G = graph_constructor([(0, 1), (1, 2)]) + with pytest.raises(nx.NetworkXNotImplemented): + f(G) + + +class TestDistanceRegular: + def test_is_distance_regular(self): + assert nx.is_distance_regular(nx.icosahedral_graph()) + assert nx.is_distance_regular(nx.petersen_graph()) + assert nx.is_distance_regular(nx.cubical_graph()) + assert nx.is_distance_regular(nx.complete_bipartite_graph(3, 3)) + assert nx.is_distance_regular(nx.tetrahedral_graph()) + assert nx.is_distance_regular(nx.dodecahedral_graph()) + assert nx.is_distance_regular(nx.pappus_graph()) + assert nx.is_distance_regular(nx.heawood_graph()) + assert nx.is_distance_regular(nx.cycle_graph(3)) + # no distance regular + assert not nx.is_distance_regular(nx.path_graph(4)) + + def test_not_connected(self): + G = nx.cycle_graph(4) + nx.add_cycle(G, [5, 6, 7]) + assert not nx.is_distance_regular(G) + + def test_global_parameters(self): + b, c = nx.intersection_array(nx.cycle_graph(5)) + g = nx.global_parameters(b, c) + assert list(g) == [(0, 0, 2), (1, 0, 1), (1, 1, 0)] + b, c = nx.intersection_array(nx.cycle_graph(3)) + g = nx.global_parameters(b, c) + assert list(g) == [(0, 0, 2), (1, 1, 0)] + + def test_intersection_array(self): + b, c = nx.intersection_array(nx.cycle_graph(5)) + assert b == [2, 1] + assert c == [1, 1] + b, c = nx.intersection_array(nx.dodecahedral_graph()) + assert b == [3, 2, 1, 1, 1] + assert c == [1, 1, 1, 2, 3] + b, c = nx.intersection_array(nx.icosahedral_graph()) + assert b == [5, 2, 1] + assert c == [1, 2, 5] + + +@pytest.mark.parametrize("f", (nx.is_distance_regular, nx.is_strongly_regular)) +def test_empty_graph_raises(f): + G = nx.Graph() + with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes"): + f(G) + + +class TestStronglyRegular: + """Unit tests for the :func:`~networkx.is_strongly_regular` + function. + + """ + + def test_cycle_graph(self): + """Tests that the cycle graph on five vertices is strongly + regular. + + """ + G = nx.cycle_graph(5) + assert is_strongly_regular(G) + + def test_petersen_graph(self): + """Tests that the Petersen graph is strongly regular.""" + G = nx.petersen_graph() + assert is_strongly_regular(G) + + def test_path_graph(self): + """Tests that the path graph is not strongly regular.""" + G = nx.path_graph(4) + assert not is_strongly_regular(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py new file mode 100644 index 0000000000000000000000000000000000000000..480b3a5c841af11b807ba533ae9f809eb1e52672 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominance.py @@ -0,0 +1,299 @@ +import pytest + +import networkx as nx + + +class TestImmediateDominators: + @pytest.mark.parametrize("G", [nx.Graph(), nx.MultiGraph()]) + def test_raises_undirected(self, G): + """Check that `immediate_dominators` raises for undirected graphs.""" + with pytest.raises( + nx.NetworkXNotImplemented, match=r"not implemented for undirected" + ): + nx.immediate_dominators(G, 0) + + def test_raises_node(self): + """Check that `immediate_dominators` raises when `start` is not in the graph.""" + G = nx.empty_graph(1, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError, match=r"not in G"): + nx.immediate_dominators(G, 1) + + def test_singleton(self): + G = nx.DiGraph() + G.add_node(0) + assert nx.immediate_dominators(G, 0) == {} + G.add_edge(0, 0) + assert nx.immediate_dominators(G, 0) == {} + + @pytest.mark.parametrize("gen", [nx.path_graph, nx.cycle_graph]) + @pytest.mark.parametrize("n", [5, 10, 20]) + def test_path_and_cycle(self, gen, n): + """Check `immediate_dominators` is correct for path and cycle graphs.""" + G = gen(n, create_using=nx.DiGraph()) + idom = nx.immediate_dominators(G, 0) + assert idom == {i: i - 1 for i in range(1, n)} + + def test_unreachable(self): + n = 5 + G = nx.path_graph(n, create_using=nx.DiGraph()) + idom = nx.immediate_dominators(G, 1) + assert idom == {i: i - 1 for i in range(2, n)} + + @pytest.mark.parametrize( + ["edgelist", "start"], + [ + ([(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)], 5), + ( + [ + (1, 2), + (2, 1), + (2, 3), + (3, 2), + (4, 2), + (4, 3), + (5, 1), + (6, 4), + (6, 5), + ], + 6, + ), + ], + ) + def test_irreducible(self, edgelist, start): + """ + Check `immediate_dominators` on irreducible reference graphs. + + Graphs taken from figures 2 and 4 of "A simple, fast dominance algorithm." (2006). + https://hdl.handle.net/1911/96345 + """ + G = nx.DiGraph(edgelist) + idom = nx.immediate_dominators(G, start) + assert idom == dict.fromkeys(range(1, start), start) + + def test_domrel_png(self): + # Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png + edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)] + G = nx.DiGraph(edges) + result = nx.immediate_dominators(G, 1) + assert result == {2: 1, 3: 2, 4: 2, 5: 2, 6: 2} + # Test postdominance. + result = nx.immediate_dominators(G.reverse(copy=False), 6) + assert result == {1: 2, 2: 6, 3: 5, 4: 5, 5: 2} + + def test_boost_example(self): + # Graph taken from Figure 1 of + # http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm + edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)] + G = nx.DiGraph(edges) + result = nx.immediate_dominators(G, 0) + assert result == {1: 0, 2: 1, 3: 1, 4: 3, 5: 4, 6: 4, 7: 1} + # Test postdominance. + result = nx.immediate_dominators(G.reverse(copy=False), 7) + assert result == {0: 1, 1: 7, 2: 7, 3: 4, 4: 5, 5: 7, 6: 4} + + +class TestDominanceFrontiers: + @pytest.mark.parametrize("G", [nx.Graph(), nx.MultiGraph()]) + def test_raises_undirected(self, G): + """Check that `dominance_frontiers` raises for undirected graphs.""" + with pytest.raises( + nx.NetworkXNotImplemented, match=r"not implemented for undirected" + ): + nx.dominance_frontiers(G, 0) + + def test_raises_node(self): + """Check that `dominance_frontiers` raises when `start` is not in the graph.""" + G = nx.empty_graph(1, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError, match=r"not in G"): + nx.dominance_frontiers(G, 1) + + def test_singleton(self): + G = nx.DiGraph() + G.add_node(0) + assert nx.dominance_frontiers(G, 0) == {0: set()} + G.add_edge(0, 0) + assert nx.dominance_frontiers(G, 0) == {0: {0}} + + @pytest.mark.parametrize("gen, df", [(nx.path_graph, set()), (nx.cycle_graph, {0})]) + @pytest.mark.parametrize("n", [5, 10, 20]) + def test_path_and_cycle(self, gen, df, n): + """Check that `dominance_frontiers` is correct for path and cycle graphs.""" + G = gen(n, create_using=nx.DiGraph()) + assert nx.dominance_frontiers(G, 0) == dict.fromkeys(range(n), df) + + def test_unreachable(self): + n = 5 + G = nx.path_graph(n, create_using=nx.DiGraph()) + assert nx.dominance_frontiers(G, 1) == dict.fromkeys(range(1, n), set()) + + def test_irreducible1(self): + """ + Graph taken from figure 2 of "A simple, fast dominance algorithm." (2006). + https://hdl.handle.net/1911/96345 + """ + edges = [(1, 2), (2, 1), (3, 2), (4, 1), (5, 3), (5, 4)] + G = nx.DiGraph(edges) + assert nx.dominance_frontiers(G, 5) == { + 1: {2}, + 2: {1}, + 3: {2}, + 4: {1}, + 5: set(), + } + + def test_irreducible2(self): + """ + Graph taken from figure 4 of "A simple, fast dominance algorithm." (2006). + https://hdl.handle.net/1911/96345 + """ + edges = [(1, 2), (2, 1), (2, 3), (3, 2), (4, 2), (4, 3), (5, 1), (6, 4), (6, 5)] + G = nx.DiGraph(edges) + assert nx.dominance_frontiers(G, 6) == { + 1: {2}, + 2: {1, 3}, + 3: {2}, + 4: {2, 3}, + 5: {1}, + 6: set(), + } + + def test_domrel_png(self): + # Graph taken from https://commons.wikipedia.org/wiki/File:Domrel.png + edges = [(1, 2), (2, 3), (2, 4), (2, 6), (3, 5), (4, 5), (5, 2)] + G = nx.DiGraph(edges) + assert nx.dominance_frontiers(G, 1) == { + 1: set(), + 2: {2}, + 3: {5}, + 4: {5}, + 5: {2}, + 6: set(), + } + # Test postdominance. + result = nx.dominance_frontiers(G.reverse(copy=False), 6) + assert result == {1: set(), 2: {2}, 3: {2}, 4: {2}, 5: {2}, 6: set()} + + def test_boost_example(self): + # Graph taken from Figure 1 of + # http://www.boost.org/doc/libs/1_56_0/libs/graph/doc/lengauer_tarjan_dominator.htm + edges = [(0, 1), (1, 2), (1, 3), (2, 7), (3, 4), (4, 5), (4, 6), (5, 7), (6, 4)] + G = nx.DiGraph(edges) + assert nx.dominance_frontiers(G, 0) == { + 0: set(), + 1: set(), + 2: {7}, + 3: {7}, + 4: {4, 7}, + 5: {7}, + 6: {4}, + 7: set(), + } + # Test postdominance. + result = nx.dominance_frontiers(G.reverse(copy=False), 7) + expected = { + 0: set(), + 1: set(), + 2: {1}, + 3: {1}, + 4: {1, 4}, + 5: {1}, + 6: {4}, + 7: set(), + } + assert result == expected + + def test_discard_issue(self): + # https://github.com/networkx/networkx/issues/2071 + g = nx.DiGraph() + g.add_edges_from( + [ + ("b0", "b1"), + ("b1", "b2"), + ("b2", "b3"), + ("b3", "b1"), + ("b1", "b5"), + ("b5", "b6"), + ("b5", "b8"), + ("b6", "b7"), + ("b8", "b7"), + ("b7", "b3"), + ("b3", "b4"), + ] + ) + df = nx.dominance_frontiers(g, "b0") + assert df == { + "b4": set(), + "b5": {"b3"}, + "b6": {"b7"}, + "b7": {"b3"}, + "b0": set(), + "b1": {"b1"}, + "b2": {"b3"}, + "b3": {"b1"}, + "b8": {"b7"}, + } + + def test_loop(self): + g = nx.DiGraph() + g.add_edges_from([("a", "b"), ("b", "c"), ("b", "a")]) + df = nx.dominance_frontiers(g, "a") + assert df == {"a": {"a"}, "b": {"a"}, "c": set()} + + def test_missing_immediate_doms(self): + # see https://github.com/networkx/networkx/issues/2070 + g = nx.DiGraph() + edges = [ + ("entry_1", "b1"), + ("b1", "b2"), + ("b2", "b3"), + ("b3", "exit"), + ("entry_2", "b3"), + ] + + # entry_1 + # | + # b1 + # | + # b2 entry_2 + # | / + # b3 + # | + # exit + + g.add_edges_from(edges) + # formerly raised KeyError on entry_2 when parsing b3 + # because entry_2 does not have immediate doms (no path) + nx.dominance_frontiers(g, "entry_1") + + def test_loops_larger(self): + # from + # http://ecee.colorado.edu/~waite/Darmstadt/motion.html + g = nx.DiGraph() + edges = [ + ("entry", "exit"), + ("entry", "1"), + ("1", "2"), + ("2", "3"), + ("3", "4"), + ("4", "5"), + ("5", "6"), + ("6", "exit"), + ("6", "2"), + ("5", "3"), + ("4", "4"), + ] + + g.add_edges_from(edges) + df = nx.dominance_frontiers(g, "entry") + answer = { + "entry": set(), + "1": {"exit"}, + "2": {"exit", "2"}, + "3": {"exit", "3", "2"}, + "4": {"exit", "4", "3", "2"}, + "5": {"exit", "3", "2"}, + "6": {"exit", "2"}, + "exit": set(), + } + for n in df: + assert set(df[n]) == set(answer[n]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py new file mode 100644 index 0000000000000000000000000000000000000000..5f51777c72c7d4b9cc22e77a6aa6f470200b66a7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_dominating.py @@ -0,0 +1,115 @@ +import pytest + +import networkx as nx + + +def test_dominating_set(): + G = nx.gnp_random_graph(100, 0.1) + D = nx.dominating_set(G) + assert nx.is_dominating_set(G, D) + D = nx.dominating_set(G, start_with=0) + assert nx.is_dominating_set(G, D) + + +def test_complete(): + """In complete graphs each node is a dominating set. + Thus the dominating set has to be of cardinality 1. + """ + K4 = nx.complete_graph(4) + assert len(nx.dominating_set(K4)) == 1 + K5 = nx.complete_graph(5) + assert len(nx.dominating_set(K5)) == 1 + + +def test_raise_dominating_set(): + with pytest.raises(nx.NetworkXError): + G = nx.path_graph(4) + D = nx.dominating_set(G, start_with=10) + + +def test_is_dominating_set(): + G = nx.path_graph(4) + d = {1, 3} + assert nx.is_dominating_set(G, d) + d = {0, 2} + assert nx.is_dominating_set(G, d) + d = {1} + assert not nx.is_dominating_set(G, d) + + +def test_wikipedia_is_dominating_set(): + """Example from https://en.wikipedia.org/wiki/Dominating_set""" + G = nx.cycle_graph(4) + G.add_edges_from([(0, 4), (1, 4), (2, 5)]) + assert nx.is_dominating_set(G, {4, 3, 5}) + assert nx.is_dominating_set(G, {0, 2}) + assert nx.is_dominating_set(G, {1, 2}) + + +def test_is_connected_dominating_set(): + G = nx.path_graph(4) + D = {1, 2} + assert nx.is_connected_dominating_set(G, D) + D = {1, 3} + assert not nx.is_connected_dominating_set(G, D) + D = {2, 3} + assert nx.is_connected(nx.subgraph(G, D)) + assert not nx.is_connected_dominating_set(G, D) + + +def test_null_graph_connected_dominating_set(): + G = nx.Graph() + assert 0 == len(nx.connected_dominating_set(G)) + + +def test_single_node_graph_connected_dominating_set(): + G = nx.Graph() + G.add_node(1) + CD = nx.connected_dominating_set(G) + assert nx.is_connected_dominating_set(G, CD) + + +def test_raise_disconnected_graph_connected_dominating_set(): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + G.add_node(1) + G.add_node(2) + nx.connected_dominating_set(G) + + +def test_complete_graph_connected_dominating_set(): + K5 = nx.complete_graph(5) + assert 1 == len(nx.connected_dominating_set(K5)) + K7 = nx.complete_graph(7) + assert 1 == len(nx.connected_dominating_set(K7)) + + +def test_docstring_example_connected_dominating_set(): + G = nx.Graph( + [ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 6), + (2, 7), + (3, 8), + (4, 9), + (5, 10), + (6, 11), + (7, 12), + (8, 12), + (9, 12), + (10, 12), + (11, 12), + ] + ) + assert {1, 2, 3, 4, 5, 6, 7} == nx.connected_dominating_set(G) + + +@pytest.mark.parametrize("seed", [1, 13, 29]) +@pytest.mark.parametrize("n,k,p", [(10, 3, 0.2), (100, 10, 0.7), (1000, 50, 0.5)]) +def test_connected_watts_strogatz_graph_connected_dominating_set(n, k, p, seed): + G = nx.connected_watts_strogatz_graph(n, k, p, seed=seed) + D = nx.connected_dominating_set(G) + assert nx.is_connected_dominating_set(G, D) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2e7d0463b3a0abeb8395df4ab870456faa64b7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_efficiency.py @@ -0,0 +1,58 @@ +"""Unit tests for the :mod:`networkx.algorithms.efficiency` module.""" + +import networkx as nx + + +class TestEfficiency: + def setup_method(self): + # G1 is a disconnected graph + self.G1 = nx.Graph() + self.G1.add_nodes_from([1, 2, 3]) + # G2 is a cycle graph + self.G2 = nx.cycle_graph(4) + # G3 is the triangle graph with one additional edge + self.G3 = nx.lollipop_graph(3, 1) + + def test_efficiency_disconnected_nodes(self): + """ + When nodes are disconnected, efficiency is 0 + """ + assert nx.efficiency(self.G1, 1, 2) == 0 + + def test_local_efficiency_disconnected_graph(self): + """ + In a disconnected graph the efficiency is 0 + """ + assert nx.local_efficiency(self.G1) == 0 + + def test_efficiency(self): + assert nx.efficiency(self.G2, 0, 1) == 1 + assert nx.efficiency(self.G2, 0, 2) == 1 / 2 + + def test_global_efficiency(self): + assert nx.global_efficiency(self.G2) == 5 / 6 + + def test_global_efficiency_complete_graph(self): + """ + Tests that the average global efficiency of the complete graph is one. + """ + for n in range(2, 10): + G = nx.complete_graph(n) + assert nx.global_efficiency(G) == 1 + + def test_local_efficiency_complete_graph(self): + """ + Test that the local efficiency for a complete graph with at least 3 + nodes should be one. For a graph with only 2 nodes, the induced + subgraph has no edges. + """ + for n in range(3, 10): + G = nx.complete_graph(n) + assert nx.local_efficiency(G) == 1 + + def test_using_ego_graph(self): + """ + Test that the ego graph is used when computing local efficiency. + For more information, see GitHub issue #2710. + """ + assert nx.local_efficiency(self.G3) == 7 / 12 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py new file mode 100644 index 0000000000000000000000000000000000000000..b5871f09b5a309df2bb00d9945ca9cf662e6f656 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_euler.py @@ -0,0 +1,314 @@ +import collections + +import pytest + +import networkx as nx + + +@pytest.mark.parametrize("f", (nx.is_eulerian, nx.is_semieulerian)) +def test_empty_graph_raises(f): + G = nx.Graph() + with pytest.raises(nx.NetworkXPointlessConcept, match="Connectivity is undefined"): + f(G) + + +class TestIsEulerian: + def test_is_eulerian(self): + assert nx.is_eulerian(nx.complete_graph(5)) + assert nx.is_eulerian(nx.complete_graph(7)) + assert nx.is_eulerian(nx.hypercube_graph(4)) + assert nx.is_eulerian(nx.hypercube_graph(6)) + + assert not nx.is_eulerian(nx.complete_graph(4)) + assert not nx.is_eulerian(nx.complete_graph(6)) + assert not nx.is_eulerian(nx.hypercube_graph(3)) + assert not nx.is_eulerian(nx.hypercube_graph(5)) + + assert not nx.is_eulerian(nx.petersen_graph()) + assert not nx.is_eulerian(nx.path_graph(4)) + + def test_is_eulerian2(self): + # not connected + G = nx.Graph() + G.add_nodes_from([1, 2, 3]) + assert not nx.is_eulerian(G) + # not strongly connected + G = nx.DiGraph() + G.add_nodes_from([1, 2, 3]) + assert not nx.is_eulerian(G) + G = nx.MultiDiGraph() + G.add_edge(1, 2) + G.add_edge(2, 3) + G.add_edge(2, 3) + G.add_edge(3, 1) + assert not nx.is_eulerian(G) + + +class TestEulerianCircuit: + def test_eulerian_circuit_cycle(self): + G = nx.cycle_graph(4) + + edges = list(nx.eulerian_circuit(G, source=0)) + nodes = [u for u, v in edges] + assert nodes == [0, 3, 2, 1] + assert edges == [(0, 3), (3, 2), (2, 1), (1, 0)] + + edges = list(nx.eulerian_circuit(G, source=1)) + nodes = [u for u, v in edges] + assert nodes == [1, 2, 3, 0] + assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)] + + G = nx.complete_graph(3) + + edges = list(nx.eulerian_circuit(G, source=0)) + nodes = [u for u, v in edges] + assert nodes == [0, 2, 1] + assert edges == [(0, 2), (2, 1), (1, 0)] + + edges = list(nx.eulerian_circuit(G, source=1)) + nodes = [u for u, v in edges] + assert nodes == [1, 2, 0] + assert edges == [(1, 2), (2, 0), (0, 1)] + + def test_eulerian_circuit_digraph(self): + G = nx.DiGraph() + nx.add_cycle(G, [0, 1, 2, 3]) + + edges = list(nx.eulerian_circuit(G, source=0)) + nodes = [u for u, v in edges] + assert nodes == [0, 1, 2, 3] + assert edges == [(0, 1), (1, 2), (2, 3), (3, 0)] + + edges = list(nx.eulerian_circuit(G, source=1)) + nodes = [u for u, v in edges] + assert nodes == [1, 2, 3, 0] + assert edges == [(1, 2), (2, 3), (3, 0), (0, 1)] + + def test_multigraph(self): + G = nx.MultiGraph() + nx.add_cycle(G, [0, 1, 2, 3]) + G.add_edge(1, 2) + G.add_edge(1, 2) + edges = list(nx.eulerian_circuit(G, source=0)) + nodes = [u for u, v in edges] + assert nodes == [0, 3, 2, 1, 2, 1] + assert edges == [(0, 3), (3, 2), (2, 1), (1, 2), (2, 1), (1, 0)] + + def test_multigraph_with_keys(self): + G = nx.MultiGraph() + nx.add_cycle(G, [0, 1, 2, 3]) + G.add_edge(1, 2) + G.add_edge(1, 2) + edges = list(nx.eulerian_circuit(G, source=0, keys=True)) + nodes = [u for u, v, k in edges] + assert nodes == [0, 3, 2, 1, 2, 1] + assert edges[:2] == [(0, 3, 0), (3, 2, 0)] + assert collections.Counter(edges[2:5]) == collections.Counter( + [(2, 1, 0), (1, 2, 1), (2, 1, 2)] + ) + assert edges[5:] == [(1, 0, 0)] + + def test_not_eulerian(self): + with pytest.raises(nx.NetworkXError): + f = list(nx.eulerian_circuit(nx.complete_graph(4))) + + +class TestIsSemiEulerian: + def test_is_semieulerian(self): + # Test graphs with Eulerian paths but no cycles return True. + assert nx.is_semieulerian(nx.path_graph(4)) + G = nx.path_graph(6, create_using=nx.DiGraph) + assert nx.is_semieulerian(G) + + # Test graphs with Eulerian cycles return False. + assert not nx.is_semieulerian(nx.complete_graph(5)) + assert not nx.is_semieulerian(nx.complete_graph(7)) + assert not nx.is_semieulerian(nx.hypercube_graph(4)) + assert not nx.is_semieulerian(nx.hypercube_graph(6)) + + +class TestHasEulerianPath: + def test_has_eulerian_path_cyclic(self): + # Test graphs with Eulerian cycles return True. + assert nx.has_eulerian_path(nx.complete_graph(5)) + assert nx.has_eulerian_path(nx.complete_graph(7)) + assert nx.has_eulerian_path(nx.hypercube_graph(4)) + assert nx.has_eulerian_path(nx.hypercube_graph(6)) + + def test_has_eulerian_path_non_cyclic(self): + # Test graphs with Eulerian paths but no cycles return True. + assert nx.has_eulerian_path(nx.path_graph(4)) + G = nx.path_graph(6, create_using=nx.DiGraph) + assert nx.has_eulerian_path(G) + + def test_has_eulerian_path_directed_graph(self): + # Test directed graphs and returns False + G = nx.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (0, 2)]) + assert not nx.has_eulerian_path(G) + + # Test directed graphs without isolated node returns True + G = nx.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 0)]) + assert nx.has_eulerian_path(G) + + # Test directed graphs with isolated node returns False + G.add_node(3) + assert not nx.has_eulerian_path(G) + + @pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) + def test_has_eulerian_path_not_weakly_connected(self, G): + G.add_edges_from([(0, 1), (2, 3), (3, 2)]) + assert not nx.has_eulerian_path(G) + + @pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) + def test_has_eulerian_path_unbalancedins_more_than_one(self, G): + G.add_edges_from([(0, 1), (2, 3)]) + assert not nx.has_eulerian_path(G) + + +class TestFindPathStart: + def testfind_path_start(self): + find_path_start = nx.algorithms.euler._find_path_start + # Test digraphs return correct starting node. + G = nx.path_graph(6, create_using=nx.DiGraph) + assert find_path_start(G) == 0 + edges = [(0, 1), (1, 2), (2, 0), (4, 0)] + assert find_path_start(nx.DiGraph(edges)) == 4 + + # Test graph with no Eulerian path return None. + edges = [(0, 1), (1, 2), (2, 3), (2, 4)] + assert find_path_start(nx.DiGraph(edges)) is None + + +class TestEulerianPath: + def test_eulerian_path(self): + x = [(4, 0), (0, 1), (1, 2), (2, 0)] + for e1, e2 in zip(x, nx.eulerian_path(nx.DiGraph(x))): + assert e1 == e2 + + def test_eulerian_path_straight_link(self): + G = nx.DiGraph() + result = [(1, 2), (2, 3), (3, 4), (4, 5)] + G.add_edges_from(result) + assert result == list(nx.eulerian_path(G)) + assert result == list(nx.eulerian_path(G, source=1)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=3)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=4)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=5)) + + def test_eulerian_path_multigraph(self): + G = nx.MultiDiGraph() + result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4), (4, 3)] + G.add_edges_from(result) + assert result == list(nx.eulerian_path(G)) + assert result == list(nx.eulerian_path(G, source=2)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=3)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=4)) + + def test_eulerian_path_eulerian_circuit(self): + G = nx.DiGraph() + result = [(1, 2), (2, 3), (3, 4), (4, 1)] + result2 = [(2, 3), (3, 4), (4, 1), (1, 2)] + result3 = [(3, 4), (4, 1), (1, 2), (2, 3)] + G.add_edges_from(result) + assert result == list(nx.eulerian_path(G)) + assert result == list(nx.eulerian_path(G, source=1)) + assert result2 == list(nx.eulerian_path(G, source=2)) + assert result3 == list(nx.eulerian_path(G, source=3)) + + def test_eulerian_path_undirected(self): + G = nx.Graph() + result = [(1, 2), (2, 3), (3, 4), (4, 5)] + result2 = [(5, 4), (4, 3), (3, 2), (2, 1)] + G.add_edges_from(result) + assert list(nx.eulerian_path(G)) in (result, result2) + assert result == list(nx.eulerian_path(G, source=1)) + assert result2 == list(nx.eulerian_path(G, source=5)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=3)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=2)) + + def test_eulerian_path_multigraph_undirected(self): + G = nx.MultiGraph() + result = [(2, 1), (1, 2), (2, 1), (1, 2), (2, 3), (3, 4)] + G.add_edges_from(result) + assert result == list(nx.eulerian_path(G)) + assert result == list(nx.eulerian_path(G, source=2)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=3)) + with pytest.raises(nx.NetworkXError): + list(nx.eulerian_path(G, source=1)) + + @pytest.mark.parametrize( + ("graph_type", "result"), + ( + (nx.MultiGraph, [(0, 1, 0), (1, 0, 1)]), + (nx.MultiDiGraph, [(0, 1, 0), (1, 0, 0)]), + ), + ) + def test_eulerian_with_keys(self, graph_type, result): + G = graph_type([(0, 1), (1, 0)]) + answer = nx.eulerian_path(G, keys=True) + assert list(answer) == result + + +class TestEulerize: + def test_disconnected(self): + with pytest.raises(nx.NetworkXError): + G = nx.from_edgelist([(0, 1), (2, 3)]) + nx.eulerize(G) + + def test_null_graph(self): + with pytest.raises(nx.NetworkXPointlessConcept): + nx.eulerize(nx.Graph()) + + def test_null_multigraph(self): + with pytest.raises(nx.NetworkXPointlessConcept): + nx.eulerize(nx.MultiGraph()) + + def test_on_empty_graph(self): + with pytest.raises(nx.NetworkXError): + nx.eulerize(nx.empty_graph(3)) + + def test_on_eulerian(self): + G = nx.cycle_graph(3) + H = nx.eulerize(G) + assert nx.is_isomorphic(G, H) + + def test_on_eulerian_multigraph(self): + G = nx.MultiGraph(nx.cycle_graph(3)) + G.add_edge(0, 1) + H = nx.eulerize(G) + assert nx.is_eulerian(H) + + def test_on_complete_graph(self): + G = nx.complete_graph(4) + assert nx.is_eulerian(nx.eulerize(G)) + assert nx.is_eulerian(nx.eulerize(nx.MultiGraph(G))) + + def test_on_non_eulerian_graph(self): + G = nx.cycle_graph(18) + G.add_edge(0, 18) + G.add_edge(18, 19) + G.add_edge(17, 19) + G.add_edge(4, 20) + G.add_edge(20, 21) + G.add_edge(21, 22) + G.add_edge(22, 23) + G.add_edge(23, 24) + G.add_edge(24, 25) + G.add_edge(25, 26) + G.add_edge(26, 27) + G.add_edge(27, 28) + G.add_edge(28, 13) + assert not nx.is_eulerian(G) + G = nx.eulerize(G) + assert nx.is_eulerian(G) + assert nx.number_of_edges(G) == 39 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py new file mode 100644 index 0000000000000000000000000000000000000000..6c90c8ff128a02143c48322853bc2dcaa5f6fffc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graph_hashing.py @@ -0,0 +1,872 @@ +import copy + +import pytest + +import networkx as nx + +# Unit tests relevant for both functions in this module. + + +def test_positive_iters(): + G1 = nx.empty_graph() + with pytest.raises( + ValueError, + match="The WL algorithm requires that `iterations` be positive", + ): + nx.weisfeiler_lehman_graph_hash(G1, iterations=-3) + with pytest.raises( + ValueError, + match="The WL algorithm requires that `iterations` be positive", + ): + nx.weisfeiler_lehman_subgraph_hashes(G1, iterations=-3) + with pytest.raises( + ValueError, + match="The WL algorithm requires that `iterations` be positive", + ): + nx.weisfeiler_lehman_graph_hash(G1, iterations=0) + with pytest.raises( + ValueError, + match="The WL algorithm requires that `iterations` be positive", + ): + nx.weisfeiler_lehman_subgraph_hashes(G1, iterations=0) + + +# Unit tests for the :func:`~networkx.weisfeiler_lehman_graph_hash` function + + +def test_empty_graph_hash(): + """ + empty graphs should give hashes regardless of other params + """ + G1 = nx.empty_graph() + G2 = nx.empty_graph() + + h1 = nx.weisfeiler_lehman_graph_hash(G1) + h2 = nx.weisfeiler_lehman_graph_hash(G2) + h3 = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="edge_attr1") + h4 = nx.weisfeiler_lehman_graph_hash(G2, node_attr="node_attr1") + h5 = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="edge_attr1", node_attr="node_attr1" + ) + h6 = nx.weisfeiler_lehman_graph_hash(G2, iterations=10) + + assert h1 == h2 + assert h1 == h3 + assert h1 == h4 + assert h1 == h5 + assert h1 == h6 + + +def test_directed(): + """ + A directed graph with no bi-directional edges should yield different a graph hash + to the same graph taken as undirected if there are no hash collisions. + """ + r = 10 + for i in range(r): + G_directed = nx.gn_graph(10 + r, seed=100 + i) + G_undirected = nx.to_undirected(G_directed) + + h_directed = nx.weisfeiler_lehman_graph_hash(G_directed) + h_undirected = nx.weisfeiler_lehman_graph_hash(G_undirected) + + assert h_directed != h_undirected + + +def test_reversed(): + """ + A directed graph with no bi-directional edges should yield different a graph hash + to the same graph taken with edge directions reversed if there are no hash + collisions. Here we test a cycle graph which is the minimal counterexample + """ + G = nx.cycle_graph(5, create_using=nx.DiGraph) + nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label") + + G_reversed = G.reverse() + + h = nx.weisfeiler_lehman_graph_hash(G, node_attr="label") + h_reversed = nx.weisfeiler_lehman_graph_hash(G_reversed, node_attr="label") + + assert h != h_reversed + + +def test_isomorphic(): + """ + graph hashes should be invariant to node-relabeling (when the output is reindexed + by the same mapping) + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i) + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g1_hash = nx.weisfeiler_lehman_graph_hash(G1) + g2_hash = nx.weisfeiler_lehman_graph_hash(G2) + + assert g1_hash == g2_hash + + +def test_isomorphic_edge_attr(): + """ + Isomorphic graphs with differing edge attributes should yield different graph + hashes if the 'edge_attr' argument is supplied and populated in the graph, + and there are no hash collisions. + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i) + + for a, b in G1.edges: + G1[a][b]["edge_attr1"] = f"{a}-{b}-1" + G1[a][b]["edge_attr2"] = f"{a}-{b}-2" + + g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="edge_attr1" + ) + g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="edge_attr2" + ) + g1_hash_no_edge_attr = nx.weisfeiler_lehman_graph_hash(G1, edge_attr=None) + + assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr + assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr + assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="edge_attr1" + ) + g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="edge_attr2" + ) + + assert g1_hash_with_edge_attr1 == g2_hash_with_edge_attr1 + assert g1_hash_with_edge_attr2 == g2_hash_with_edge_attr2 + + +def test_missing_edge_attr(): + """ + If the 'edge_attr' argument is supplied but is missing from an edge in the graph, + we should raise a KeyError + """ + G = nx.Graph() + G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})]) + pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, edge_attr="edge_attr1") + + +def test_isomorphic_node_attr(): + """ + Isomorphic graphs with differing node attributes should yield different graph + hashes if the 'node_attr' argument is supplied and populated in the graph, and + there are no hash collisions. + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i) + + for u in G1.nodes(): + G1.nodes[u]["node_attr1"] = f"{u}-1" + G1.nodes[u]["node_attr2"] = f"{u}-2" + + g1_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash( + G1, node_attr="node_attr1" + ) + g1_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash( + G1, node_attr="node_attr2" + ) + g1_hash_no_node_attr = nx.weisfeiler_lehman_graph_hash(G1, node_attr=None) + + assert g1_hash_with_node_attr1 != g1_hash_no_node_attr + assert g1_hash_with_node_attr2 != g1_hash_no_node_attr + assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_with_node_attr1 = nx.weisfeiler_lehman_graph_hash( + G2, node_attr="node_attr1" + ) + g2_hash_with_node_attr2 = nx.weisfeiler_lehman_graph_hash( + G2, node_attr="node_attr2" + ) + + assert g1_hash_with_node_attr1 == g2_hash_with_node_attr1 + assert g1_hash_with_node_attr2 == g2_hash_with_node_attr2 + + +def test_missing_node_attr(): + """ + If the 'node_attr' argument is supplied but is missing from a node in the graph, + we should raise a KeyError + """ + G = nx.Graph() + G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})]) + G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)]) + pytest.raises(KeyError, nx.weisfeiler_lehman_graph_hash, G, node_attr="node_attr1") + + +def test_isomorphic_edge_attr_and_node_attr(): + """ + Isomorphic graphs with differing node attributes should yield different graph + hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in + the graph, and there are no hash collisions. + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i) + + for u in G1.nodes(): + G1.nodes[u]["node_attr1"] = f"{u}-1" + G1.nodes[u]["node_attr2"] = f"{u}-2" + + for a, b in G1.edges: + G1[a][b]["edge_attr1"] = f"{a}-{b}-1" + G1[a][b]["edge_attr2"] = f"{a}-{b}-2" + + g1_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="edge_attr1", node_attr="node_attr1" + ) + g1_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="edge_attr2", node_attr="node_attr2" + ) + g1_hash_edge1_node2 = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="edge_attr1", node_attr="node_attr2" + ) + g1_hash_no_attr = nx.weisfeiler_lehman_graph_hash(G1) + + assert g1_hash_edge1_node1 != g1_hash_no_attr + assert g1_hash_edge2_node2 != g1_hash_no_attr + assert g1_hash_edge1_node1 != g1_hash_edge2_node2 + assert g1_hash_edge1_node2 != g1_hash_edge2_node2 + assert g1_hash_edge1_node2 != g1_hash_edge1_node1 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_edge1_node1 = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="edge_attr1", node_attr="node_attr1" + ) + g2_hash_edge2_node2 = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="edge_attr2", node_attr="node_attr2" + ) + + assert g1_hash_edge1_node1 == g2_hash_edge1_node1 + assert g1_hash_edge2_node2 == g2_hash_edge2_node2 + + +def test_digest_size(): + """ + The hash string lengths should be as expected for a variety of graphs and + digest sizes + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i) + + h16 = nx.weisfeiler_lehman_graph_hash(G) + h32 = nx.weisfeiler_lehman_graph_hash(G, digest_size=32) + + assert h16 != h32 + assert len(h16) == 16 * 2 + assert len(h32) == 32 * 2 + + +def test_directed_bugs(): + """ + These were bugs for directed graphs as discussed in issue #7806 + """ + Ga = nx.DiGraph() + Gb = nx.DiGraph() + Ga.add_nodes_from([1, 2, 3, 4]) + Gb.add_nodes_from([1, 2, 3, 4]) + Ga.add_edges_from([(1, 2), (3, 2)]) + Gb.add_edges_from([(1, 2), (3, 4)]) + Ga_hash = nx.weisfeiler_lehman_graph_hash(Ga) + Gb_hash = nx.weisfeiler_lehman_graph_hash(Gb) + assert Ga_hash != Gb_hash + + Tree1 = nx.DiGraph() + Tree1.add_edges_from([(0, 4), (1, 5), (2, 6), (3, 7)]) + Tree1.add_edges_from([(4, 8), (5, 8), (6, 9), (7, 9)]) + Tree1.add_edges_from([(8, 10), (9, 10)]) + nx.set_node_attributes( + Tree1, {10: "s", 8: "a", 9: "a", 4: "b", 5: "b", 6: "b", 7: "b"}, "weight" + ) + Tree2 = copy.deepcopy(Tree1) + nx.set_node_attributes(Tree1, {0: "d", 1: "c", 2: "d", 3: "c"}, "weight") + nx.set_node_attributes(Tree2, {0: "d", 1: "d", 2: "c", 3: "c"}, "weight") + Tree1_hash_short = nx.weisfeiler_lehman_graph_hash( + Tree1, iterations=1, node_attr="weight" + ) + Tree2_hash_short = nx.weisfeiler_lehman_graph_hash( + Tree2, iterations=1, node_attr="weight" + ) + assert Tree1_hash_short == Tree2_hash_short + Tree1_hash = nx.weisfeiler_lehman_graph_hash( + Tree1, node_attr="weight" + ) # Default is 3 iterations + Tree2_hash = nx.weisfeiler_lehman_graph_hash(Tree2, node_attr="weight") + assert Tree1_hash != Tree2_hash + + +def test_trivial_labels_isomorphism(): + """ + Trivial labelling of the graph should not change isomorphism verdicts. + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i) + G2 = nx.erdos_renyi_graph(n, p * i, seed=42 + i) + G1_hash = nx.weisfeiler_lehman_graph_hash(G1) + G2_hash = nx.weisfeiler_lehman_graph_hash(G2) + equal = G1_hash == G2_hash + + nx.set_node_attributes(G1, values=1, name="weight") + nx.set_node_attributes(G2, values=1, name="weight") + G1_hash_node = nx.weisfeiler_lehman_graph_hash(G1, node_attr="weight") + G2_hash_node = nx.weisfeiler_lehman_graph_hash(G2, node_attr="weight") + equal_node = G1_hash_node == G2_hash_node + + nx.set_edge_attributes(G1, values="a", name="e_weight") + nx.set_edge_attributes(G2, values="a", name="e_weight") + G1_hash_edge = nx.weisfeiler_lehman_graph_hash(G1, edge_attr="e_weight") + G2_hash_edge = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="e_weight") + equal_edge = G1_hash_edge == G2_hash_edge + + G1_hash_both = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="e_weight", node_attr="weight" + ) + G2_hash_both = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="e_weight", node_attr="weight" + ) + equal_both = G1_hash_both == G2_hash_both + + assert equal == equal_node + assert equal_node == equal_edge + assert equal_edge == equal_both + + +def test_trivial_labels_isomorphism_directed(): + """ + Trivial labelling of the graph should not change isomorphism verdicts on digraphs. + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, directed=True, seed=500 + i) + G2 = nx.erdos_renyi_graph(n, p * i, directed=True, seed=42 + i) + G1_hash = nx.weisfeiler_lehman_graph_hash(G1) + G2_hash = nx.weisfeiler_lehman_graph_hash(G2) + equal = G1_hash == G2_hash + + nx.set_node_attributes(G1, values=1, name="weight") + nx.set_node_attributes(G2, values=1, name="weight") + G1_hash_node = nx.weisfeiler_lehman_graph_hash(G1, node_attr="weight") + G2_hash_node = nx.weisfeiler_lehman_graph_hash(G2, node_attr="weight") + equal_node = G1_hash_node == G2_hash_node + + nx.set_edge_attributes(G1, values="a", name="e_weight") + nx.set_edge_attributes(G2, values="a", name="e_weight") + G1_hash_edge = nx.weisfeiler_lehman_graph_hash(G1, edge_attr="e_weight") + G2_hash_edge = nx.weisfeiler_lehman_graph_hash(G2, edge_attr="e_weight") + equal_edge = G1_hash_edge == G2_hash_edge + + G1_hash_both = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="e_weight", node_attr="weight" + ) + G2_hash_both = nx.weisfeiler_lehman_graph_hash( + G2, edge_attr="e_weight", node_attr="weight" + ) + equal_both = G1_hash_both == G2_hash_both + + assert equal == equal_node + assert equal_node == equal_edge + assert equal_edge == equal_both + + # Specific case that was found to be a bug in issue #7806 + # Without weights worked + Ga = nx.DiGraph() + Ga.add_nodes_from([1, 2, 3, 4]) + Gb = copy.deepcopy(Ga) + Ga.add_edges_from([(1, 2), (3, 2)]) + Gb.add_edges_from([(1, 2), (3, 4)]) + Ga_hash = nx.weisfeiler_lehman_graph_hash(Ga) + Gb_hash = nx.weisfeiler_lehman_graph_hash(Gb) + assert Ga_hash != Gb_hash + + # Now with trivial weights + nx.set_node_attributes(Ga, values=1, name="weight") + nx.set_node_attributes(Gb, values=1, name="weight") + Ga_hash = nx.weisfeiler_lehman_graph_hash(Ga, node_attr="weight") + Gb_hash = nx.weisfeiler_lehman_graph_hash(Gb, node_attr="weight") + assert Ga_hash != Gb_hash + + +def test_trivial_labels_hashes(): + """ + Test that 'empty' labelling of nodes or edges shouldn't have a different impact + on the calculated hash. Note that we cannot assume that trivial weights have no + impact at all. Without (trivial) weights, a node will start with hashing its + degree. This step is omitted when there are weights. + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i) + nx.set_node_attributes(G1, values="", name="weight") + first = nx.weisfeiler_lehman_graph_hash(G1, node_attr="weight") + nx.set_edge_attributes(G1, values="", name="e_weight") + second = nx.weisfeiler_lehman_graph_hash(G1, edge_attr="e_weight") + assert first == second + third = nx.weisfeiler_lehman_graph_hash( + G1, edge_attr="e_weight", node_attr="weight" + ) + assert second == third + + +# Unit tests for the :func:`~networkx.weisfeiler_lehman_subgraph_hashes` function + + +def is_subiteration(a, b): + """ + returns True if that each hash sequence in 'a' is a prefix for + the corresponding sequence indexed by the same node in 'b'. + """ + return all(b[node][: len(hashes)] == hashes for node, hashes in a.items()) + + +def hexdigest_sizes_correct(a, digest_size): + """ + returns True if all hex digest sizes are the expected length in a + node:subgraph-hashes dictionary. Hex digest string length == 2 * bytes digest + length since each pair of hex digits encodes 1 byte + (https://docs.python.org/3/library/hashlib.html) + """ + hexdigest_size = digest_size * 2 + + def list_digest_sizes_correct(l): + return all(len(x) == hexdigest_size for x in l) + + return all(list_digest_sizes_correct(hashes) for hashes in a.values()) + + +def test_empty_graph_subgraph_hash(): + """ " + empty graphs should give empty dict subgraph hashes regardless of other params + """ + G = nx.empty_graph() + + subgraph_hashes1 = nx.weisfeiler_lehman_subgraph_hashes(G) + subgraph_hashes2 = nx.weisfeiler_lehman_subgraph_hashes(G, edge_attr="edge_attr") + subgraph_hashes3 = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="edge_attr") + subgraph_hashes4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=2) + subgraph_hashes5 = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=64) + + assert subgraph_hashes1 == {} + assert subgraph_hashes2 == {} + assert subgraph_hashes3 == {} + assert subgraph_hashes4 == {} + assert subgraph_hashes5 == {} + + +def test_directed_subgraph_hash(): + """ + A directed graph with no bi-directional edges should yield different subgraph + hashes to the same graph taken as undirected, if all hashes don't collide. + """ + r = 10 + for i in range(r): + G_directed = nx.gn_graph(10 + r, seed=100 + i) + G_undirected = nx.to_undirected(G_directed) + + directed_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_directed) + undirected_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G_undirected) + + assert directed_subgraph_hashes != undirected_subgraph_hashes + + +def test_reversed_subgraph_hash(): + """ + A directed graph with no bi-directional edges should yield different subgraph + hashes to the same graph taken with edge directions reversed if there are no + hash collisions. Here we test a cycle graph which is the minimal counterexample + """ + G = nx.cycle_graph(5, create_using=nx.DiGraph) + nx.set_node_attributes(G, {n: str(n) for n in G.nodes()}, name="label") + + G_reversed = G.reverse() + + h = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label") + h_reversed = nx.weisfeiler_lehman_subgraph_hashes(G_reversed, node_attr="label") + + assert h != h_reversed + + +def test_isomorphic_subgraph_hash(): + """ + the subgraph hashes should be invariant to node-relabeling when the output is + reindexed by the same mapping and all hashes don't collide. + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=200 + i) + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g1_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G1) + g2_subgraph_hashes = nx.weisfeiler_lehman_subgraph_hashes(G2) + + assert g1_subgraph_hashes == {-1 * k: v for k, v in g2_subgraph_hashes.items()} + + +def test_isomorphic_edge_attr_subgraph_hash(): + """ + Isomorphic graphs with differing edge attributes should yield different subgraph + hashes if the 'edge_attr' argument is supplied and populated in the graph, and + all hashes don't collide. + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=300 + i) + + for a, b in G1.edges: + G1[a][b]["edge_attr1"] = f"{a}-{b}-1" + G1[a][b]["edge_attr2"] = f"{a}-{b}-2" + + g1_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes( + G1, edge_attr="edge_attr1" + ) + g1_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes( + G1, edge_attr="edge_attr2" + ) + g1_hash_no_edge_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, edge_attr=None) + + assert g1_hash_with_edge_attr1 != g1_hash_no_edge_attr + assert g1_hash_with_edge_attr2 != g1_hash_no_edge_attr + assert g1_hash_with_edge_attr1 != g1_hash_with_edge_attr2 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_with_edge_attr1 = nx.weisfeiler_lehman_subgraph_hashes( + G2, edge_attr="edge_attr1" + ) + g2_hash_with_edge_attr2 = nx.weisfeiler_lehman_subgraph_hashes( + G2, edge_attr="edge_attr2" + ) + + assert g1_hash_with_edge_attr1 == { + -1 * k: v for k, v in g2_hash_with_edge_attr1.items() + } + assert g1_hash_with_edge_attr2 == { + -1 * k: v for k, v in g2_hash_with_edge_attr2.items() + } + + +def test_missing_edge_attr_subgraph_hash(): + """ + If the 'edge_attr' argument is supplied but is missing from an edge in the graph, + we should raise a KeyError + """ + G = nx.Graph() + G.add_edges_from([(1, 2, {"edge_attr1": "a"}), (1, 3, {})]) + pytest.raises( + KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, edge_attr="edge_attr1" + ) + + +def test_isomorphic_node_attr_subgraph_hash(): + """ + Isomorphic graphs with differing node attributes should yield different subgraph + hashes if the 'node_attr' argument is supplied and populated in the graph, and + all hashes don't collide. + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=400 + i) + + for u in G1.nodes(): + G1.nodes[u]["node_attr1"] = f"{u}-1" + G1.nodes[u]["node_attr2"] = f"{u}-2" + + g1_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes( + G1, node_attr="node_attr1" + ) + g1_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes( + G1, node_attr="node_attr2" + ) + g1_hash_no_node_attr = nx.weisfeiler_lehman_subgraph_hashes(G1, node_attr=None) + + assert g1_hash_with_node_attr1 != g1_hash_no_node_attr + assert g1_hash_with_node_attr2 != g1_hash_no_node_attr + assert g1_hash_with_node_attr1 != g1_hash_with_node_attr2 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_with_node_attr1 = nx.weisfeiler_lehman_subgraph_hashes( + G2, node_attr="node_attr1" + ) + g2_hash_with_node_attr2 = nx.weisfeiler_lehman_subgraph_hashes( + G2, node_attr="node_attr2" + ) + + assert g1_hash_with_node_attr1 == { + -1 * k: v for k, v in g2_hash_with_node_attr1.items() + } + assert g1_hash_with_node_attr2 == { + -1 * k: v for k, v in g2_hash_with_node_attr2.items() + } + + +def test_missing_node_attr_subgraph_hash(): + """ + If the 'node_attr' argument is supplied but is missing from a node in the graph, + we should raise a KeyError + """ + G = nx.Graph() + G.add_nodes_from([(1, {"node_attr1": "a"}), (2, {})]) + G.add_edges_from([(1, 2), (2, 3), (3, 1), (1, 4)]) + pytest.raises( + KeyError, nx.weisfeiler_lehman_subgraph_hashes, G, node_attr="node_attr1" + ) + + +def test_isomorphic_edge_attr_and_node_attr_subgraph_hash(): + """ + Isomorphic graphs with differing node attributes should yield different subgraph + hashes if the 'node_attr' and 'edge_attr' argument is supplied and populated in + the graph, and all hashes don't collide + The output should still be invariant to node-relabeling + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G1 = nx.erdos_renyi_graph(n, p * i, seed=500 + i) + + for u in G1.nodes(): + G1.nodes[u]["node_attr1"] = f"{u}-1" + G1.nodes[u]["node_attr2"] = f"{u}-2" + + for a, b in G1.edges: + G1[a][b]["edge_attr1"] = f"{a}-{b}-1" + G1[a][b]["edge_attr2"] = f"{a}-{b}-2" + + g1_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes( + G1, edge_attr="edge_attr1", node_attr="node_attr1" + ) + g1_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes( + G1, edge_attr="edge_attr2", node_attr="node_attr2" + ) + g1_hash_edge1_node2 = nx.weisfeiler_lehman_subgraph_hashes( + G1, edge_attr="edge_attr1", node_attr="node_attr2" + ) + g1_hash_no_attr = nx.weisfeiler_lehman_subgraph_hashes(G1) + + assert g1_hash_edge1_node1 != g1_hash_no_attr + assert g1_hash_edge2_node2 != g1_hash_no_attr + assert g1_hash_edge1_node1 != g1_hash_edge2_node2 + assert g1_hash_edge1_node2 != g1_hash_edge2_node2 + assert g1_hash_edge1_node2 != g1_hash_edge1_node1 + + G2 = nx.relabel_nodes(G1, {u: -1 * u for u in G1.nodes()}) + + g2_hash_edge1_node1 = nx.weisfeiler_lehman_subgraph_hashes( + G2, edge_attr="edge_attr1", node_attr="node_attr1" + ) + g2_hash_edge2_node2 = nx.weisfeiler_lehman_subgraph_hashes( + G2, edge_attr="edge_attr2", node_attr="node_attr2" + ) + + assert g1_hash_edge1_node1 == { + -1 * k: v for k, v in g2_hash_edge1_node1.items() + } + assert g1_hash_edge2_node2 == { + -1 * k: v for k, v in g2_hash_edge2_node2.items() + } + + +def test_iteration_depth(): + """ + All nodes should have the correct number of subgraph hashes in the output when + using degree as initial node labels. + Subsequent iteration depths for the same graph should be additive for each node + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=600 + i) + + depth3 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=3) + depth4 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=4) + depth5 = nx.weisfeiler_lehman_subgraph_hashes(G, iterations=5) + + assert all(len(hashes) == 3 for hashes in depth3.values()) + assert all(len(hashes) == 4 for hashes in depth4.values()) + assert all(len(hashes) == 5 for hashes in depth5.values()) + + assert is_subiteration(depth3, depth4) + assert is_subiteration(depth4, depth5) + assert is_subiteration(depth3, depth5) + + +def test_iteration_depth_edge_attr(): + """ + All nodes should have the correct number of subgraph hashes in the output when + setting initial node labels empty and using an edge attribute when aggregating + neighborhoods. + Subsequent iteration depths for the same graph should be additive for each node + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=700 + i) + + for a, b in G.edges: + G[a][b]["edge_attr1"] = f"{a}-{b}-1" + + depth3 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", iterations=3 + ) + depth4 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", iterations=4 + ) + depth5 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", iterations=5 + ) + + assert all(len(hashes) == 3 for hashes in depth3.values()) + assert all(len(hashes) == 4 for hashes in depth4.values()) + assert all(len(hashes) == 5 for hashes in depth5.values()) + + assert is_subiteration(depth3, depth4) + assert is_subiteration(depth4, depth5) + assert is_subiteration(depth3, depth5) + + +def test_iteration_depth_node_attr(): + """ + All nodes should have the correct number of subgraph hashes in the output when + setting initial node labels to an attribute. + Subsequent iteration depths for the same graph should be additive for each node + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=800 + i) + + for u in G.nodes(): + G.nodes[u]["node_attr1"] = f"{u}-1" + + depth3 = nx.weisfeiler_lehman_subgraph_hashes( + G, node_attr="node_attr1", iterations=3 + ) + depth4 = nx.weisfeiler_lehman_subgraph_hashes( + G, node_attr="node_attr1", iterations=4 + ) + depth5 = nx.weisfeiler_lehman_subgraph_hashes( + G, node_attr="node_attr1", iterations=5 + ) + + assert all(len(hashes) == 3 for hashes in depth3.values()) + assert all(len(hashes) == 4 for hashes in depth4.values()) + assert all(len(hashes) == 5 for hashes in depth5.values()) + + assert is_subiteration(depth3, depth4) + assert is_subiteration(depth4, depth5) + assert is_subiteration(depth3, depth5) + + +def test_iteration_depth_node_edge_attr(): + """ + All nodes should have the correct number of subgraph hashes in the output when + setting initial node labels to an attribute and also using an edge attribute when + aggregating neighborhoods. + Subsequent iteration depths for the same graph should be additive for each node + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=900 + i) + + for u in G.nodes(): + G.nodes[u]["node_attr1"] = f"{u}-1" + + for a, b in G.edges: + G[a][b]["edge_attr1"] = f"{a}-{b}-1" + + depth3 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=3 + ) + depth4 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=4 + ) + depth5 = nx.weisfeiler_lehman_subgraph_hashes( + G, edge_attr="edge_attr1", node_attr="node_attr1", iterations=5 + ) + + assert all(len(hashes) == 3 for hashes in depth3.values()) + assert all(len(hashes) == 4 for hashes in depth4.values()) + assert all(len(hashes) == 5 for hashes in depth5.values()) + + assert is_subiteration(depth3, depth4) + assert is_subiteration(depth4, depth5) + assert is_subiteration(depth3, depth5) + + +def test_digest_size_subgraph_hash(): + """ + The hash string lengths should be as expected for a variety of graphs and + digest sizes + """ + n, r = 100, 10 + p = 1.0 / r + for i in range(1, r + 1): + G = nx.erdos_renyi_graph(n, p * i, seed=1000 + i) + + digest_size16_hashes = nx.weisfeiler_lehman_subgraph_hashes(G) + digest_size32_hashes = nx.weisfeiler_lehman_subgraph_hashes(G, digest_size=32) + + assert digest_size16_hashes != digest_size32_hashes + + assert hexdigest_sizes_correct(digest_size16_hashes, 16) + assert hexdigest_sizes_correct(digest_size32_hashes, 32) + + +def test_initial_node_labels_subgraph_hash(): + """ + Including the hashed initial label prepends an extra hash to the lists + """ + G = nx.path_graph(5) + nx.set_node_attributes(G, {i: int(0 < i < 4) for i in G}, "label") + # initial node labels: + # 0--1--1--1--0 + + without_initial_label = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="label") + assert all(len(v) == 3 for v in without_initial_label.values()) + # 3 different 1 hop nhds + assert len({v[0] for v in without_initial_label.values()}) == 3 + + with_initial_label = nx.weisfeiler_lehman_subgraph_hashes( + G, node_attr="label", include_initial_labels=True + ) + assert all(len(v) == 4 for v in with_initial_label.values()) + # 2 different initial labels + assert len({v[0] for v in with_initial_label.values()}) == 2 + + # check hashes match otherwise + for u in G: + for a, b in zip( + with_initial_label[u][1:], without_initial_label[u], strict=True + ): + assert a == b diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py new file mode 100644 index 0000000000000000000000000000000000000000..99f766f799d8573e80d905482f4b685a2d16bcc0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_graphical.py @@ -0,0 +1,163 @@ +import pytest + +import networkx as nx + + +def test_valid_degree_sequence1(): + n = 100 + p = 0.3 + for i in range(10): + G = nx.erdos_renyi_graph(n, p) + deg = (d for n, d in G.degree()) + assert nx.is_graphical(deg, method="eg") + assert nx.is_graphical(deg, method="hh") + + +def test_valid_degree_sequence2(): + n = 100 + for i in range(10): + G = nx.barabasi_albert_graph(n, 1) + deg = (d for n, d in G.degree()) + assert nx.is_graphical(deg, method="eg") + assert nx.is_graphical(deg, method="hh") + + +def test_string_input(): + pytest.raises(nx.NetworkXException, nx.is_graphical, [], "foo") + pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "hh") + pytest.raises(nx.NetworkXException, nx.is_graphical, ["red"], "eg") + + +def test_non_integer_input(): + pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "eg") + pytest.raises(nx.NetworkXException, nx.is_graphical, [72.5], "hh") + + +def test_negative_input(): + assert not nx.is_graphical([-1], "hh") + assert not nx.is_graphical([-1], "eg") + + +class TestAtlas: + @classmethod + def setup_class(cls): + global atlas + from networkx.generators import atlas + + cls.GAG = atlas.graph_atlas_g() + + def test_atlas(self): + for graph in self.GAG: + deg = (d for n, d in graph.degree()) + assert nx.is_graphical(deg, method="eg") + assert nx.is_graphical(deg, method="hh") + + +def test_small_graph_true(): + z = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + assert nx.is_graphical(z, method="hh") + assert nx.is_graphical(z, method="eg") + z = [10, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2] + assert nx.is_graphical(z, method="hh") + assert nx.is_graphical(z, method="eg") + z = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + assert nx.is_graphical(z, method="hh") + assert nx.is_graphical(z, method="eg") + + +def test_small_graph_false(): + z = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + assert not nx.is_graphical(z, method="hh") + assert not nx.is_graphical(z, method="eg") + z = [6, 5, 4, 4, 2, 1, 1, 1] + assert not nx.is_graphical(z, method="hh") + assert not nx.is_graphical(z, method="eg") + z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + assert not nx.is_graphical(z, method="hh") + assert not nx.is_graphical(z, method="eg") + + +def test_directed_degree_sequence(): + # Test a range of valid directed degree sequences + n, r = 100, 10 + p = 1.0 / r + for i in range(r): + G = nx.erdos_renyi_graph(n, p * (i + 1), None, True) + din = (d for n, d in G.in_degree()) + dout = (d for n, d in G.out_degree()) + assert nx.is_digraphical(din, dout) + + +def test_small_directed_sequences(): + dout = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + din = [3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1] + assert nx.is_digraphical(din, dout) + # Test nongraphical directed sequence + dout = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + din = [103, 102, 102, 102, 102, 102, 102, 102, 102, 102] + assert not nx.is_digraphical(din, dout) + # Test digraphical small sequence + dout = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1] + assert nx.is_digraphical(din, dout) + # Test nonmatching sum + din = [2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1] + assert not nx.is_digraphical(din, dout) + # Test for negative integer in sequence + din = [2, 2, 2, -2, 2, 2, 2, 2, 1, 1, 4] + assert not nx.is_digraphical(din, dout) + # Test for noninteger + din = dout = [1, 1, 1.1, 1] + assert not nx.is_digraphical(din, dout) + din = dout = [1, 1, "rer", 1] + assert not nx.is_digraphical(din, dout) + + +def test_multi_sequence(): + # Test nongraphical multi sequence + seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1] + assert not nx.is_multigraphical(seq) + # Test small graphical multi sequence + seq = [6, 5, 4, 4, 2, 1, 1, 1] + assert nx.is_multigraphical(seq) + # Test for negative integer in sequence + seq = [6, 5, 4, -4, 2, 1, 1, 1] + assert not nx.is_multigraphical(seq) + # Test for sequence with odd sum + seq = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + assert not nx.is_multigraphical(seq) + # Test for noninteger + seq = [1, 1, 1.1, 1] + assert not nx.is_multigraphical(seq) + seq = [1, 1, "rer", 1] + assert not nx.is_multigraphical(seq) + + +def test_pseudo_sequence(): + # Test small valid pseudo sequence + seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1] + assert nx.is_pseudographical(seq) + # Test for sequence with odd sum + seq = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + assert not nx.is_pseudographical(seq) + # Test for negative integer in sequence + seq = [1000, 3, 3, 3, 3, 2, 2, -2, 1, 1] + assert not nx.is_pseudographical(seq) + # Test for noninteger + seq = [1, 1, 1.1, 1] + assert not nx.is_pseudographical(seq) + seq = [1, 1, "rer", 1] + assert not nx.is_pseudographical(seq) + + +def test_numpy_degree_sequence(): + np = pytest.importorskip("numpy") + ds = np.array([1, 2, 2, 2, 1], dtype=np.int64) + assert nx.is_graphical(ds, "eg") + assert nx.is_graphical(ds, "hh") + ds = np.array([1, 2, 2, 2, 1], dtype=np.float64) + assert nx.is_graphical(ds, "eg") + assert nx.is_graphical(ds, "hh") + ds = np.array([1.1, 2, 2, 2, 1], dtype=np.float64) + pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "eg") + pytest.raises(nx.NetworkXException, nx.is_graphical, ds, "hh") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py new file mode 100644 index 0000000000000000000000000000000000000000..eaa6a67b8b7f048719aa189b8365ef8e4c65951c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hierarchy.py @@ -0,0 +1,46 @@ +import pytest + +import networkx as nx + + +def test_hierarchy_undirected(): + G = nx.cycle_graph(5) + pytest.raises(nx.NetworkXError, nx.flow_hierarchy, G) + + +def test_hierarchy_cycle(): + G = nx.cycle_graph(5, create_using=nx.DiGraph()) + assert nx.flow_hierarchy(G) == 0.0 + + +def test_hierarchy_tree(): + G = nx.full_rary_tree(2, 16, create_using=nx.DiGraph()) + assert nx.flow_hierarchy(G) == 1.0 + + +def test_hierarchy_1(): + G = nx.DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 1), (3, 4), (0, 4)]) + assert nx.flow_hierarchy(G) == 0.5 + + +def test_hierarchy_weight(): + G = nx.DiGraph() + G.add_edges_from( + [ + (0, 1, {"weight": 0.3}), + (1, 2, {"weight": 0.1}), + (2, 3, {"weight": 0.1}), + (3, 1, {"weight": 0.1}), + (3, 4, {"weight": 0.3}), + (0, 4, {"weight": 0.3}), + ] + ) + assert nx.flow_hierarchy(G, weight="weight") == 0.75 + + +@pytest.mark.parametrize("n", (0, 1, 3)) +def test_hierarchy_empty_graph(n): + G = nx.empty_graph(n, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError, match=".*not applicable to empty graphs"): + nx.flow_hierarchy(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py new file mode 100644 index 0000000000000000000000000000000000000000..6af0016498549caed58772e304c93113a8b693d9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_hybrid.py @@ -0,0 +1,24 @@ +import networkx as nx + + +def test_2d_grid_graph(): + # FC article claims 2d grid graph of size n is (3,3)-connected + # and (5,9)-connected, but I don't think it is (5,9)-connected + G = nx.grid_2d_graph(8, 8, periodic=True) + assert nx.is_kl_connected(G, 3, 3) + assert not nx.is_kl_connected(G, 5, 9) + (H, graphOK) = nx.kl_connected_subgraph(G, 5, 9, same_as_graph=True) + assert not graphOK + + +def test_small_graph(): + G = nx.Graph() + G.add_edge(1, 2) + G.add_edge(1, 3) + G.add_edge(2, 3) + assert nx.is_kl_connected(G, 2, 2) + H = nx.kl_connected_subgraph(G, 2, 2) + (H, graphOK) = nx.kl_connected_subgraph( + G, 2, 2, low_memory=True, same_as_graph=True + ) + assert graphOK diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py new file mode 100644 index 0000000000000000000000000000000000000000..d29b306d2b13c2457905c41218e5c60793b309ba --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_isolate.py @@ -0,0 +1,26 @@ +"""Unit tests for the :mod:`networkx.algorithms.isolates` module.""" + +import networkx as nx + + +def test_is_isolate(): + G = nx.Graph() + G.add_edge(0, 1) + G.add_node(2) + assert not nx.is_isolate(G, 0) + assert not nx.is_isolate(G, 1) + assert nx.is_isolate(G, 2) + + +def test_isolates(): + G = nx.Graph() + G.add_edge(0, 1) + G.add_nodes_from([2, 3]) + assert sorted(nx.isolates(G)) == [2, 3] + + +def test_number_of_isolates(): + G = nx.Graph() + G.add_edge(0, 1) + G.add_nodes_from([2, 3]) + assert nx.number_of_isolates(G) == 2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py new file mode 100644 index 0000000000000000000000000000000000000000..0220d9cd24dc5f1eceb8937e3274a21415529349 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_link_prediction.py @@ -0,0 +1,615 @@ +import math +from functools import partial + +import pytest + +import networkx as nx + + +def _test_func(G, ebunch, expected, predict_func, **kwargs): + result = predict_func(G, ebunch, **kwargs) + exp_dict = {tuple(sorted([u, v])): score for u, v, score in expected} + res_dict = {tuple(sorted([u, v])): score for u, v, score in result} + + assert len(exp_dict) == len(res_dict) + for p in exp_dict: + assert exp_dict[p] == pytest.approx(res_dict[p], abs=1e-7) + + +class TestResourceAllocationIndex: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.resource_allocation_index) + cls.test = staticmethod(partial(_test_func, predict_func=cls.func)) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, [(0, 1)], [(0, 1, 0.75)]) + + def test_P3(self): + G = nx.path_graph(3) + self.test(G, [(0, 2)], [(0, 2, 0.5)]) + + def test_S4(self): + G = nx.star_graph(4) + self.test(G, [(1, 2)], [(1, 2, 0.25)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(4) + self.test(G, [(0, 0)], [(0, 0, 1)]) + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)]) + + +class TestJaccardCoefficient: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.jaccard_coefficient) + cls.test = staticmethod(partial(_test_func, predict_func=cls.func)) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, [(0, 1)], [(0, 1, 0.6)]) + + def test_P4(self): + G = nx.path_graph(4) + self.test(G, [(0, 2)], [(0, 2, 0.5)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (2, 3)]) + self.test(G, [(0, 2)], [(0, 2, 0)]) + + def test_isolated_nodes(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + self.test(G, None, [(0, 3, 0.5), (1, 2, 0.5), (1, 3, 0)]) + + +class TestAdamicAdarIndex: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.adamic_adar_index) + cls.test = staticmethod(partial(_test_func, predict_func=cls.func)) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, [(0, 1)], [(0, 1, 3 / math.log(4))]) + + def test_P3(self): + G = nx.path_graph(3) + self.test(G, [(0, 2)], [(0, 2, 1 / math.log(2))]) + + def test_S4(self): + G = nx.star_graph(4) + self.test(G, [(1, 2)], [(1, 2, 1 / math.log(4))]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + with pytest.raises(nx.NetworkXNotImplemented): + G = graph_type([(0, 1), (1, 2)]) + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(4) + self.test(G, [(0, 0)], [(0, 0, 3 / math.log(3))]) + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + self.test( + G, None, [(0, 3, 1 / math.log(2)), (1, 2, 1 / math.log(2)), (1, 3, 0)] + ) + + +class TestCommonNeighborCentrality: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.common_neighbor_centrality) + cls.test = staticmethod(partial(_test_func, predict_func=cls.func)) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, [(0, 1)], [(0, 1, 3.0)], alpha=1) + self.test(G, [(0, 1)], [(0, 1, 5.0)], alpha=0) + + def test_P3(self): + G = nx.path_graph(3) + self.test(G, [(0, 2)], [(0, 2, 1.25)], alpha=0.5) + + def test_S4(self): + G = nx.star_graph(4) + self.test(G, [(1, 2)], [(1, 2, 1.75)], alpha=0.5) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_u_not_found(self): + G = nx.Graph() + G.add_edges_from([(1, 3), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 1)]) + + def test_node_v_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(4) + with pytest.raises(nx.NetworkXAlgorithmError): + self.test(G, [(0, 0)], []) + + def test_equal_nodes_with_alpha_one_raises_error(self): + G = nx.complete_graph(4) + with pytest.raises(nx.NetworkXAlgorithmError): + self.test(G, [(0, 0)], [], alpha=1.0) + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + self.test(G, None, [(0, 3, 1.5), (1, 2, 1.5), (1, 3, 2 / 3)], alpha=0.5) + + +class TestPreferentialAttachment: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.preferential_attachment) + cls.test = staticmethod(partial(_test_func, predict_func=cls.func)) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, [(0, 1)], [(0, 1, 16)]) + + def test_P3(self): + G = nx.path_graph(3) + self.test(G, [(0, 1)], [(0, 1, 2)]) + + def test_S4(self): + G = nx.star_graph(4) + self.test(G, [(0, 2)], [(0, 2, 4)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_zero_degrees(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + self.test(G, None, [(0, 3, 2), (1, 2, 2), (1, 3, 1)]) + + +class TestCNSoundarajanHopcroft: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.cn_soundarajan_hopcroft) + cls.test = staticmethod( + partial(_test_func, predict_func=cls.func, community="community") + ) + + def test_K5(self): + G = nx.complete_graph(5) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 1 + self.test(G, [(0, 1)], [(0, 1, 5)]) + + def test_P3(self): + G = nx.path_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 2)], [(0, 2, 1)]) + + def test_S4(self): + G = nx.star_graph(4) + G.nodes[0]["community"] = 1 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 1 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 2)], [(1, 2, 2)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + G.add_nodes_from([0, 1, 2], community=0) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 0)], [(0, 0, 4)]) + + def test_different_community(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 1 + self.test(G, [(0, 3)], [(0, 3, 2)]) + + def test_no_community_information(self): + G = nx.complete_graph(5) + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 1)])) + + def test_insufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 3)])) + + def test_sufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]) + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 4)], [(1, 4, 4)]) + + def test_custom_community_attribute_name(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["cmty"] = 0 + G.nodes[1]["cmty"] = 0 + G.nodes[2]["cmty"] = 0 + G.nodes[3]["cmty"] = 1 + self.test(G, [(0, 3)], [(0, 3, 2)], community="cmty") + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + self.test(G, None, [(0, 3, 2), (1, 2, 1), (1, 3, 0)]) + + +class TestRAIndexSoundarajanHopcroft: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.ra_index_soundarajan_hopcroft) + cls.test = staticmethod( + partial(_test_func, predict_func=cls.func, community="community") + ) + + def test_K5(self): + G = nx.complete_graph(5) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 1 + self.test(G, [(0, 1)], [(0, 1, 0.5)]) + + def test_P3(self): + G = nx.path_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 2)], [(0, 2, 0)]) + + def test_S4(self): + G = nx.star_graph(4) + G.nodes[0]["community"] = 1 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 1 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 2)], [(1, 2, 0.25)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + G.add_nodes_from([0, 1, 2], community=0) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 0)], [(0, 0, 1)]) + + def test_different_community(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 1 + self.test(G, [(0, 3)], [(0, 3, 0)]) + + def test_no_community_information(self): + G = nx.complete_graph(5) + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 1)])) + + def test_insufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 3)])) + + def test_sufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]) + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 4)], [(1, 4, 1)]) + + def test_custom_community_attribute_name(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["cmty"] = 0 + G.nodes[1]["cmty"] = 0 + G.nodes[2]["cmty"] = 0 + G.nodes[3]["cmty"] = 1 + self.test(G, [(0, 3)], [(0, 3, 0)], community="cmty") + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + self.test(G, None, [(0, 3, 0.5), (1, 2, 0), (1, 3, 0)]) + + +class TestWithinInterCluster: + @classmethod + def setup_class(cls): + cls.delta = 0.001 + cls.func = staticmethod(nx.within_inter_cluster) + cls.test = staticmethod( + partial( + _test_func, + predict_func=cls.func, + delta=cls.delta, + community="community", + ) + ) + + def test_K5(self): + G = nx.complete_graph(5) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 1 + self.test(G, [(0, 1)], [(0, 1, 2 / (1 + self.delta))]) + + def test_P3(self): + G = nx.path_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 2)], [(0, 2, 0)]) + + def test_S4(self): + G = nx.star_graph(4) + G.nodes[0]["community"] = 1 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 1 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 2)], [(1, 2, 1 / self.delta)]) + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) + def test_notimplemented(self, graph_type): + G = graph_type([(0, 1), (1, 2)]) + G.add_nodes_from([0, 1, 2], community=0) + with pytest.raises(nx.NetworkXNotImplemented): + self.func(G, [(0, 2)]) + + def test_node_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NodeNotFound): + self.func(G, [(0, 4)]) + + def test_no_common_neighbor(self): + G = nx.Graph() + G.add_nodes_from([0, 1]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + self.test(G, [(0, 1)], [(0, 1, 0)]) + + def test_equal_nodes(self): + G = nx.complete_graph(3) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + self.test(G, [(0, 0)], [(0, 0, 2 / self.delta)]) + + def test_different_community(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 1 + self.test(G, [(0, 3)], [(0, 3, 0)]) + + def test_no_inter_cluster_common_neighbor(self): + G = nx.complete_graph(4) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)]) + + def test_no_community_information(self): + G = nx.complete_graph(5) + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 1)])) + + def test_insufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 0 + G.nodes[3]["community"] = 0 + with pytest.raises(nx.NetworkXAlgorithmError): + list(self.func(G, [(0, 3)])) + + def test_sufficient_community_information(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]) + G.nodes[1]["community"] = 0 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + G.nodes[4]["community"] = 0 + self.test(G, [(1, 4)], [(1, 4, 2 / self.delta)]) + + def test_invalid_delta(self): + G = nx.complete_graph(3) + G.add_nodes_from([0, 1, 2], community=0) + with pytest.raises(nx.NetworkXAlgorithmError): + self.func(G, [(0, 1)], 0) + with pytest.raises(nx.NetworkXAlgorithmError): + self.func(G, [(0, 1)], -0.5) + + def test_custom_community_attribute_name(self): + G = nx.complete_graph(4) + G.nodes[0]["cmty"] = 0 + G.nodes[1]["cmty"] = 0 + G.nodes[2]["cmty"] = 0 + G.nodes[3]["cmty"] = 0 + self.test(G, [(0, 3)], [(0, 3, 2 / self.delta)], community="cmty") + + def test_all_nonexistent_edges(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (2, 3)]) + G.nodes[0]["community"] = 0 + G.nodes[1]["community"] = 1 + G.nodes[2]["community"] = 0 + G.nodes[3]["community"] = 0 + self.test(G, None, [(0, 3, 1 / self.delta), (1, 2, 0), (1, 3, 0)]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py new file mode 100644 index 0000000000000000000000000000000000000000..639a31fd5f306e2df432cf03d45154f5ee3ea48d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_lowest_common_ancestors.py @@ -0,0 +1,459 @@ +from itertools import chain, combinations, product + +import pytest + +import networkx as nx + +tree_all_pairs_lca = nx.tree_all_pairs_lowest_common_ancestor +all_pairs_lca = nx.all_pairs_lowest_common_ancestor + + +def get_pair(dictionary, n1, n2): + if (n1, n2) in dictionary: + return dictionary[n1, n2] + else: + return dictionary[n2, n1] + + +class TestTreeLCA: + @classmethod + def setup_class(cls): + cls.DG = nx.DiGraph() + edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)] + cls.DG.add_edges_from(edges) + cls.ans = dict(tree_all_pairs_lca(cls.DG, 0)) + gold = {(n, n): n for n in cls.DG} + gold.update({(0, i): 0 for i in range(1, 7)}) + gold.update( + { + (1, 2): 0, + (1, 3): 1, + (1, 4): 1, + (1, 5): 0, + (1, 6): 0, + (2, 3): 0, + (2, 4): 0, + (2, 5): 2, + (2, 6): 2, + (3, 4): 1, + (3, 5): 0, + (3, 6): 0, + (4, 5): 0, + (4, 6): 0, + (5, 6): 2, + } + ) + + cls.gold = gold + + @staticmethod + def assert_has_same_pairs(d1, d2): + for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)): + assert get_pair(d1, a, b) == get_pair(d2, a, b) + + def test_tree_all_pairs_lca_default_root(self): + assert dict(tree_all_pairs_lca(self.DG)) == self.ans + + def test_tree_all_pairs_lca_return_subset(self): + test_pairs = [(0, 1), (0, 1), (1, 0)] + ans = dict(tree_all_pairs_lca(self.DG, 0, test_pairs)) + assert (0, 1) in ans and (1, 0) in ans + assert len(ans) == 2 + + def test_tree_all_pairs_lca(self): + all_pairs = chain(combinations(self.DG, 2), ((node, node) for node in self.DG)) + + ans = dict(tree_all_pairs_lca(self.DG, 0, all_pairs)) + self.assert_has_same_pairs(ans, self.ans) + + def test_tree_all_pairs_gold_example(self): + ans = dict(tree_all_pairs_lca(self.DG)) + self.assert_has_same_pairs(self.gold, ans) + + def test_tree_all_pairs_lca_invalid_input(self): + empty_digraph = tree_all_pairs_lca(nx.DiGraph()) + pytest.raises(nx.NetworkXPointlessConcept, list, empty_digraph) + + bad_pairs_digraph = tree_all_pairs_lca(self.DG, pairs=[(-1, -2)]) + pytest.raises(nx.NodeNotFound, list, bad_pairs_digraph) + + def test_tree_all_pairs_lca_subtrees(self): + ans = dict(tree_all_pairs_lca(self.DG, 1)) + gold = { + pair: lca + for (pair, lca) in self.gold.items() + if all(n in (1, 3, 4) for n in pair) + } + self.assert_has_same_pairs(gold, ans) + + def test_tree_all_pairs_lca_disconnected_nodes(self): + G = nx.DiGraph() + G.add_node(1) + assert {(1, 1): 1} == dict(tree_all_pairs_lca(G)) + + G.add_node(0) + assert {(1, 1): 1} == dict(tree_all_pairs_lca(G, 1)) + assert {(0, 0): 0} == dict(tree_all_pairs_lca(G, 0)) + + pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G)) + + def test_tree_all_pairs_lca_error_if_input_not_tree(self): + # Cycle + G = nx.DiGraph([(1, 2), (2, 1)]) + pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G)) + # DAG + G = nx.DiGraph([(0, 2), (1, 2)]) + pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G)) + + def test_tree_all_pairs_lca_generator(self): + pairs = iter([(0, 1), (0, 1), (1, 0)]) + some_pairs = dict(tree_all_pairs_lca(self.DG, 0, pairs)) + assert (0, 1) in some_pairs and (1, 0) in some_pairs + assert len(some_pairs) == 2 + + def test_tree_all_pairs_lca_nonexisting_pairs_exception(self): + lca = tree_all_pairs_lca(self.DG, 0, [(-1, -1)]) + pytest.raises(nx.NodeNotFound, list, lca) + # check if node is None + lca = tree_all_pairs_lca(self.DG, None, [(-1, -1)]) + pytest.raises(nx.NodeNotFound, list, lca) + + def test_tree_all_pairs_lca_routine_bails_on_DAGs(self): + G = nx.DiGraph([(3, 4), (5, 4)]) + pytest.raises(nx.NetworkXError, list, tree_all_pairs_lca(G)) + + def test_tree_all_pairs_lca_not_implemented(self): + NNI = nx.NetworkXNotImplemented + G = nx.Graph([(0, 1)]) + with pytest.raises(NNI): + next(tree_all_pairs_lca(G)) + with pytest.raises(NNI): + next(all_pairs_lca(G)) + pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1) + G = nx.MultiGraph([(0, 1)]) + with pytest.raises(NNI): + next(tree_all_pairs_lca(G)) + with pytest.raises(NNI): + next(all_pairs_lca(G)) + pytest.raises(NNI, nx.lowest_common_ancestor, G, 0, 1) + + def test_tree_all_pairs_lca_trees_without_LCAs(self): + G = nx.DiGraph() + G.add_node(3) + ans = list(tree_all_pairs_lca(G)) + assert ans == [((3, 3), 3)] + + +class TestMultiTreeLCA(TestTreeLCA): + @classmethod + def setup_class(cls): + cls.DG = nx.MultiDiGraph() + edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)] + cls.DG.add_edges_from(edges) + cls.ans = dict(tree_all_pairs_lca(cls.DG, 0)) + # add multiedges + cls.DG.add_edges_from(edges) + + gold = {(n, n): n for n in cls.DG} + gold.update({(0, i): 0 for i in range(1, 7)}) + gold.update( + { + (1, 2): 0, + (1, 3): 1, + (1, 4): 1, + (1, 5): 0, + (1, 6): 0, + (2, 3): 0, + (2, 4): 0, + (2, 5): 2, + (2, 6): 2, + (3, 4): 1, + (3, 5): 0, + (3, 6): 0, + (4, 5): 0, + (4, 6): 0, + (5, 6): 2, + } + ) + + cls.gold = gold + + +class TestDAGLCA: + @classmethod + def setup_class(cls): + cls.DG = nx.DiGraph() + nx.add_path(cls.DG, (0, 1, 2, 3)) + nx.add_path(cls.DG, (0, 4, 3)) + nx.add_path(cls.DG, (0, 5, 6, 8, 3)) + nx.add_path(cls.DG, (5, 7, 8)) + cls.DG.add_edge(6, 2) + cls.DG.add_edge(7, 2) + + cls.root_distance = nx.shortest_path_length(cls.DG, source=0) + + cls.gold = { + (1, 1): 1, + (1, 2): 1, + (1, 3): 1, + (1, 4): 0, + (1, 5): 0, + (1, 6): 0, + (1, 7): 0, + (1, 8): 0, + (2, 2): 2, + (2, 3): 2, + (2, 4): 0, + (2, 5): 5, + (2, 6): 6, + (2, 7): 7, + (2, 8): 7, + (3, 3): 3, + (3, 4): 4, + (3, 5): 5, + (3, 6): 6, + (3, 7): 7, + (3, 8): 8, + (4, 4): 4, + (4, 5): 0, + (4, 6): 0, + (4, 7): 0, + (4, 8): 0, + (5, 5): 5, + (5, 6): 5, + (5, 7): 5, + (5, 8): 5, + (6, 6): 6, + (6, 7): 5, + (6, 8): 6, + (7, 7): 7, + (7, 8): 7, + (8, 8): 8, + } + cls.gold.update(((0, n), 0) for n in cls.DG) + + def assert_lca_dicts_same(self, d1, d2, G=None): + """Checks if d1 and d2 contain the same pairs and + have a node at the same distance from root for each. + If G is None use self.DG.""" + if G is None: + G = self.DG + root_distance = self.root_distance + else: + roots = [n for n, deg in G.in_degree if deg == 0] + assert len(roots) == 1 + root_distance = nx.shortest_path_length(G, source=roots[0]) + + for a, b in ((min(pair), max(pair)) for pair in chain(d1, d2)): + assert ( + root_distance[get_pair(d1, a, b)] == root_distance[get_pair(d2, a, b)] + ) + + def test_all_pairs_lca_gold_example(self): + self.assert_lca_dicts_same(dict(all_pairs_lca(self.DG)), self.gold) + + def test_all_pairs_lca_all_pairs_given(self): + all_pairs = list(product(self.DG.nodes(), self.DG.nodes())) + ans = all_pairs_lca(self.DG, pairs=all_pairs) + self.assert_lca_dicts_same(dict(ans), self.gold) + + def test_all_pairs_lca_generator(self): + all_pairs = product(self.DG.nodes(), self.DG.nodes()) + ans = all_pairs_lca(self.DG, pairs=all_pairs) + self.assert_lca_dicts_same(dict(ans), self.gold) + + def test_all_pairs_lca_input_graph_with_two_roots(self): + G = self.DG.copy() + G.add_edge(9, 10) + G.add_edge(9, 4) + gold = self.gold.copy() + gold[9, 9] = 9 + gold[9, 10] = 9 + gold[9, 4] = 9 + gold[9, 3] = 9 + gold[10, 4] = 9 + gold[10, 3] = 9 + gold[10, 10] = 10 + + testing = dict(all_pairs_lca(G)) + + G.add_edge(-1, 9) + G.add_edge(-1, 0) + self.assert_lca_dicts_same(testing, gold, G) + + def test_all_pairs_lca_nonexisting_pairs_exception(self): + pytest.raises(nx.NodeNotFound, all_pairs_lca, self.DG, [(-1, -1)]) + + def test_all_pairs_lca_pairs_without_lca(self): + G = self.DG.copy() + G.add_node(-1) + gen = all_pairs_lca(G, [(-1, -1), (-1, 0)]) + assert dict(gen) == {(-1, -1): -1} + + def test_all_pairs_lca_null_graph(self): + pytest.raises(nx.NetworkXPointlessConcept, all_pairs_lca, nx.DiGraph()) + + def test_all_pairs_lca_non_dags(self): + pytest.raises(nx.NetworkXError, all_pairs_lca, nx.DiGraph([(3, 4), (4, 3)])) + + def test_all_pairs_lca_nonempty_graph_without_lca(self): + G = nx.DiGraph() + G.add_node(3) + ans = list(all_pairs_lca(G)) + assert ans == [((3, 3), 3)] + + def test_all_pairs_lca_bug_gh4942(self): + G = nx.DiGraph([(0, 2), (1, 2), (2, 3)]) + ans = list(all_pairs_lca(G)) + assert len(ans) == 9 + + def test_all_pairs_lca_default_kwarg(self): + G = nx.DiGraph([(0, 1), (2, 1)]) + sentinel = object() + assert nx.lowest_common_ancestor(G, 0, 2, default=sentinel) is sentinel + + def test_all_pairs_lca_identity(self): + G = nx.DiGraph() + G.add_node(3) + assert nx.lowest_common_ancestor(G, 3, 3) == 3 + + def test_all_pairs_lca_issue_4574(self): + G = nx.DiGraph() + G.add_nodes_from(range(17)) + G.add_edges_from( + [ + (2, 0), + (1, 2), + (3, 2), + (5, 2), + (8, 2), + (11, 2), + (4, 5), + (6, 5), + (7, 8), + (10, 8), + (13, 11), + (14, 11), + (15, 11), + (9, 10), + (12, 13), + (16, 15), + ] + ) + + assert nx.lowest_common_ancestor(G, 7, 9) is None + + def test_all_pairs_lca_one_pair_gh4942(self): + G = nx.DiGraph() + # Note: order edge addition is critical to the test + G.add_edge(0, 1) + G.add_edge(2, 0) + G.add_edge(2, 3) + G.add_edge(4, 0) + G.add_edge(5, 2) + + assert nx.lowest_common_ancestor(G, 1, 3) == 2 + + +class TestMultiDiGraph_DAGLCA(TestDAGLCA): + @classmethod + def setup_class(cls): + cls.DG = nx.MultiDiGraph() + nx.add_path(cls.DG, (0, 1, 2, 3)) + # add multiedges + nx.add_path(cls.DG, (0, 1, 2, 3)) + nx.add_path(cls.DG, (0, 4, 3)) + nx.add_path(cls.DG, (0, 5, 6, 8, 3)) + nx.add_path(cls.DG, (5, 7, 8)) + cls.DG.add_edge(6, 2) + cls.DG.add_edge(7, 2) + + cls.root_distance = nx.shortest_path_length(cls.DG, source=0) + + cls.gold = { + (1, 1): 1, + (1, 2): 1, + (1, 3): 1, + (1, 4): 0, + (1, 5): 0, + (1, 6): 0, + (1, 7): 0, + (1, 8): 0, + (2, 2): 2, + (2, 3): 2, + (2, 4): 0, + (2, 5): 5, + (2, 6): 6, + (2, 7): 7, + (2, 8): 7, + (3, 3): 3, + (3, 4): 4, + (3, 5): 5, + (3, 6): 6, + (3, 7): 7, + (3, 8): 8, + (4, 4): 4, + (4, 5): 0, + (4, 6): 0, + (4, 7): 0, + (4, 8): 0, + (5, 5): 5, + (5, 6): 5, + (5, 7): 5, + (5, 8): 5, + (6, 6): 6, + (6, 7): 5, + (6, 8): 6, + (7, 7): 7, + (7, 8): 7, + (8, 8): 8, + } + cls.gold.update(((0, n), 0) for n in cls.DG) + + +def test_all_pairs_lca_self_ancestors(): + """Self-ancestors should always be the node itself, i.e. lca of (0, 0) is 0. + See gh-4458.""" + # DAG for test - note order of node/edge addition is relevant + G = nx.DiGraph() + G.add_nodes_from(range(5)) + G.add_edges_from([(1, 0), (2, 0), (3, 2), (4, 1), (4, 3)]) + + ap_lca = nx.all_pairs_lowest_common_ancestor + assert all(u == v == a for (u, v), a in ap_lca(G) if u == v) + MG = nx.MultiDiGraph(G) + assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v) + MG.add_edges_from([(1, 0), (2, 0)]) + assert all(u == v == a for (u, v), a in ap_lca(MG) if u == v) + + +def test_lca_on_null_graph(): + G = nx.null_graph(create_using=nx.DiGraph) + with pytest.raises( + nx.NetworkXPointlessConcept, match="LCA meaningless on null graphs" + ): + nx.lowest_common_ancestor(G, 0, 0) + + +def test_lca_on_cycle_graph(): + G = nx.cycle_graph(6, create_using=nx.DiGraph) + with pytest.raises( + nx.NetworkXError, match="LCA only defined on directed acyclic graphs" + ): + nx.lowest_common_ancestor(G, 0, 3) + + +def test_lca_multiple_valid_solutions(): + G = nx.DiGraph() + G.add_nodes_from(range(4)) + G.add_edges_from([(2, 0), (3, 0), (2, 1), (3, 1)]) + assert nx.lowest_common_ancestor(G, 0, 1) in {2, 3} + + +def test_lca_dont_rely_on_single_successor(): + # Nodes 0 and 1 have nodes 2 and 3 as immediate ancestors, + # and node 2 also has node 3 as an immediate ancestor. + G = nx.DiGraph() + G.add_nodes_from(range(4)) + G.add_edges_from([(2, 0), (2, 1), (3, 1), (3, 0), (3, 2)]) + assert nx.lowest_common_ancestor(G, 0, 1) == 2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py new file mode 100644 index 0000000000000000000000000000000000000000..5c98900669edf2cd1edbe37471b2cb1b56ae466e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_matching.py @@ -0,0 +1,556 @@ +import math +from itertools import permutations + +import pytest + +import networkx as nx +from networkx.utils import edges_equal + + +@pytest.mark.parametrize( + "fn", (nx.is_matching, nx.is_maximal_matching, nx.is_perfect_matching) +) +@pytest.mark.parametrize( + "edgeset", + ( + {(0, 5)}, # Single edge, node not in G + {(5, 0)}, # for both edge orders + {(0, 5), (2, 3)}, # node not in G, but other edge is valid matching + {(5, 5), (2, 3)}, # Self-loop hits node not in G validation first + ), +) +def test_is_matching_node_not_in_G(fn, edgeset): + """All is_*matching functions have consistent exception message for node + not in G.""" + G = nx.path_graph(4) + with pytest.raises(nx.NetworkXError, match="matching.*with node not in G"): + fn(G, edgeset) + + +@pytest.mark.parametrize( + "fn", (nx.is_matching, nx.is_maximal_matching, nx.is_perfect_matching) +) +@pytest.mark.parametrize( + "edgeset", + ( + {(0, 1, 2), (2, 3)}, # 3-tuple + {(0,), (2, 3)}, # 1-tuple + ), +) +def test_is_matching_invalid_edge(fn, edgeset): + """All is_*matching functions have consistent exception message for invalid + edges in matching.""" + G = nx.path_graph(4) + with pytest.raises(nx.NetworkXError, match=".*non-2-tuple edge.*"): + fn(G, edgeset) + + +@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.DiGraph, nx.MultiDiGraph)) +@pytest.mark.parametrize( + "fn", (nx.max_weight_matching, nx.min_weight_matching, nx.maximal_matching) +) +def test_wrong_graph_type(fn, graph_type): + G = graph_type() + with pytest.raises(nx.NetworkXNotImplemented): + fn(G) + + +class TestMaxWeightMatching: + """Unit tests for the + :func:`~networkx.algorithms.matching.max_weight_matching` function. + + """ + + def test_trivial1(self): + """Empty graph""" + G = nx.Graph() + assert nx.max_weight_matching(G) == set() + assert nx.min_weight_matching(G) == set() + + def test_selfloop(self): + G = nx.Graph() + G.add_edge(0, 0, weight=100) + assert nx.max_weight_matching(G) == set() + assert nx.min_weight_matching(G) == set() + + def test_single_edge(self): + G = nx.Graph() + G.add_edge(0, 1) + assert edges_equal(nx.max_weight_matching(G), {(0, 1)}) + assert edges_equal(nx.min_weight_matching(G), {(0, 1)}) + + def test_two_path(self): + G = nx.Graph() + G.add_edge("one", "two", weight=10) + G.add_edge("two", "three", weight=11) + assert edges_equal(nx.max_weight_matching(G), {("two", "three")}) + assert edges_equal(nx.min_weight_matching(G), {("one", "two")}) + + def test_path(self): + G = nx.Graph() + G.add_edge(1, 2, weight=5) + G.add_edge(2, 3, weight=11) + G.add_edge(3, 4, weight=5) + assert edges_equal(nx.max_weight_matching(G), {(2, 3)}) + assert edges_equal(nx.max_weight_matching(G, weight=None), {(1, 2), (3, 4)}) + assert edges_equal(nx.min_weight_matching(G), {(1, 2), (3, 4)}) + assert edges_equal(nx.min_weight_matching(G, weight=None), {(1, 2), (3, 4)}) + + def test_square(self): + G = nx.Graph() + G.add_edge(1, 4, weight=2) + G.add_edge(2, 3, weight=2) + G.add_edge(1, 2, weight=1) + G.add_edge(3, 4, weight=4) + assert edges_equal(nx.max_weight_matching(G), {(1, 2), (3, 4)}) + assert edges_equal(nx.min_weight_matching(G), {(1, 4), (2, 3)}) + + def test_edge_attribute_name(self): + G = nx.Graph() + G.add_edge("one", "two", weight=10, abcd=11) + G.add_edge("two", "three", weight=11, abcd=10) + assert edges_equal(nx.max_weight_matching(G, weight="abcd"), {("one", "two")}) + assert edges_equal(nx.min_weight_matching(G, weight="abcd"), {("two", "three")}) + + def test_floating_point_weights(self): + G = nx.Graph() + G.add_edge(1, 2, weight=math.pi) + G.add_edge(2, 3, weight=math.exp(1)) + G.add_edge(1, 3, weight=3.0) + G.add_edge(1, 4, weight=math.sqrt(2.0)) + assert edges_equal(nx.max_weight_matching(G), {(1, 4), (2, 3)}) + assert edges_equal(nx.min_weight_matching(G), {(1, 4), (2, 3)}) + + def test_negative_weights(self): + G = nx.Graph() + G.add_edge(1, 2, weight=2) + G.add_edge(1, 3, weight=-2) + G.add_edge(2, 3, weight=1) + G.add_edge(2, 4, weight=-1) + G.add_edge(3, 4, weight=-6) + assert edges_equal(nx.max_weight_matching(G), {(1, 2)}) + assert edges_equal( + nx.max_weight_matching(G, maxcardinality=True), {(1, 3), (2, 4)} + ) + assert edges_equal(nx.min_weight_matching(G), {(1, 2), (3, 4)}) + + def test_s_blossom(self): + """Create S-blossom and use it for augmentation:""" + G = nx.Graph() + G.add_weighted_edges_from([(1, 2, 8), (1, 3, 9), (2, 3, 10), (3, 4, 7)]) + answer = {(1, 2), (3, 4)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + G.add_weighted_edges_from([(1, 6, 5), (4, 5, 6)]) + answer = {(1, 6), (2, 3), (4, 5)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_s_t_blossom(self): + """Create S-blossom, relabel as T-blossom, use for augmentation:""" + G = nx.Graph() + G.add_weighted_edges_from( + [(1, 2, 9), (1, 3, 8), (2, 3, 10), (1, 4, 5), (4, 5, 4), (1, 6, 3)] + ) + answer = {(1, 6), (2, 3), (4, 5)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + G.add_edge(4, 5, weight=3) + G.add_edge(1, 6, weight=4) + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + G.remove_edge(1, 6) + G.add_edge(3, 6, weight=4) + answer = {(1, 2), (3, 6), (4, 5)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nested_s_blossom(self): + """Create nested S-blossom, use for augmentation:""" + + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 9), + (1, 3, 9), + (2, 3, 10), + (2, 4, 8), + (3, 5, 8), + (4, 5, 10), + (5, 6, 6), + ] + ) + expected_edgeset = {(1, 3), (2, 4), (5, 6)} + expected = {frozenset(e) for e in expected_edgeset} + answer = {frozenset(e) for e in nx.max_weight_matching(G)} + assert answer == expected + answer = {frozenset(e) for e in nx.min_weight_matching(G)} + assert answer == expected + + def test_nested_s_blossom_relabel(self): + """Create S-blossom, relabel as S, include in nested S-blossom:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 10), + (1, 7, 10), + (2, 3, 12), + (3, 4, 20), + (3, 5, 20), + (4, 5, 25), + (5, 6, 10), + (6, 7, 10), + (7, 8, 8), + ] + ) + answer = {(1, 2), (3, 4), (5, 6), (7, 8)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nested_s_blossom_expand(self): + """Create nested S-blossom, augment, expand recursively:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 8), + (1, 3, 8), + (2, 3, 10), + (2, 4, 12), + (3, 5, 12), + (4, 5, 14), + (4, 6, 12), + (5, 7, 12), + (6, 7, 14), + (7, 8, 12), + ] + ) + answer = {(1, 2), (3, 5), (4, 6), (7, 8)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_s_blossom_relabel_expand(self): + """Create S-blossom, relabel as T, expand:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 23), + (1, 5, 22), + (1, 6, 15), + (2, 3, 25), + (3, 4, 22), + (4, 5, 25), + (4, 8, 14), + (5, 7, 13), + ] + ) + answer = {(1, 6), (2, 3), (4, 8), (5, 7)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nested_s_blossom_relabel_expand(self): + """Create nested S-blossom, relabel as T, expand:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 19), + (1, 3, 20), + (1, 8, 8), + (2, 3, 25), + (2, 4, 18), + (3, 5, 18), + (4, 5, 13), + (4, 7, 7), + (5, 6, 7), + ] + ) + answer = {(1, 8), (2, 3), (4, 7), (5, 6)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nasty_blossom1(self): + """Create blossom, relabel as T in more than one way, expand, + augment: + """ + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 45), + (1, 5, 45), + (2, 3, 50), + (3, 4, 45), + (4, 5, 50), + (1, 6, 30), + (3, 9, 35), + (4, 8, 35), + (5, 7, 26), + (9, 10, 5), + ] + ) + answer = {(1, 6), (2, 3), (4, 8), (5, 7), (9, 10)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nasty_blossom2(self): + """Again but slightly different:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 45), + (1, 5, 45), + (2, 3, 50), + (3, 4, 45), + (4, 5, 50), + (1, 6, 30), + (3, 9, 35), + (4, 8, 26), + (5, 7, 40), + (9, 10, 5), + ] + ) + answer = {(1, 6), (2, 3), (4, 8), (5, 7), (9, 10)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nasty_blossom_least_slack(self): + """Create blossom, relabel as T, expand such that a new + least-slack S-to-free dge is produced, augment: + """ + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 45), + (1, 5, 45), + (2, 3, 50), + (3, 4, 45), + (4, 5, 50), + (1, 6, 30), + (3, 9, 35), + (4, 8, 28), + (5, 7, 26), + (9, 10, 5), + ] + ) + answer = {(1, 6), (2, 3), (4, 8), (5, 7), (9, 10)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nasty_blossom_augmenting(self): + """Create nested blossom, relabel as T in more than one way""" + # expand outer blossom such that inner blossom ends up on an + # augmenting path: + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 45), + (1, 7, 45), + (2, 3, 50), + (3, 4, 45), + (4, 5, 95), + (4, 6, 94), + (5, 6, 94), + (6, 7, 50), + (1, 8, 30), + (3, 11, 35), + (5, 9, 36), + (7, 10, 26), + (11, 12, 5), + ] + ) + answer = {(1, 8), (2, 3), (4, 6), (5, 9), (7, 10), (11, 12)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_nasty_blossom_expand_recursively(self): + """Create nested S-blossom, relabel as S, expand recursively:""" + G = nx.Graph() + G.add_weighted_edges_from( + [ + (1, 2, 40), + (1, 3, 40), + (2, 3, 60), + (2, 4, 55), + (3, 5, 55), + (4, 5, 50), + (1, 8, 15), + (5, 7, 30), + (7, 6, 10), + (8, 10, 10), + (4, 9, 30), + ] + ) + answer = {(1, 2), (3, 5), (4, 9), (6, 7), (8, 10)} + assert edges_equal(nx.max_weight_matching(G), answer) + assert edges_equal(nx.min_weight_matching(G), answer) + + def test_min_weight_matching_max_cardinality(self): + G = nx.Graph() + G.add_weighted_edges_from([(1, 2, 1000), (2, 3, 2), (3, 4, 3000)]) + # The minimum-weight maximal matching is {(2, 3)}; the minimum-weight + # maximum-cardinality matching is {(1, 2), (3, 4)}. See gh-8062. + answer = {(1, 2), (3, 4)} + assert edges_equal(nx.min_weight_matching(G), answer) + + +class TestIsMatching: + """Unit tests for the + :func:`~networkx.algorithms.matching.is_matching` function. + + """ + + def test_dict(self): + G = nx.path_graph(4) + assert nx.is_matching(G, {0: 1, 1: 0, 2: 3, 3: 2}) + + def test_empty_matching(self): + G = nx.path_graph(4) + assert nx.is_matching(G, set()) + + def test_single_edge(self): + G = nx.path_graph(4) + assert nx.is_matching(G, {(1, 2)}) + + def test_edge_order(self): + G = nx.path_graph(4) + assert nx.is_matching(G, {(0, 1), (2, 3)}) + assert nx.is_matching(G, {(1, 0), (2, 3)}) + assert nx.is_matching(G, {(0, 1), (3, 2)}) + assert nx.is_matching(G, {(1, 0), (3, 2)}) + + def test_valid_matching(self): + G = nx.path_graph(4) + assert nx.is_matching(G, {(0, 1), (2, 3)}) + + def test_selfloops(self): + G = nx.path_graph(4) + # selfloop edge not in G + assert not nx.is_matching(G, {(0, 0), (1, 2), (2, 3)}) + # selfloop edge in G + G.add_edge(0, 0) + assert not nx.is_matching(G, {(0, 0), (1, 2)}) + + def test_invalid_matching(self): + G = nx.path_graph(4) + assert not nx.is_matching(G, {(0, 1), (1, 2), (2, 3)}) + + def test_invalid_edge(self): + G = nx.path_graph(4) + assert not nx.is_matching(G, {(0, 3), (1, 2)}) + + G = nx.DiGraph(G.edges) + assert nx.is_matching(G, {(0, 1)}) + assert not nx.is_matching(G, {(1, 0)}) + + +class TestIsMaximalMatching: + """Unit tests for the + :func:`~networkx.algorithms.matching.is_maximal_matching` function. + + """ + + def test_dict(self): + G = nx.path_graph(4) + assert nx.is_maximal_matching(G, {0: 1, 1: 0, 2: 3, 3: 2}) + + def test_valid(self): + G = nx.path_graph(4) + assert nx.is_maximal_matching(G, {(0, 1), (2, 3)}) + + def test_not_matching(self): + G = nx.path_graph(4) + assert not nx.is_maximal_matching(G, {(0, 1), (1, 2), (2, 3)}) + assert not nx.is_maximal_matching(G, {(0, 3)}) + G.add_edge(0, 0) + assert not nx.is_maximal_matching(G, {(0, 0)}) + + def test_not_maximal(self): + G = nx.path_graph(4) + assert not nx.is_maximal_matching(G, {(0, 1)}) + + +class TestIsPerfectMatching: + """Unit tests for the + :func:`~networkx.algorithms.matching.is_perfect_matching` function. + + """ + + def test_dict(self): + G = nx.path_graph(4) + assert nx.is_perfect_matching(G, {0: 1, 1: 0, 2: 3, 3: 2}) + + def test_valid(self): + G = nx.path_graph(4) + assert nx.is_perfect_matching(G, {(0, 1), (2, 3)}) + + def test_valid_not_path(self): + G = nx.cycle_graph(4) + G.add_edge(0, 4) + G.add_edge(1, 4) + G.add_edge(5, 2) + + assert nx.is_perfect_matching(G, {(1, 4), (0, 3), (5, 2)}) + + def test_selfloops(self): + G = nx.path_graph(4) + # selfloop edge not in G + assert not nx.is_perfect_matching(G, {(0, 0), (1, 2), (2, 3)}) + # selfloop edge in G + G.add_edge(0, 0) + assert not nx.is_perfect_matching(G, {(0, 0), (1, 2)}) + + def test_not_matching(self): + G = nx.path_graph(4) + assert not nx.is_perfect_matching(G, {(0, 3)}) + assert not nx.is_perfect_matching(G, {(0, 1), (1, 2), (2, 3)}) + + def test_maximal_but_not_perfect(self): + G = nx.cycle_graph(4) + G.add_edge(0, 4) + G.add_edge(1, 4) + + assert not nx.is_perfect_matching(G, {(1, 4), (0, 3)}) + + +class TestMaximalMatching: + """Unit tests for the + :func:`~networkx.algorithms.matching.maximal_matching`. + + """ + + def test_valid_matching(self): + edges = [(1, 2), (1, 5), (2, 3), (2, 5), (3, 4), (3, 6), (5, 6)] + G = nx.Graph(edges) + matching = nx.maximal_matching(G) + assert nx.is_maximal_matching(G, matching) + + def test_single_edge_matching(self): + # In the star graph, any maximal matching has just one edge. + G = nx.star_graph(5) + matching = nx.maximal_matching(G) + assert 1 == len(matching) + assert nx.is_maximal_matching(G, matching) + + def test_self_loops(self): + # Create the path graph with two self-loops. + G = nx.path_graph(3) + G.add_edges_from([(0, 0), (1, 1)]) + matching = nx.maximal_matching(G) + assert len(matching) == 1 + # The matching should never include self-loops. + assert not any(u == v for u, v in matching) + assert nx.is_maximal_matching(G, matching) + + def test_ordering(self): + """Tests that a maximal matching is computed correctly + regardless of the order in which nodes are added to the graph. + + """ + for nodes in permutations(range(3)): + G = nx.Graph() + G.add_nodes_from(nodes) + G.add_edges_from([(0, 1), (0, 2)]) + matching = nx.maximal_matching(G) + assert len(matching) == 1 + assert nx.is_maximal_matching(G, matching) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py new file mode 100644 index 0000000000000000000000000000000000000000..6cd8584ebd5c2ab04741234018a976472a92ef91 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_max_weight_clique.py @@ -0,0 +1,179 @@ +"""Maximum weight clique test suite.""" + +import pytest + +import networkx as nx + + +class TestMaximumWeightClique: + def test_basic_cases(self): + def check_basic_case(graph_func, expected_weight, weight_accessor): + graph = graph_func() + clique, weight = nx.algorithms.max_weight_clique(graph, weight_accessor) + assert verify_clique( + graph, clique, weight, expected_weight, weight_accessor + ) + + for graph_func, (expected_weight, expected_size) in TEST_CASES.items(): + check_basic_case(graph_func, expected_weight, "weight") + check_basic_case(graph_func, expected_size, None) + + def test_key_error(self): + graph = two_node_graph() + with pytest.raises(KeyError): + nx.algorithms.max_weight_clique(graph, "nonexistent-key") + + def test_error_on_non_integer_weight(self): + graph = two_node_graph() + graph.nodes[2]["weight"] = 1.5 + with pytest.raises(ValueError): + nx.algorithms.max_weight_clique(graph) + + def test_unaffected_by_self_loops(self): + graph = two_node_graph() + graph.add_edge(1, 1) + graph.add_edge(2, 2) + clique, weight = nx.algorithms.max_weight_clique(graph, "weight") + assert verify_clique(graph, clique, weight, 30, "weight") + graph = three_node_independent_set() + graph.add_edge(1, 1) + clique, weight = nx.algorithms.max_weight_clique(graph, "weight") + assert verify_clique(graph, clique, weight, 20, "weight") + + def test_30_node_prob(self): + G = nx.Graph() + G.add_nodes_from(range(1, 31)) + for i in range(1, 31): + G.nodes[i]["weight"] = i + 1 + # fmt: off + G.add_edges_from( + [ + (1, 12), (1, 13), (1, 15), (1, 16), (1, 18), (1, 19), (1, 20), + (1, 23), (1, 26), (1, 28), (1, 29), (1, 30), (2, 3), (2, 4), + (2, 5), (2, 8), (2, 9), (2, 10), (2, 14), (2, 17), (2, 18), + (2, 21), (2, 22), (2, 23), (2, 27), (3, 9), (3, 15), (3, 21), + (3, 22), (3, 23), (3, 24), (3, 27), (3, 28), (3, 29), (4, 5), + (4, 6), (4, 8), (4, 21), (4, 22), (4, 23), (4, 26), (4, 28), + (4, 30), (5, 6), (5, 8), (5, 9), (5, 13), (5, 14), (5, 15), + (5, 16), (5, 20), (5, 21), (5, 22), (5, 25), (5, 28), (5, 29), + (6, 7), (6, 8), (6, 13), (6, 17), (6, 18), (6, 19), (6, 24), + (6, 26), (6, 27), (6, 28), (6, 29), (7, 12), (7, 14), (7, 15), + (7, 16), (7, 17), (7, 20), (7, 25), (7, 27), (7, 29), (7, 30), + (8, 10), (8, 15), (8, 16), (8, 18), (8, 20), (8, 22), (8, 24), + (8, 26), (8, 27), (8, 28), (8, 30), (9, 11), (9, 12), (9, 13), + (9, 14), (9, 15), (9, 16), (9, 19), (9, 20), (9, 21), (9, 24), + (9, 30), (10, 12), (10, 15), (10, 18), (10, 19), (10, 20), + (10, 22), (10, 23), (10, 24), (10, 26), (10, 27), (10, 29), + (10, 30), (11, 13), (11, 15), (11, 16), (11, 17), (11, 18), + (11, 19), (11, 20), (11, 22), (11, 29), (11, 30), (12, 14), + (12, 17), (12, 18), (12, 19), (12, 20), (12, 21), (12, 23), + (12, 25), (12, 26), (12, 30), (13, 20), (13, 22), (13, 23), + (13, 24), (13, 30), (14, 16), (14, 20), (14, 21), (14, 22), + (14, 23), (14, 25), (14, 26), (14, 27), (14, 29), (14, 30), + (15, 17), (15, 18), (15, 20), (15, 21), (15, 26), (15, 27), + (15, 28), (16, 17), (16, 18), (16, 19), (16, 20), (16, 21), + (16, 29), (16, 30), (17, 18), (17, 21), (17, 22), (17, 25), + (17, 27), (17, 28), (17, 30), (18, 19), (18, 20), (18, 21), + (18, 22), (18, 23), (18, 24), (19, 20), (19, 22), (19, 23), + (19, 24), (19, 25), (19, 27), (19, 30), (20, 21), (20, 23), + (20, 24), (20, 26), (20, 28), (20, 29), (21, 23), (21, 26), + (21, 27), (21, 29), (22, 24), (22, 25), (22, 26), (22, 29), + (23, 25), (23, 30), (24, 25), (24, 26), (25, 27), (25, 29), + (26, 27), (26, 28), (26, 30), (28, 29), (29, 30), + ] + ) + # fmt: on + clique, weight = nx.algorithms.max_weight_clique(G) + assert verify_clique(G, clique, weight, 111, "weight") + + +# ############################ Utility functions ############################ +def verify_clique( + graph, clique, reported_clique_weight, expected_clique_weight, weight_accessor +): + for node1 in clique: + for node2 in clique: + if node1 == node2: + continue + if not graph.has_edge(node1, node2): + return False + + if weight_accessor is None: + clique_weight = len(clique) + else: + clique_weight = sum(graph.nodes[v]["weight"] for v in clique) + + if clique_weight != expected_clique_weight: + return False + if clique_weight != reported_clique_weight: + return False + + return True + + +# ############################ Graph Generation ############################ + + +def empty_graph(): + return nx.Graph() + + +def one_node_graph(): + graph = nx.Graph() + graph.add_nodes_from([1]) + graph.nodes[1]["weight"] = 10 + return graph + + +def two_node_graph(): + graph = nx.Graph() + graph.add_nodes_from([1, 2]) + graph.add_edges_from([(1, 2)]) + graph.nodes[1]["weight"] = 10 + graph.nodes[2]["weight"] = 20 + return graph + + +def three_node_clique(): + graph = nx.Graph() + graph.add_nodes_from([1, 2, 3]) + graph.add_edges_from([(1, 2), (1, 3), (2, 3)]) + graph.nodes[1]["weight"] = 10 + graph.nodes[2]["weight"] = 20 + graph.nodes[3]["weight"] = 5 + return graph + + +def three_node_independent_set(): + graph = nx.Graph() + graph.add_nodes_from([1, 2, 3]) + graph.nodes[1]["weight"] = 10 + graph.nodes[2]["weight"] = 20 + graph.nodes[3]["weight"] = 5 + return graph + + +def disconnected(): + graph = nx.Graph() + graph.add_edges_from([(1, 2), (2, 3), (4, 5), (5, 6)]) + graph.nodes[1]["weight"] = 10 + graph.nodes[2]["weight"] = 20 + graph.nodes[3]["weight"] = 5 + graph.nodes[4]["weight"] = 100 + graph.nodes[5]["weight"] = 200 + graph.nodes[6]["weight"] = 50 + return graph + + +# -------------------------------------------------------------------------- +# Basic tests for all strategies +# For each basic graph function, specify expected weight of max weight clique +# and expected size of maximum clique +TEST_CASES = { + empty_graph: (0, 0), + one_node_graph: (10, 1), + two_node_graph: (30, 2), + three_node_clique: (35, 3), + three_node_independent_set: (20, 1), + disconnected: (300, 2), +} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py new file mode 100644 index 0000000000000000000000000000000000000000..02be02d4c33f233d27d2838e5e3d361c4212c40b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_mis.py @@ -0,0 +1,62 @@ +""" +Tests for maximal (not maximum) independent sets. + +""" + +import random + +import pytest + +import networkx as nx + + +def test_random_seed(): + G = nx.empty_graph(5) + assert nx.maximal_independent_set(G, seed=1) == [1, 0, 3, 2, 4] + + +@pytest.mark.parametrize("graph", [nx.complete_graph(5), nx.complete_graph(55)]) +def test_K5(graph): + """Maximal independent set for complete graphs""" + assert all(nx.maximal_independent_set(graph, [n]) == [n] for n in graph) + + +def test_exceptions(): + """Bad input should raise exception.""" + G = nx.florentine_families_graph() + pytest.raises(nx.NetworkXUnfeasible, nx.maximal_independent_set, G, ["Smith"]) + pytest.raises( + nx.NetworkXUnfeasible, nx.maximal_independent_set, G, ["Salviati", "Pazzi"] + ) + # MaximalIndependentSet is not implemented for directed graphs + pytest.raises(nx.NetworkXNotImplemented, nx.maximal_independent_set, nx.DiGraph(G)) + + +def test_florentine_family(): + G = nx.florentine_families_graph() + indep = nx.maximal_independent_set(G, ["Medici", "Bischeri"]) + assert set(indep) == { + "Medici", + "Bischeri", + "Castellani", + "Pazzi", + "Ginori", + "Lamberteschi", + } + + +def test_bipartite(): + G = nx.complete_bipartite_graph(12, 34) + indep = nx.maximal_independent_set(G, [4, 5, 9, 10]) + assert sorted(indep) == list(range(12)) + + +def test_random_graphs(): + """Generate 5 random graphs of different types and sizes and + make sure that all sets are independent and maximal.""" + for i in range(0, 50, 10): + G = nx.erdos_renyi_graph(i * 10 + 1, random.random()) + IS = nx.maximal_independent_set(G) + assert G.subgraph(IS).number_of_edges() == 0 + nbrs_of_MIS = set.union(*(set(G.neighbors(v)) for v in IS)) + assert all(v in nbrs_of_MIS for v in set(G.nodes()).difference(IS)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py new file mode 100644 index 0000000000000000000000000000000000000000..fc98c9729a95897857013ae22333e3b8c17202fb --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_moral.py @@ -0,0 +1,15 @@ +import networkx as nx +from networkx.algorithms.moral import moral_graph + + +def test_get_moral_graph(): + graph = nx.DiGraph() + graph.add_nodes_from([1, 2, 3, 4, 5, 6, 7]) + graph.add_edges_from([(1, 2), (3, 2), (4, 1), (4, 5), (6, 5), (7, 5)]) + H = moral_graph(graph) + assert not H.is_directed() + assert H.has_edge(1, 3) + assert H.has_edge(4, 6) + assert H.has_edge(6, 7) + assert H.has_edge(4, 7) + assert not H.has_edge(1, 5) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py new file mode 100644 index 0000000000000000000000000000000000000000..2e1fc79d48ae830625c3528f52e805d2e0d183ad --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_node_classification.py @@ -0,0 +1,140 @@ +import pytest + +pytest.importorskip("numpy") +pytest.importorskip("scipy") + +import networkx as nx +from networkx.algorithms import node_classification + + +class TestHarmonicFunction: + def test_path_graph(self): + G = nx.path_graph(4) + label_name = "label" + G.nodes[0][label_name] = "A" + G.nodes[3][label_name] = "B" + predicted = node_classification.harmonic_function(G, label_name=label_name) + assert predicted[0] == "A" + assert predicted[1] == "A" + assert predicted[2] == "B" + assert predicted[3] == "B" + + def test_no_labels(self): + with pytest.raises(nx.NetworkXError): + G = nx.path_graph(4) + node_classification.harmonic_function(G) + + def test_no_nodes(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + node_classification.harmonic_function(G) + + def test_no_edges(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + G.add_node(1) + G.add_node(2) + node_classification.harmonic_function(G) + + def test_digraph(self): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.DiGraph() + G.add_edge(0, 1) + G.add_edge(1, 2) + G.add_edge(2, 3) + label_name = "label" + G.nodes[0][label_name] = "A" + G.nodes[3][label_name] = "B" + node_classification.harmonic_function(G) + + def test_one_labeled_node(self): + G = nx.path_graph(4) + label_name = "label" + G.nodes[0][label_name] = "A" + predicted = node_classification.harmonic_function(G, label_name=label_name) + assert predicted[0] == "A" + assert predicted[1] == "A" + assert predicted[2] == "A" + assert predicted[3] == "A" + + def test_nodes_all_labeled(self): + G = nx.karate_club_graph() + label_name = "club" + predicted = node_classification.harmonic_function(G, label_name=label_name) + for i in range(len(G)): + assert predicted[i] == G.nodes[i][label_name] + + def test_labeled_nodes_are_not_changed(self): + G = nx.karate_club_graph() + label_name = "club" + label_removed = {0, 1, 2, 3, 4, 5, 6, 7} + for i in label_removed: + del G.nodes[i][label_name] + predicted = node_classification.harmonic_function(G, label_name=label_name) + label_not_removed = set(range(len(G))) - label_removed + for i in label_not_removed: + assert predicted[i] == G.nodes[i][label_name] + + +class TestLocalAndGlobalConsistency: + def test_path_graph(self): + G = nx.path_graph(4) + label_name = "label" + G.nodes[0][label_name] = "A" + G.nodes[3][label_name] = "B" + predicted = node_classification.local_and_global_consistency( + G, label_name=label_name + ) + assert predicted[0] == "A" + assert predicted[1] == "A" + assert predicted[2] == "B" + assert predicted[3] == "B" + + def test_no_labels(self): + with pytest.raises(nx.NetworkXError): + G = nx.path_graph(4) + node_classification.local_and_global_consistency(G) + + def test_no_nodes(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + node_classification.local_and_global_consistency(G) + + def test_no_edges(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + G.add_node(1) + G.add_node(2) + node_classification.local_and_global_consistency(G) + + def test_digraph(self): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.DiGraph() + G.add_edge(0, 1) + G.add_edge(1, 2) + G.add_edge(2, 3) + label_name = "label" + G.nodes[0][label_name] = "A" + G.nodes[3][label_name] = "B" + node_classification.harmonic_function(G) + + def test_one_labeled_node(self): + G = nx.path_graph(4) + label_name = "label" + G.nodes[0][label_name] = "A" + predicted = node_classification.local_and_global_consistency( + G, label_name=label_name + ) + assert predicted[0] == "A" + assert predicted[1] == "A" + assert predicted[2] == "A" + assert predicted[3] == "A" + + def test_nodes_all_labeled(self): + G = nx.karate_club_graph() + label_name = "club" + predicted = node_classification.local_and_global_consistency( + G, alpha=0, label_name=label_name + ) + for i in range(len(G)): + assert predicted[i] == G.nodes[i][label_name] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py new file mode 100644 index 0000000000000000000000000000000000000000..bdbcdaf15445f17fb22a7e3ec737a9f1a774b51c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_non_randomness.py @@ -0,0 +1,60 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") + + +@pytest.mark.parametrize( + "k, weight, expected", + [ + (None, None, 7.21), # infers 3 communities + (2, None, 11.7), + (None, "weight", 25.45), + (2, "weight", 38.8), + ], +) +def test_non_randomness(k, weight, expected): + G = nx.karate_club_graph() + np.testing.assert_almost_equal( + nx.non_randomness(G, k, weight)[0], expected, decimal=2 + ) + + +def test_non_connected(): + G = nx.Graph([(1, 2)]) + G.add_node(3) + with pytest.raises(nx.NetworkXException, match="Non connected"): + nx.non_randomness(G) + + +def test_self_loops(): + G = nx.Graph() + G.add_edge(1, 2) + G.add_edge(1, 1) + with pytest.raises(nx.NetworkXError, match="Graph must not contain self-loops"): + nx.non_randomness(G) + + +def test_empty_graph(): + G = nx.empty_graph(1) + with pytest.raises(nx.NetworkXError, match=".*not applicable to empty graphs"): + nx.non_randomness(G) + + +@pytest.mark.parametrize("k", [-1, 0, 2, 5]) +def test_value_error(k): + """ + Check that invalid values of k raise (must be between 1 and n - 1, inclusive, + and such that the probability is between 0 and 1, exclusive). + """ + G = nx.path_graph(5) + with pytest.raises(ValueError, match=r"invalid number of communities"): + nx.non_randomness(G, k=k) + + +@pytest.mark.parametrize("G", [nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()]) +def test_not_implemented(G): + """Check that non-randomness is not implemented for directed or multigraphs.""" + with pytest.raises(nx.NetworkXNotImplemented, match=r"not implemented for"): + nx.non_randomness(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_perfect_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_perfect_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..51c12589fb713176d1a06b272c63e095d04ad2af --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_perfect_graph.py @@ -0,0 +1,27 @@ +import networkx as nx + + +def test_chordal_graph(): + G = nx.complete_graph(5) + assert nx.is_perfect_graph(G) + + +def test_odd_cycle(): + G = nx.cycle_graph(5) # Induced odd cycle + assert not nx.is_perfect_graph(G) + + +def test_even_cycle(): + G = nx.cycle_graph(6) # Even cycle is perfect + assert nx.is_perfect_graph(G) + + +def test_complement_of_odd_cycle(): + G = nx.cycle_graph(7) + GC = nx.complement(G) + assert not nx.is_perfect_graph(GC) + + +def test_disconnected_union_of_cliques(): + G = nx.disjoint_union(nx.complete_graph(3), nx.complete_graph(4)) + assert nx.is_perfect_graph(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py new file mode 100644 index 0000000000000000000000000000000000000000..b59d5c17331d6aac71e688b3dfde46991ea2ef97 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planar_drawing.py @@ -0,0 +1,274 @@ +import math + +import pytest + +import networkx as nx +from networkx.algorithms.planar_drawing import triangulate_embedding + + +def test_graph1(): + embedding_data = {0: [1, 2, 3], 1: [2, 0], 2: [3, 0, 1], 3: [2, 0]} + check_embedding_data(embedding_data) + + +def test_graph2(): + embedding_data = { + 0: [8, 6], + 1: [2, 6, 9], + 2: [8, 1, 7, 9, 6, 4], + 3: [9], + 4: [2], + 5: [6, 8], + 6: [9, 1, 0, 5, 2], + 7: [9, 2], + 8: [0, 2, 5], + 9: [1, 6, 2, 7, 3], + } + check_embedding_data(embedding_data) + + +def test_circle_graph(): + embedding_data = { + 0: [1, 9], + 1: [0, 2], + 2: [1, 3], + 3: [2, 4], + 4: [3, 5], + 5: [4, 6], + 6: [5, 7], + 7: [6, 8], + 8: [7, 9], + 9: [8, 0], + } + check_embedding_data(embedding_data) + + +def test_grid_graph(): + embedding_data = { + (0, 1): [(0, 0), (1, 1), (0, 2)], + (1, 2): [(1, 1), (2, 2), (0, 2)], + (0, 0): [(0, 1), (1, 0)], + (2, 1): [(2, 0), (2, 2), (1, 1)], + (1, 1): [(2, 1), (1, 2), (0, 1), (1, 0)], + (2, 0): [(1, 0), (2, 1)], + (2, 2): [(1, 2), (2, 1)], + (1, 0): [(0, 0), (2, 0), (1, 1)], + (0, 2): [(1, 2), (0, 1)], + } + check_embedding_data(embedding_data) + + +def test_one_node_graph(): + embedding_data = {0: []} + check_embedding_data(embedding_data) + + +def test_two_node_graph(): + embedding_data = {0: [1], 1: [0]} + check_embedding_data(embedding_data) + + +def test_three_node_graph(): + embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1]} + check_embedding_data(embedding_data) + + +def test_multiple_component_graph1(): + embedding_data = {0: [], 1: []} + check_embedding_data(embedding_data) + + +def test_multiple_component_graph2(): + embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1], 3: [4, 5], 4: [3, 5], 5: [3, 4]} + check_embedding_data(embedding_data) + + +def test_invalid_half_edge(): + with pytest.raises(nx.NetworkXException): + embedding_data = {1: [2, 3, 4], 2: [1, 3, 4], 3: [1, 2, 4], 4: [1, 2, 3]} + embedding = nx.PlanarEmbedding() + embedding.set_data(embedding_data) + nx.combinatorial_embedding_to_pos(embedding) + + +def test_triangulate_embedding1(): + embedding = nx.PlanarEmbedding() + embedding.add_node(1) + expected_embedding = {1: []} + check_triangulation(embedding, expected_embedding) + + +def test_triangulate_embedding2(): + embedding = nx.PlanarEmbedding() + embedding.connect_components(1, 2) + expected_embedding = {1: [2], 2: [1]} + check_triangulation(embedding, expected_embedding) + + +def check_triangulation(embedding, expected_embedding): + res_embedding, _ = triangulate_embedding(embedding, True) + assert res_embedding.get_data() == expected_embedding, ( + "Expected embedding incorrect" + ) + res_embedding, _ = triangulate_embedding(embedding, False) + assert res_embedding.get_data() == expected_embedding, ( + "Expected embedding incorrect" + ) + + +def check_embedding_data(embedding_data): + """Checks that the planar embedding of the input is correct""" + embedding = nx.PlanarEmbedding() + embedding.set_data(embedding_data) + pos_fully = nx.combinatorial_embedding_to_pos(embedding, False) + msg = "Planar drawing does not conform to the embedding (fully triangulation)" + assert planar_drawing_conforms_to_embedding(embedding, pos_fully), msg + check_edge_intersections(embedding, pos_fully) + pos_internally = nx.combinatorial_embedding_to_pos(embedding, True) + msg = "Planar drawing does not conform to the embedding (internal triangulation)" + assert planar_drawing_conforms_to_embedding(embedding, pos_internally), msg + check_edge_intersections(embedding, pos_internally) + + +def is_close(a, b, rel_tol=1e-09, abs_tol=0.0): + # Check if float numbers are basically equal, for python >=3.5 there is + # function for that in the standard library + return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) + + +def point_in_between(a, b, p): + # checks if p is on the line between a and b + x1, y1 = a + x2, y2 = b + px, py = p + dist_1_2 = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) + dist_1_p = math.sqrt((x1 - px) ** 2 + (y1 - py) ** 2) + dist_2_p = math.sqrt((x2 - px) ** 2 + (y2 - py) ** 2) + return is_close(dist_1_p + dist_2_p, dist_1_2) + + +def check_edge_intersections(G, pos): + """Check all edges in G for intersections. + + Raises an exception if an intersection is found. + + Parameters + ---------- + G : NetworkX graph + pos : dict + Maps every node to a tuple (x, y) representing its position + + """ + for a, b in G.edges(): + for c, d in G.edges(): + # Check if end points are different + if a != c and b != d and b != c and a != d: + x1, y1 = pos[a] + x2, y2 = pos[b] + x3, y3 = pos[c] + x4, y4 = pos[d] + determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if determinant != 0: # the lines are not parallel + # calculate intersection point, see: + # https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection + px = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * ( + x3 * y4 - y3 * x4 + ) / determinant + py = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * ( + x3 * y4 - y3 * x4 + ) / determinant + + # Check if intersection lies between the points + if point_in_between(pos[a], pos[b], (px, py)) and point_in_between( + pos[c], pos[d], (px, py) + ): + msg = f"There is an intersection at {px},{py}" + raise nx.NetworkXException(msg) + + # Check overlap + msg = "A node lies on a edge connecting two other nodes" + if ( + point_in_between(pos[a], pos[b], pos[c]) + or point_in_between(pos[a], pos[b], pos[d]) + or point_in_between(pos[c], pos[d], pos[a]) + or point_in_between(pos[c], pos[d], pos[b]) + ): + raise nx.NetworkXException(msg) + # No edge intersection found + + +class Vector: + """Compare vectors by their angle without loss of precision + + All vectors in direction [0, 1] are the smallest. + The vectors grow in clockwise direction. + """ + + __slots__ = ["x", "y", "node", "quadrant"] + + def __init__(self, x, y, node): + self.x = x + self.y = y + self.node = node + if self.x >= 0 and self.y > 0: + self.quadrant = 1 + elif self.x > 0 and self.y <= 0: + self.quadrant = 2 + elif self.x <= 0 and self.y < 0: + self.quadrant = 3 + else: + self.quadrant = 4 + + def __eq__(self, other): + return self.quadrant == other.quadrant and self.x * other.y == self.y * other.x + + def __lt__(self, other): + if self.quadrant < other.quadrant: + return True + elif self.quadrant > other.quadrant: + return False + else: + return self.x * other.y < self.y * other.x + + def __ne__(self, other): + return self != other + + def __le__(self, other): + return not other < self + + def __gt__(self, other): + return other < self + + def __ge__(self, other): + return not self < other + + +def planar_drawing_conforms_to_embedding(embedding, pos): + """Checks if pos conforms to the planar embedding + + Returns true iff the neighbors are actually oriented in the orientation + specified of the embedding + """ + for v in embedding: + nbr_vectors = [] + v_pos = pos[v] + for nbr in embedding[v]: + new_vector = Vector(pos[nbr][0] - v_pos[0], pos[nbr][1] - v_pos[1], nbr) + nbr_vectors.append(new_vector) + # Sort neighbors according to their phi angle + nbr_vectors.sort() + for idx, nbr_vector in enumerate(nbr_vectors): + cw_vector = nbr_vectors[(idx + 1) % len(nbr_vectors)] + ccw_vector = nbr_vectors[idx - 1] + if ( + embedding[v][nbr_vector.node]["cw"] != cw_vector.node + or embedding[v][nbr_vector.node]["ccw"] != ccw_vector.node + ): + return False + if cw_vector.node != nbr_vector.node and cw_vector == nbr_vector: + # Lines overlap + return False + if ccw_vector.node != nbr_vector.node and ccw_vector == nbr_vector: + # Lines overlap + return False + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py new file mode 100644 index 0000000000000000000000000000000000000000..af2d9a981d0120f97dce4a3a32bef457431c2e3d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_planarity.py @@ -0,0 +1,556 @@ +import pytest + +import networkx as nx +from networkx.algorithms.planarity import ( + check_planarity_recursive, + get_counterexample, + get_counterexample_recursive, +) + + +class TestLRPlanarity: + """Nose Unit tests for the :mod:`networkx.algorithms.planarity` module. + + Tests three things: + 1. Check that the result is correct + (returns planar if and only if the graph is actually planar) + 2. In case a counter example is returned: Check if it is correct + 3. In case an embedding is returned: Check if its actually an embedding + """ + + @staticmethod + def check_graph(G, is_planar=None): + """Raises an exception if the lr_planarity check returns a wrong result + + Parameters + ---------- + G : NetworkX graph + is_planar : bool + The expected result of the planarity check. + If set to None only counter example or embedding are verified. + + """ + + # obtain results of planarity check + is_planar_lr, result = nx.check_planarity(G, True) + is_planar_lr_rec, result_rec = check_planarity_recursive(G, True) + + if is_planar is not None: + # set a message for the assert + if is_planar: + msg = "Wrong planarity check result. Should be planar." + else: + msg = "Wrong planarity check result. Should be non-planar." + + # check if the result is as expected + assert is_planar == is_planar_lr, msg + assert is_planar == is_planar_lr_rec, msg + + if is_planar_lr: + # check embedding + check_embedding(G, result) + check_embedding(G, result_rec) + else: + # check counter example + check_counterexample(G, result) + check_counterexample(G, result_rec) + + def test_simple_planar_graph(self): + e = [ + (1, 2), + (2, 3), + (3, 4), + (4, 6), + (6, 7), + (7, 1), + (1, 5), + (5, 2), + (2, 4), + (4, 5), + (5, 7), + ] + self.check_graph(nx.Graph(e), is_planar=True) + + def test_planar_with_selfloop(self): + e = [ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (1, 2), + (1, 3), + (1, 5), + (2, 5), + (2, 4), + (3, 4), + (3, 5), + (4, 5), + ] + self.check_graph(nx.Graph(e), is_planar=True) + + def test_k3_3(self): + self.check_graph(nx.complete_bipartite_graph(3, 3), is_planar=False) + + def test_k5(self): + self.check_graph(nx.complete_graph(5), is_planar=False) + + def test_multiple_components_planar(self): + e = [(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)] + self.check_graph(nx.Graph(e), is_planar=True) + + def test_multiple_components_non_planar(self): + G = nx.complete_graph(5) + # add another planar component to the non planar component + # G stays non planar + G.add_edges_from([(6, 7), (7, 8), (8, 6)]) + self.check_graph(G, is_planar=False) + + def test_non_planar_with_selfloop(self): + G = nx.complete_graph(5) + # add self loops + for i in range(5): + G.add_edge(i, i) + self.check_graph(G, is_planar=False) + + def test_non_planar1(self): + # tests a graph that has no subgraph directly isomorph to K5 or K3_3 + e = [ + (1, 5), + (1, 6), + (1, 7), + (2, 6), + (2, 3), + (3, 5), + (3, 7), + (4, 5), + (4, 6), + (4, 7), + ] + self.check_graph(nx.Graph(e), is_planar=False) + + def test_loop(self): + # test a graph with a selfloop + e = [(1, 2), (2, 2)] + G = nx.Graph(e) + self.check_graph(G, is_planar=True) + + def test_comp(self): + # test multiple component graph + e = [(1, 2), (3, 4)] + G = nx.Graph(e) + G.remove_edge(1, 2) + self.check_graph(G, is_planar=True) + + def test_goldner_harary(self): + # test goldner-harary graph (a maximal planar graph) + e = [ + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 7), + (1, 8), + (1, 10), + (1, 11), + (2, 3), + (2, 4), + (2, 6), + (2, 7), + (2, 9), + (2, 10), + (2, 11), + (3, 4), + (4, 5), + (4, 6), + (4, 7), + (5, 7), + (6, 7), + (7, 8), + (7, 9), + (7, 10), + (8, 10), + (9, 10), + (10, 11), + ] + G = nx.Graph(e) + self.check_graph(G, is_planar=True) + + def test_planar_multigraph(self): + G = nx.MultiGraph([(1, 2), (1, 2), (1, 2), (1, 2), (2, 3), (3, 1)]) + self.check_graph(G, is_planar=True) + + def test_non_planar_multigraph(self): + G = nx.MultiGraph(nx.complete_graph(5)) + G.add_edges_from([(1, 2)] * 5) + self.check_graph(G, is_planar=False) + + def test_planar_digraph(self): + G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (4, 1), (4, 2), (1, 4), (3, 2)]) + self.check_graph(G, is_planar=True) + + def test_non_planar_digraph(self): + G = nx.DiGraph(nx.complete_graph(5)) + G.remove_edge(1, 2) + G.remove_edge(4, 1) + self.check_graph(G, is_planar=False) + + def test_single_component(self): + # Test a graph with only a single node + G = nx.Graph() + G.add_node(1) + self.check_graph(G, is_planar=True) + + def test_graph1(self): + G = nx.Graph( + [ + (3, 10), + (2, 13), + (1, 13), + (7, 11), + (0, 8), + (8, 13), + (0, 2), + (0, 7), + (0, 10), + (1, 7), + ] + ) + self.check_graph(G, is_planar=True) + + def test_graph2(self): + G = nx.Graph( + [ + (1, 2), + (4, 13), + (0, 13), + (4, 5), + (7, 10), + (1, 7), + (0, 3), + (2, 6), + (5, 6), + (7, 13), + (4, 8), + (0, 8), + (0, 9), + (2, 13), + (6, 7), + (3, 6), + (2, 8), + ] + ) + self.check_graph(G, is_planar=False) + + def test_graph3(self): + G = nx.Graph( + [ + (0, 7), + (3, 11), + (3, 4), + (8, 9), + (4, 11), + (1, 7), + (1, 13), + (1, 11), + (3, 5), + (5, 7), + (1, 3), + (0, 4), + (5, 11), + (5, 13), + ] + ) + self.check_graph(G, is_planar=False) + + def test_counterexample_planar(self): + with pytest.raises(nx.NetworkXException): + # Try to get a counterexample of a planar graph + G = nx.Graph() + G.add_node(1) + get_counterexample(G) + + def test_counterexample_planar_recursive(self): + with pytest.raises(nx.NetworkXException): + # Try to get a counterexample of a planar graph + G = nx.Graph() + G.add_node(1) + get_counterexample_recursive(G) + + def test_edge_removal_from_planar_embedding(self): + # PlanarEmbedding.check_structure() must succeed after edge removal + edges = ((0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (0, 3)) + G = nx.Graph(edges) + cert, P = nx.check_planarity(G) + assert cert is True + P.remove_edge(0, 2) + self.check_graph(P, is_planar=True) + P.add_half_edge_ccw(1, 3, 2) + P.add_half_edge_cw(3, 1, 2) + self.check_graph(P, is_planar=True) + P.remove_edges_from(((0, 3), (1, 3))) + self.check_graph(P, is_planar=True) + + @pytest.mark.parametrize("graph_type", (nx.Graph, nx.MultiGraph)) + def test_graph_planar_embedding_to_undirected(self, graph_type): + G = graph_type([(0, 1), (0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]) + is_planar, P = nx.check_planarity(G) + assert is_planar + U = P.to_undirected() + assert isinstance(U, nx.Graph) + assert all((d == {} for _, _, d in U.edges(data=True))) + + @pytest.mark.parametrize( + "reciprocal, as_view", [(True, True), (True, False), (False, True)] + ) + def test_planar_embedding_to_undirected_invalid_parameters( + self, reciprocal, as_view + ): + G = nx.Graph([(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]) + is_planar, P = nx.check_planarity(G) + assert is_planar + with pytest.raises(ValueError, match="is not supported for PlanarEmbedding."): + P.to_undirected(reciprocal=reciprocal, as_view=as_view) + + +def check_embedding(G, embedding): + """Raises an exception if the combinatorial embedding is not correct + + Parameters + ---------- + G : NetworkX graph + embedding : a dict mapping nodes to a list of edges + This specifies the ordering of the outgoing edges from a node for + a combinatorial embedding + + Notes + ----- + Checks the following things: + - The type of the embedding is correct + - The nodes and edges match the original graph + - Every half edge has its matching opposite half edge + - No intersections of edges (checked by Euler's formula) + """ + + if not isinstance(embedding, nx.PlanarEmbedding): + raise nx.NetworkXException("Bad embedding. Not of type nx.PlanarEmbedding") + + # Check structure + embedding.check_structure() + + # Check that graphs are equivalent + + assert set(G.nodes) == set(embedding.nodes), ( + "Bad embedding. Nodes don't match the original graph." + ) + + # Check that the edges are equal + g_edges = set() + for edge in G.edges: + if edge[0] != edge[1]: + g_edges.add((edge[0], edge[1])) + g_edges.add((edge[1], edge[0])) + assert g_edges == set(embedding.edges), ( + "Bad embedding. Edges don't match the original graph." + ) + + +def check_counterexample(G, sub_graph): + """Raises an exception if the counterexample is wrong. + + Parameters + ---------- + G : NetworkX graph + subdivision_nodes : set + A set of nodes inducing a subgraph as a counterexample + """ + # 1. Create the sub graph + sub_graph = nx.Graph(sub_graph) + + # 2. Remove self loops + for u in sub_graph: + if sub_graph.has_edge(u, u): + sub_graph.remove_edge(u, u) + + # keep track of nodes we might need to contract + contract = list(sub_graph) + + # 3. Contract Edges + while len(contract) > 0: + contract_node = contract.pop() + if contract_node not in sub_graph: + # Node was already contracted + continue + degree = sub_graph.degree[contract_node] + # Check if we can remove the node + if degree == 2: + # Get the two neighbors + neighbors = iter(sub_graph[contract_node]) + u = next(neighbors) + v = next(neighbors) + # Save nodes for later + contract.append(u) + contract.append(v) + # Contract edge + sub_graph.remove_node(contract_node) + sub_graph.add_edge(u, v) + + # 4. Check for isomorphism with K5 or K3_3 graphs + if len(sub_graph) == 5: + if not nx.is_isomorphic(nx.complete_graph(5), sub_graph): + raise nx.NetworkXException("Bad counter example.") + elif len(sub_graph) == 6: + if not nx.is_isomorphic(nx.complete_bipartite_graph(3, 3), sub_graph): + raise nx.NetworkXException("Bad counter example.") + else: + raise nx.NetworkXException("Bad counter example.") + + +class TestPlanarEmbeddingClass: + def test_add_half_edge(self): + embedding = nx.PlanarEmbedding() + embedding.add_half_edge(0, 1) + with pytest.raises( + nx.NetworkXException, match="Invalid clockwise reference node." + ): + embedding.add_half_edge(0, 2, cw=3) + with pytest.raises( + nx.NetworkXException, match="Invalid counterclockwise reference node." + ): + embedding.add_half_edge(0, 2, ccw=3) + with pytest.raises( + nx.NetworkXException, match="Only one of cw/ccw can be specified." + ): + embedding.add_half_edge(0, 2, cw=1, ccw=1) + with pytest.raises( + nx.NetworkXException, + match=( + r"Node already has out-half-edge\(s\), either" + " cw or ccw reference node required." + ), + ): + embedding.add_half_edge(0, 2) + # these should work + embedding.add_half_edge(0, 2, cw=1) + embedding.add_half_edge(0, 3, ccw=1) + assert sorted(embedding.edges(data=True)) == [ + (0, 1, {"ccw": 2, "cw": 3}), + (0, 2, {"cw": 1, "ccw": 3}), + (0, 3, {"cw": 2, "ccw": 1}), + ] + + def test_get_data(self): + embedding = self.get_star_embedding(4) + data = embedding.get_data() + data_cmp = {0: [3, 2, 1], 1: [0], 2: [0], 3: [0]} + assert data == data_cmp + + def test_edge_removal(self): + embedding = nx.PlanarEmbedding() + embedding.set_data( + { + 1: [2, 5, 7], + 2: [1, 3, 4, 5], + 3: [2, 4], + 4: [3, 6, 5, 2], + 5: [7, 1, 2, 4], + 6: [4, 7], + 7: [6, 1, 5], + } + ) + # remove_edges_from() calls remove_edge(), so both are tested here + embedding.remove_edges_from(((5, 4), (1, 5))) + embedding.check_structure() + embedding_expected = nx.PlanarEmbedding() + embedding_expected.set_data( + { + 1: [2, 7], + 2: [1, 3, 4, 5], + 3: [2, 4], + 4: [3, 6, 2], + 5: [7, 2], + 6: [4, 7], + 7: [6, 1, 5], + } + ) + assert nx.utils.graphs_equal(embedding, embedding_expected) + + def test_missing_edge_orientation(self): + embedding = nx.PlanarEmbedding({1: {2: {}}, 2: {1: {}}}) + with pytest.raises(nx.NetworkXException): + # Invalid structure because the orientation of the edge was not set + embedding.check_structure() + + def test_invalid_edge_orientation(self): + embedding = nx.PlanarEmbedding( + { + 1: {2: {"cw": 2, "ccw": 2}}, + 2: {1: {"cw": 1, "ccw": 1}}, + 1: {3: {}}, + 3: {1: {}}, + } + ) + with pytest.raises(nx.NetworkXException): + embedding.check_structure() + + def test_missing_half_edge(self): + embedding = nx.PlanarEmbedding() + embedding.add_half_edge(1, 2) + with pytest.raises(nx.NetworkXException): + # Invalid structure because other half edge is missing + embedding.check_structure() + + def test_not_fulfilling_euler_formula(self): + embedding = nx.PlanarEmbedding() + for i in range(5): + ref = None + for j in range(5): + if i != j: + embedding.add_half_edge(i, j, cw=ref) + ref = j + with pytest.raises(nx.NetworkXException): + embedding.check_structure() + + def test_missing_reference(self): + embedding = nx.PlanarEmbedding() + with pytest.raises(nx.NetworkXException, match="Invalid reference node."): + embedding.add_half_edge(1, 2, ccw=3) + + def test_connect_components(self): + embedding = nx.PlanarEmbedding() + embedding.connect_components(1, 2) + + def test_successful_face_traversal(self): + embedding = nx.PlanarEmbedding() + embedding.add_half_edge(1, 2) + embedding.add_half_edge(2, 1) + face = embedding.traverse_face(1, 2) + assert face == [1, 2] + + def test_unsuccessful_face_traversal(self): + embedding = nx.PlanarEmbedding( + {1: {2: {"cw": 3, "ccw": 2}}, 2: {1: {"cw": 3, "ccw": 1}}} + ) + with pytest.raises(nx.NetworkXException): + embedding.traverse_face(1, 2) + + def test_forbidden_methods(self): + embedding = nx.PlanarEmbedding() + embedding.add_node(42) # no exception + embedding.add_nodes_from([(23, 24)]) # no exception + with pytest.raises(NotImplementedError): + embedding.add_edge(1, 3) + with pytest.raises(NotImplementedError): + embedding.add_edges_from([(0, 2), (1, 4)]) + with pytest.raises(NotImplementedError): + embedding.add_weighted_edges_from([(0, 2, 350), (1, 4, 125)]) + + @staticmethod + def get_star_embedding(n): + embedding = nx.PlanarEmbedding() + ref = None + for i in range(1, n): + embedding.add_half_edge(0, i, cw=ref) + ref = i + embedding.add_half_edge(i, 0) + return embedding diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py new file mode 100644 index 0000000000000000000000000000000000000000..a81d6a69551ead74d3335fda408111a0b580bf6a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_polynomials.py @@ -0,0 +1,57 @@ +"""Unit tests for the :mod:`networkx.algorithms.polynomials` module.""" + +import pytest + +import networkx as nx + +sympy = pytest.importorskip("sympy") + + +# Mapping of input graphs to a string representation of their tutte polynomials +_test_tutte_graphs = { + nx.complete_graph(1): "1", + nx.complete_graph(4): "x**3 + 3*x**2 + 4*x*y + 2*x + y**3 + 3*y**2 + 2*y", + nx.cycle_graph(5): "x**4 + x**3 + x**2 + x + y", + nx.diamond_graph(): "x**3 + 2*x**2 + 2*x*y + x + y**2 + y", +} + +_test_chromatic_graphs = { + nx.complete_graph(1): "x", + nx.complete_graph(4): "x**4 - 6*x**3 + 11*x**2 - 6*x", + nx.cycle_graph(5): "x**5 - 5*x**4 + 10*x**3 - 10*x**2 + 4*x", + nx.diamond_graph(): "x**4 - 5*x**3 + 8*x**2 - 4*x", + nx.path_graph(5): "x**5 - 4*x**4 + 6*x**3 - 4*x**2 + x", +} + + +@pytest.mark.parametrize(("G", "expected"), _test_tutte_graphs.items()) +def test_tutte_polynomial(G, expected): + assert nx.tutte_polynomial(G).equals(expected) + + +@pytest.mark.parametrize("G", _test_tutte_graphs.keys()) +def test_tutte_polynomial_disjoint(G): + """Tutte polynomial factors into the Tutte polynomials of its components. + Verify this property with the disjoint union of two copies of the input graph. + """ + t_g = nx.tutte_polynomial(G) + H = nx.disjoint_union(G, G) + t_h = nx.tutte_polynomial(H) + assert sympy.simplify(t_g * t_g).equals(t_h) + + +@pytest.mark.parametrize(("G", "expected"), _test_chromatic_graphs.items()) +def test_chromatic_polynomial(G, expected): + assert nx.chromatic_polynomial(G).equals(expected) + + +@pytest.mark.parametrize("G", _test_chromatic_graphs.keys()) +def test_chromatic_polynomial_disjoint(G): + """Chromatic polynomial factors into the Chromatic polynomials of its + components. Verify this property with the disjoint union of two copies of + the input graph. + """ + x_g = nx.chromatic_polynomial(G) + H = nx.disjoint_union(G, G) + x_h = nx.chromatic_polynomial(H) + assert sympy.simplify(x_g * x_g).equals(x_h) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py new file mode 100644 index 0000000000000000000000000000000000000000..e713bc4303f9bfea1199f01d8369c6bdab1a221f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_reciprocity.py @@ -0,0 +1,37 @@ +import pytest + +import networkx as nx + + +class TestReciprocity: + # test overall reciprocity by passing whole graph + def test_reciprocity_digraph(self): + DG = nx.DiGraph([(1, 2), (2, 1)]) + reciprocity = nx.reciprocity(DG) + assert reciprocity == 1.0 + + # test empty graph's overall reciprocity which will throw an error + def test_overall_reciprocity_empty_graph(self): + with pytest.raises(nx.NetworkXError): + DG = nx.DiGraph() + nx.overall_reciprocity(DG) + + # test for reciprocity for a list of nodes + def test_reciprocity_graph_nodes(self): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 2)]) + reciprocity = nx.reciprocity(DG, [1, 2]) + expected_reciprocity = {1: 0.0, 2: 0.6666666666666666} + assert reciprocity == expected_reciprocity + + # test for reciprocity for a single node + def test_reciprocity_graph_node(self): + DG = nx.DiGraph([(1, 2), (2, 3), (3, 2)]) + reciprocity = nx.reciprocity(DG, 2) + assert reciprocity == 0.6666666666666666 + + # test for reciprocity for an isolated node + def test_reciprocity_graph_isolated_nodes(self): + with pytest.raises(nx.NetworkXError): + DG = nx.DiGraph([(1, 2)]) + DG.add_node(4) + nx.reciprocity(DG, 4) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py new file mode 100644 index 0000000000000000000000000000000000000000..b652089a65c927256efda2bae2f4510d7aead739 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_regular.py @@ -0,0 +1,88 @@ +import pytest + +import networkx as nx +import networkx.algorithms.regular as reg +import networkx.generators as gen + + +class TestKFactor: + @pytest.mark.parametrize("n", [3, 4, 5]) + def test_k_factor_cycle(self, n): + g = nx.cycle_graph(n) + kf = nx.k_factor(g, 2) + assert g.edges == kf.edges + assert g.nodes == kf.nodes + + @pytest.mark.parametrize("k", range(3)) + def test_k_factor_grid(self, k): + g = nx.grid_2d_graph(4, 4) + kf = nx.k_factor(g, k) + assert g.nodes == kf.nodes + assert all(g.has_edge(*e) for e in kf.edges) + assert nx.is_k_regular(kf, k) + + @pytest.mark.parametrize("k", range(6)) + def test_k_factor_complete(self, k): + g = nx.complete_graph(6) + kf = nx.k_factor(g, k) + assert g.nodes == kf.nodes + assert all(g.has_edge(*e) for e in kf.edges) + assert nx.is_k_regular(kf, k) + + def test_k_factor_degree(self): + g = nx.grid_2d_graph(4, 4) + with pytest.raises(nx.NetworkXUnfeasible, match=r"degree less than"): + nx.k_factor(g, 3) + + def test_k_factor_no_matching(self): + g = nx.hexagonal_lattice_graph(4, 4) + # Perfect matching doesn't exist for 4,4 hexagonal lattice graph + with pytest.raises(nx.NetworkXUnfeasible, match=r"no perfect matching"): + nx.k_factor(g, 2) + + @pytest.mark.parametrize("graph_type", [nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]) + def test_k_factor_not_implemented(self, graph_type): + with pytest.raises(nx.NetworkXNotImplemented, match=r"not implemented for"): + nx.k_factor(graph_type(), 2) + + +class TestIsRegular: + @pytest.mark.parametrize( + "graph,expected", + [ + (nx.cycle_graph(4), True), + (nx.complete_graph(5), True), + (nx.path_graph(5), False), + (nx.lollipop_graph(5, 5), False), + (nx.cycle_graph(3, create_using=nx.DiGraph), True), + (nx.Graph([(0, 1)]), True), + (nx.DiGraph([(0, 1)]), False), + (nx.MultiGraph([(0, 1), (0, 1)]), True), + (nx.MultiDiGraph([(0, 1), (0, 1)]), False), + ], + ) + def test_is_regular(self, graph, expected): + assert reg.is_regular(graph) == expected + + def test_is_regular_empty_graph_raises(self): + G = nx.Graph() + with pytest.raises(nx.NetworkXPointlessConcept, match="Graph has no nodes"): + nx.is_regular(G) + + +class TestIsKRegular: + def test_is_k_regular1(self): + g = gen.cycle_graph(4) + assert reg.is_k_regular(g, 2) + assert not reg.is_k_regular(g, 3) + + def test_is_k_regular2(self): + g = gen.complete_graph(5) + assert reg.is_k_regular(g, 4) + assert not reg.is_k_regular(g, 3) + assert not reg.is_k_regular(g, 6) + + def test_is_k_regular3(self): + g = gen.lollipop_graph(5, 5) + assert not reg.is_k_regular(g, 5) + assert not reg.is_k_regular(g, 6) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py new file mode 100644 index 0000000000000000000000000000000000000000..21721577fed219aebcfe9a4388b25503ad252140 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_richclub.py @@ -0,0 +1,149 @@ +import pytest + +import networkx as nx + + +def test_richclub(): + G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)]) + rc = nx.richclub.rich_club_coefficient(G, normalized=False) + assert rc == {0: 12.0 / 30, 1: 8.0 / 12} + + # test single value + rc0 = nx.richclub.rich_club_coefficient(G, normalized=False)[0] + assert rc0 == 12.0 / 30.0 + + +def test_richclub_seed(): + G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)]) + rcNorm = nx.richclub.rich_club_coefficient(G, Q=2, seed=1) + assert rcNorm == {0: 1.0, 1: 1.0} + + +def test_richclub_normalized(): + G = nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (4, 5)]) + rcNorm = nx.richclub.rich_club_coefficient(G, Q=2, seed=42) + assert rcNorm == {0: 1.0, 1: 1.0} + + +def test_richclub2(): + T = nx.balanced_tree(2, 10) + rc = nx.richclub.rich_club_coefficient(T, normalized=False) + assert rc == { + 0: 4092 / (2047 * 2046.0), + 1: (2044.0 / (1023 * 1022)), + 2: (2040.0 / (1022 * 1021)), + } + + +def test_richclub3(): + # tests edgecase + G = nx.karate_club_graph() + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == { + 0: 156.0 / 1122, + 1: 154.0 / 1056, + 2: 110.0 / 462, + 3: 78.0 / 240, + 4: 44.0 / 90, + 5: 22.0 / 42, + 6: 10.0 / 20, + 7: 10.0 / 20, + 8: 10.0 / 20, + 9: 6.0 / 12, + 10: 2.0 / 6, + 11: 2.0 / 6, + 12: 0.0, + 13: 0.0, + 14: 0.0, + 15: 0.0, + } + + +def test_richclub4(): + G = nx.Graph() + G.add_edges_from( + [(0, 1), (0, 2), (0, 3), (0, 4), (4, 5), (5, 9), (6, 9), (7, 9), (8, 9)] + ) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {0: 18 / 90.0, 1: 6 / 12.0, 2: 0.0, 3: 0.0} + + +def test_richclub_exception(): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.DiGraph() + nx.rich_club_coefficient(G) + + +def test_rich_club_exception2(): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.MultiGraph() + nx.rich_club_coefficient(G) + + +def test_rich_club_selfloop(): + G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + G.add_edge(1, 1) # self loop + G.add_edge(1, 2) + with pytest.raises( + Exception, + match="rich_club_coefficient is not implemented for graphs with self loops.", + ): + nx.rich_club_coefficient(G) + + +def test_rich_club_leq_3_nodes_unnormalized(): + # edgeless graphs upto 3 nodes + G = nx.Graph() + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {} + + for i in range(3): + G.add_node(i) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {} + + # 2 nodes, single edge + G = nx.Graph() + G.add_edge(0, 1) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {0: 1} + + # 3 nodes, single edge + G = nx.Graph() + G.add_nodes_from([0, 1, 2]) + G.add_edge(0, 1) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {0: 1} + + # 3 nodes, 2 edges + G.add_edge(1, 2) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {0: 2 / 3} + + # 3 nodes, 3 edges + G.add_edge(0, 2) + rc = nx.rich_club_coefficient(G, normalized=False) + assert rc == {0: 1, 1: 1} + + +def test_rich_club_leq_3_nodes_normalized(): + G = nx.Graph() + with pytest.raises( + nx.exception.NetworkXError, + match="Graph has fewer than four nodes", + ): + rc = nx.rich_club_coefficient(G, normalized=True) + + for i in range(3): + G.add_node(i) + with pytest.raises( + nx.exception.NetworkXError, + match="Graph has fewer than four nodes", + ): + rc = nx.rich_club_coefficient(G, normalized=True) + + +# def test_richclub2_normalized(): +# T = nx.balanced_tree(2,10) +# rcNorm = nx.richclub.rich_club_coefficient(T,Q=2) +# assert_true(rcNorm[0] ==1.0 and rcNorm[1] < 0.9 and rcNorm[2] < 0.9) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee0a1fd10bb25d57af5d355ec3ff91b2140fd0e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_similarity.py @@ -0,0 +1,1158 @@ +import pytest + +import networkx as nx +from networkx.algorithms.similarity import ( + graph_edit_distance, + optimal_edit_paths, + optimize_graph_edit_distance, +) +from networkx.generators.classic import ( + circular_ladder_graph, + cycle_graph, + path_graph, + wheel_graph, +) + + +@pytest.mark.parametrize("source", (10, "foo")) +def test_generate_random_paths_source_not_in_G(source): + pytest.importorskip("numpy") + G = nx.complete_graph(5) + # No exception at generator construction time + path_gen = nx.generate_random_paths(G, sample_size=3, source=source) + with pytest.raises(nx.NodeNotFound, match="Initial node.*not in G"): + next(path_gen) + + +@pytest.mark.filterwarnings("ignore::RuntimeWarning") +def test_generate_random_paths_with_isolated_nodes(): + pytest.importorskip("numpy") + G = nx.Graph() + G.add_nodes_from([0, 1, 2]) + G.add_edge(0, 1) + + # Connected source node + paths = list(nx.generate_random_paths(G, 2, path_length=2, source=0, seed=42)) + assert len(paths) == 2 + assert all(len(path) == 3 for path in paths) + assert all(path[0] == 0 for path in paths) + + # Isolated source node + path_gen = nx.generate_random_paths(G, 2, path_length=2, source=2, seed=42) + with pytest.raises(ValueError, match="probabilities contain NaN"): + list(path_gen) + + # Random source that might pick isolated node + path_gen = nx.generate_random_paths(G, 2, path_length=2, seed=42) + with pytest.raises(ValueError, match="probabilities contain NaN"): + list(path_gen) + + +def nmatch(n1, n2): + return n1 == n2 + + +def ematch(e1, e2): + return e1 == e2 + + +def getCanonical(): + G = nx.Graph() + G.add_node("A", label="A") + G.add_node("B", label="B") + G.add_node("C", label="C") + G.add_node("D", label="D") + G.add_edge("A", "B", label="a-b") + G.add_edge("B", "C", label="b-c") + G.add_edge("B", "D", label="b-d") + return G + + +class TestSimilarity: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + pytest.importorskip("scipy") + + def test_graph_edit_distance_roots_and_timeout(self): + G0 = nx.star_graph(5) + G1 = G0.copy() + pytest.raises(ValueError, graph_edit_distance, G0, G1, roots=[2]) + pytest.raises(ValueError, graph_edit_distance, G0, G1, roots=[2, 3, 4]) + pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(9, 3)) + pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(3, 9)) + pytest.raises(nx.NodeNotFound, graph_edit_distance, G0, G1, roots=(9, 9)) + assert graph_edit_distance(G0, G1, roots=(1, 2)) == 0 + assert graph_edit_distance(G0, G1, roots=(0, 1)) == 8 + assert graph_edit_distance(G0, G1, roots=(1, 2), timeout=5) == 0 + assert graph_edit_distance(G0, G1, roots=(0, 1), timeout=5) == 8 + assert graph_edit_distance(G0, G1, roots=(0, 1), timeout=0.0001) is None + # test raise on 0 timeout + pytest.raises(nx.NetworkXError, graph_edit_distance, G0, G1, timeout=0) + + def test_graph_edit_distance(self): + G0 = nx.Graph() + G1 = path_graph(6) + G2 = cycle_graph(6) + G3 = wheel_graph(7) + + assert graph_edit_distance(G0, G0) == 0 + assert graph_edit_distance(G0, G1) == 11 + assert graph_edit_distance(G1, G0) == 11 + assert graph_edit_distance(G0, G2) == 12 + assert graph_edit_distance(G2, G0) == 12 + assert graph_edit_distance(G0, G3) == 19 + assert graph_edit_distance(G3, G0) == 19 + + assert graph_edit_distance(G1, G1) == 0 + assert graph_edit_distance(G1, G2) == 1 + assert graph_edit_distance(G2, G1) == 1 + assert graph_edit_distance(G1, G3) == 8 + assert graph_edit_distance(G3, G1) == 8 + + assert graph_edit_distance(G2, G2) == 0 + assert graph_edit_distance(G2, G3) == 7 + assert graph_edit_distance(G3, G2) == 7 + + assert graph_edit_distance(G3, G3) == 0 + + def test_graph_edit_distance_node_match(self): + G1 = cycle_graph(5) + G2 = cycle_graph(5) + for n, attr in G1.nodes.items(): + attr["color"] = "red" if n % 2 == 0 else "blue" + for n, attr in G2.nodes.items(): + attr["color"] = "red" if n % 2 == 1 else "blue" + assert graph_edit_distance(G1, G2) == 0 + assert ( + graph_edit_distance( + G1, G2, node_match=lambda n1, n2: n1["color"] == n2["color"] + ) + == 1 + ) + + def test_graph_edit_distance_edge_match(self): + G1 = path_graph(6) + G2 = path_graph(6) + for e, attr in G1.edges.items(): + attr["color"] = "red" if min(e) % 2 == 0 else "blue" + for e, attr in G2.edges.items(): + attr["color"] = "red" if min(e) // 3 == 0 else "blue" + assert graph_edit_distance(G1, G2) == 0 + assert ( + graph_edit_distance( + G1, G2, edge_match=lambda e1, e2: e1["color"] == e2["color"] + ) + == 2 + ) + + def test_graph_edit_distance_node_cost(self): + G1 = path_graph(6) + G2 = path_graph(6) + for n, attr in G1.nodes.items(): + attr["color"] = "red" if n % 2 == 0 else "blue" + for n, attr in G2.nodes.items(): + attr["color"] = "red" if n % 2 == 1 else "blue" + + def node_subst_cost(uattr, vattr): + if uattr["color"] == vattr["color"]: + return 1 + else: + return 10 + + def node_del_cost(attr): + if attr["color"] == "blue": + return 20 + else: + return 50 + + def node_ins_cost(attr): + if attr["color"] == "blue": + return 40 + else: + return 100 + + assert ( + graph_edit_distance( + G1, + G2, + node_subst_cost=node_subst_cost, + node_del_cost=node_del_cost, + node_ins_cost=node_ins_cost, + ) + == 6 + ) + + def test_graph_edit_distance_edge_cost(self): + G1 = path_graph(6) + G2 = path_graph(6) + for e, attr in G1.edges.items(): + attr["color"] = "red" if min(e) % 2 == 0 else "blue" + for e, attr in G2.edges.items(): + attr["color"] = "red" if min(e) // 3 == 0 else "blue" + + def edge_subst_cost(gattr, hattr): + if gattr["color"] == hattr["color"]: + return 0.01 + else: + return 0.1 + + def edge_del_cost(attr): + if attr["color"] == "blue": + return 0.2 + else: + return 0.5 + + def edge_ins_cost(attr): + if attr["color"] == "blue": + return 0.4 + else: + return 1.0 + + assert ( + graph_edit_distance( + G1, + G2, + edge_subst_cost=edge_subst_cost, + edge_del_cost=edge_del_cost, + edge_ins_cost=edge_ins_cost, + ) + == 0.23 + ) + + def test_graph_edit_distance_upper_bound(self): + G1 = circular_ladder_graph(2) + G2 = circular_ladder_graph(6) + assert graph_edit_distance(G1, G2, upper_bound=5) is None + assert graph_edit_distance(G1, G2, upper_bound=24) == 22 + assert graph_edit_distance(G1, G2) == 22 + + def test_optimal_edit_paths(self): + G1 = path_graph(3) + G2 = cycle_graph(3) + paths, cost = optimal_edit_paths(G1, G2) + assert cost == 1 + assert len(paths) == 6 + + def canonical(vertex_path, edge_path): + return ( + tuple(sorted(vertex_path)), + tuple(sorted(edge_path, key=lambda x: (None in x, x))), + ) + + expected_paths = [ + ( + [(0, 0), (1, 1), (2, 2)], + [((0, 1), (0, 1)), ((1, 2), (1, 2)), (None, (0, 2))], + ), + ( + [(0, 0), (1, 2), (2, 1)], + [((0, 1), (0, 2)), ((1, 2), (1, 2)), (None, (0, 1))], + ), + ( + [(0, 1), (1, 0), (2, 2)], + [((0, 1), (0, 1)), ((1, 2), (0, 2)), (None, (1, 2))], + ), + ( + [(0, 1), (1, 2), (2, 0)], + [((0, 1), (1, 2)), ((1, 2), (0, 2)), (None, (0, 1))], + ), + ( + [(0, 2), (1, 0), (2, 1)], + [((0, 1), (0, 2)), ((1, 2), (0, 1)), (None, (1, 2))], + ), + ( + [(0, 2), (1, 1), (2, 0)], + [((0, 1), (1, 2)), ((1, 2), (0, 1)), (None, (0, 2))], + ), + ] + assert {canonical(*p) for p in paths} == {canonical(*p) for p in expected_paths} + + def test_optimize_graph_edit_distance(self): + G1 = circular_ladder_graph(2) + G2 = circular_ladder_graph(6) + bestcost = 1000 + for cost in optimize_graph_edit_distance(G1, G2): + assert cost < bestcost + bestcost = cost + assert bestcost == 22 + + # def test_graph_edit_distance_bigger(self): + # G1 = circular_ladder_graph(12) + # G2 = circular_ladder_graph(16) + # assert_equal(graph_edit_distance(G1, G2), 22) + + def test_selfloops(self): + G0 = nx.Graph() + G1 = nx.Graph() + G1.add_edges_from((("A", "A"), ("A", "B"))) + G2 = nx.Graph() + G2.add_edges_from((("A", "B"), ("B", "B"))) + G3 = nx.Graph() + G3.add_edges_from((("A", "A"), ("A", "B"), ("B", "B"))) + + assert graph_edit_distance(G0, G0) == 0 + assert graph_edit_distance(G0, G1) == 4 + assert graph_edit_distance(G1, G0) == 4 + assert graph_edit_distance(G0, G2) == 4 + assert graph_edit_distance(G2, G0) == 4 + assert graph_edit_distance(G0, G3) == 5 + assert graph_edit_distance(G3, G0) == 5 + + assert graph_edit_distance(G1, G1) == 0 + assert graph_edit_distance(G1, G2) == 0 + assert graph_edit_distance(G2, G1) == 0 + assert graph_edit_distance(G1, G3) == 1 + assert graph_edit_distance(G3, G1) == 1 + + assert graph_edit_distance(G2, G2) == 0 + assert graph_edit_distance(G2, G3) == 1 + assert graph_edit_distance(G3, G2) == 1 + + assert graph_edit_distance(G3, G3) == 0 + + def test_digraph(self): + G0 = nx.DiGraph() + G1 = nx.DiGraph() + G1.add_edges_from((("A", "B"), ("B", "C"), ("C", "D"), ("D", "A"))) + G2 = nx.DiGraph() + G2.add_edges_from((("A", "B"), ("B", "C"), ("C", "D"), ("A", "D"))) + G3 = nx.DiGraph() + G3.add_edges_from((("A", "B"), ("A", "C"), ("B", "D"), ("C", "D"))) + + assert graph_edit_distance(G0, G0) == 0 + assert graph_edit_distance(G0, G1) == 8 + assert graph_edit_distance(G1, G0) == 8 + assert graph_edit_distance(G0, G2) == 8 + assert graph_edit_distance(G2, G0) == 8 + assert graph_edit_distance(G0, G3) == 8 + assert graph_edit_distance(G3, G0) == 8 + + assert graph_edit_distance(G1, G1) == 0 + assert graph_edit_distance(G1, G2) == 2 + assert graph_edit_distance(G2, G1) == 2 + assert graph_edit_distance(G1, G3) == 4 + assert graph_edit_distance(G3, G1) == 4 + + assert graph_edit_distance(G2, G2) == 0 + assert graph_edit_distance(G2, G3) == 2 + assert graph_edit_distance(G3, G2) == 2 + + assert graph_edit_distance(G3, G3) == 0 + + def test_multigraph(self): + G0 = nx.MultiGraph() + G1 = nx.MultiGraph() + G1.add_edges_from((("A", "B"), ("B", "C"), ("A", "C"))) + G2 = nx.MultiGraph() + G2.add_edges_from((("A", "B"), ("B", "C"), ("B", "C"), ("A", "C"))) + G3 = nx.MultiGraph() + G3.add_edges_from((("A", "B"), ("B", "C"), ("A", "C"), ("A", "C"), ("A", "C"))) + + assert graph_edit_distance(G0, G0) == 0 + assert graph_edit_distance(G0, G1) == 6 + assert graph_edit_distance(G1, G0) == 6 + assert graph_edit_distance(G0, G2) == 7 + assert graph_edit_distance(G2, G0) == 7 + assert graph_edit_distance(G0, G3) == 8 + assert graph_edit_distance(G3, G0) == 8 + + assert graph_edit_distance(G1, G1) == 0 + assert graph_edit_distance(G1, G2) == 1 + assert graph_edit_distance(G2, G1) == 1 + assert graph_edit_distance(G1, G3) == 2 + assert graph_edit_distance(G3, G1) == 2 + + assert graph_edit_distance(G2, G2) == 0 + assert graph_edit_distance(G2, G3) == 1 + assert graph_edit_distance(G3, G2) == 1 + + assert graph_edit_distance(G3, G3) == 0 + + def test_multidigraph(self): + G1 = nx.MultiDiGraph() + G1.add_edges_from( + ( + ("hardware", "kernel"), + ("kernel", "hardware"), + ("kernel", "userspace"), + ("userspace", "kernel"), + ) + ) + G2 = nx.MultiDiGraph() + G2.add_edges_from( + ( + ("winter", "spring"), + ("spring", "summer"), + ("summer", "autumn"), + ("autumn", "winter"), + ) + ) + + assert graph_edit_distance(G1, G2) == 5 + assert graph_edit_distance(G2, G1) == 5 + + # by https://github.com/jfbeaumont + def testCopy(self): + G = nx.Graph() + G.add_node("A", label="A") + G.add_node("B", label="B") + G.add_edge("A", "B", label="a-b") + assert ( + graph_edit_distance(G, G.copy(), node_match=nmatch, edge_match=ematch) == 0 + ) + + def testSame(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_edge("A", "B", label="a-b") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 0 + + def testOneEdgeLabelDiff(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_edge("A", "B", label="bad") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1 + + def testOneNodeLabelDiff(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="Z") + G2.add_node("B", label="B") + G2.add_edge("A", "B", label="a-b") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1 + + def testOneExtraNode(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_edge("A", "B", label="a-b") + G2.add_node("C", label="C") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1 + + def testOneExtraEdge(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_node("C", label="C") + G1.add_node("C", label="C") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("A", "C", label="a-c") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1 + + def testOneExtraNodeAndEdge(self): + G1 = nx.Graph() + G1.add_node("A", label="A") + G1.add_node("B", label="B") + G1.add_edge("A", "B", label="a-b") + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("A", "C", label="a-c") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2 + + def testGraph1(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("D", label="D") + G2.add_node("E", label="E") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("B", "D", label="b-d") + G2.add_edge("D", "E", label="d-e") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 3 + + def testGraph2(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_node("D", label="D") + G2.add_node("E", label="E") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("B", "C", label="b-c") + G2.add_edge("C", "D", label="c-d") + G2.add_edge("C", "E", label="c-e") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 4 + + def testGraph3(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_node("D", label="D") + G2.add_node("E", label="E") + G2.add_node("F", label="F") + G2.add_node("G", label="G") + G2.add_edge("A", "C", label="a-c") + G2.add_edge("A", "D", label="a-d") + G2.add_edge("D", "E", label="d-e") + G2.add_edge("D", "F", label="d-f") + G2.add_edge("D", "G", label="d-g") + G2.add_edge("E", "B", label="e-b") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 12 + + def testGraph4(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_node("D", label="D") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("B", "C", label="b-c") + G2.add_edge("C", "D", label="c-d") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2 + + def testGraph4_a(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_node("D", label="D") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("B", "C", label="b-c") + G2.add_edge("A", "D", label="a-d") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 2 + + def testGraph4_b(self): + G1 = getCanonical() + G2 = nx.Graph() + G2.add_node("A", label="A") + G2.add_node("B", label="B") + G2.add_node("C", label="C") + G2.add_node("D", label="D") + G2.add_edge("A", "B", label="a-b") + G2.add_edge("B", "C", label="b-c") + G2.add_edge("B", "D", label="bad") + assert graph_edit_distance(G1, G2, node_match=nmatch, edge_match=ematch) == 1 + + # note: nx.simrank_similarity_numpy not included because returns np.array + simrank_algs = [ + nx.simrank_similarity, + nx.algorithms.similarity._simrank_similarity_python, + ] + + @pytest.mark.parametrize("simrank_similarity", simrank_algs) + def test_simrank_no_source_no_target(self, simrank_similarity): + G = nx.cycle_graph(5) + expected = { + 0: { + 0: 1, + 1: 0.3951219505902448, + 2: 0.5707317069281646, + 3: 0.5707317069281646, + 4: 0.3951219505902449, + }, + 1: { + 0: 0.3951219505902448, + 1: 1, + 2: 0.3951219505902449, + 3: 0.5707317069281646, + 4: 0.5707317069281646, + }, + 2: { + 0: 0.5707317069281646, + 1: 0.3951219505902449, + 2: 1, + 3: 0.3951219505902449, + 4: 0.5707317069281646, + }, + 3: { + 0: 0.5707317069281646, + 1: 0.5707317069281646, + 2: 0.3951219505902449, + 3: 1, + 4: 0.3951219505902449, + }, + 4: { + 0: 0.3951219505902449, + 1: 0.5707317069281646, + 2: 0.5707317069281646, + 3: 0.3951219505902449, + 4: 1, + }, + } + actual = simrank_similarity(G) + for k, v in expected.items(): + assert v == pytest.approx(actual[k], abs=1e-2) + + # For a DiGraph test, use the first graph from the paper cited in + # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126 + G = nx.DiGraph() + G.add_node(0, label="Univ") + G.add_node(1, label="ProfA") + G.add_node(2, label="ProfB") + G.add_node(3, label="StudentA") + G.add_node(4, label="StudentB") + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)]) + + expected = { + 0: {0: 1, 1: 0.0, 2: 0.1323363991265798, 3: 0.0, 4: 0.03387811817640443}, + 1: {0: 0.0, 1: 1, 2: 0.4135512472705618, 3: 0.0, 4: 0.10586911930126384}, + 2: { + 0: 0.1323363991265798, + 1: 0.4135512472705618, + 2: 1, + 3: 0.04234764772050554, + 4: 0.08822426608438655, + }, + 3: {0: 0.0, 1: 0.0, 2: 0.04234764772050554, 3: 1, 4: 0.3308409978164495}, + 4: { + 0: 0.03387811817640443, + 1: 0.10586911930126384, + 2: 0.08822426608438655, + 3: 0.3308409978164495, + 4: 1, + }, + } + # Use the importance_factor from the paper to get the same numbers. + actual = simrank_similarity(G, importance_factor=0.8) + for k, v in expected.items(): + assert v == pytest.approx(actual[k], abs=1e-2) + + @pytest.mark.parametrize("simrank_similarity", simrank_algs) + def test_simrank_source_no_target(self, simrank_similarity): + G = nx.cycle_graph(5) + expected = { + 0: 1, + 1: 0.3951219505902448, + 2: 0.5707317069281646, + 3: 0.5707317069281646, + 4: 0.3951219505902449, + } + actual = simrank_similarity(G, source=0) + assert expected == pytest.approx(actual, abs=1e-2) + + # For a DiGraph test, use the first graph from the paper cited in + # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126 + G = nx.DiGraph() + G.add_node(0, label="Univ") + G.add_node(1, label="ProfA") + G.add_node(2, label="ProfB") + G.add_node(3, label="StudentA") + G.add_node(4, label="StudentB") + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)]) + + expected = {0: 1, 1: 0.0, 2: 0.1323363991265798, 3: 0.0, 4: 0.03387811817640443} + # Use the importance_factor from the paper to get the same numbers. + actual = simrank_similarity(G, importance_factor=0.8, source=0) + assert expected == pytest.approx(actual, abs=1e-2) + + @pytest.mark.parametrize("simrank_similarity", simrank_algs) + def test_simrank_noninteger_nodes(self, simrank_similarity): + G = nx.cycle_graph(5) + G = nx.relabel_nodes(G, dict(enumerate("abcde"))) + expected = { + "a": 1, + "b": 0.3951219505902448, + "c": 0.5707317069281646, + "d": 0.5707317069281646, + "e": 0.3951219505902449, + } + actual = simrank_similarity(G, source="a") + assert expected == pytest.approx(actual, abs=1e-2) + + # For a DiGraph test, use the first graph from the paper cited in + # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126 + G = nx.DiGraph() + G.add_node(0, label="Univ") + G.add_node(1, label="ProfA") + G.add_node(2, label="ProfB") + G.add_node(3, label="StudentA") + G.add_node(4, label="StudentB") + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)]) + node_labels = dict(enumerate(nx.get_node_attributes(G, "label").values())) + G = nx.relabel_nodes(G, node_labels) + + expected = { + "Univ": 1, + "ProfA": 0.0, + "ProfB": 0.1323363991265798, + "StudentA": 0.0, + "StudentB": 0.03387811817640443, + } + # Use the importance_factor from the paper to get the same numbers. + actual = simrank_similarity(G, importance_factor=0.8, source="Univ") + assert expected == pytest.approx(actual, abs=1e-2) + + @pytest.mark.parametrize("simrank_similarity", simrank_algs) + def test_simrank_source_and_target(self, simrank_similarity): + G = nx.cycle_graph(5) + expected = 1 + actual = simrank_similarity(G, source=0, target=0) + assert expected == pytest.approx(actual, abs=1e-2) + + # For a DiGraph test, use the first graph from the paper cited in + # the docs: https://dl.acm.org/doi/pdf/10.1145/775047.775126 + G = nx.DiGraph() + G.add_node(0, label="Univ") + G.add_node(1, label="ProfA") + G.add_node(2, label="ProfB") + G.add_node(3, label="StudentA") + G.add_node(4, label="StudentB") + G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 4), (4, 2), (3, 0)]) + + expected = 0.1323363991265798 + # Use the importance_factor from the paper to get the same numbers. + # Use the pair (0,2) because (0,0) and (0,1) have trivial results. + actual = simrank_similarity(G, importance_factor=0.8, source=0, target=2) + assert expected == pytest.approx(actual, abs=1e-5) + + @pytest.mark.parametrize("alg", simrank_algs) + def test_simrank_max_iterations(self, alg): + G = nx.cycle_graph(5) + pytest.raises(nx.ExceededMaxIterations, alg, G, max_iterations=10) + + def test_simrank_source_not_found(self): + G = nx.cycle_graph(5) + with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"): + nx.simrank_similarity(G, source=10) + + def test_simrank_target_not_found(self): + G = nx.cycle_graph(5) + with pytest.raises(nx.NodeNotFound, match="Target node 10 not in G"): + nx.simrank_similarity(G, target=10) + + def test_simrank_between_versions(self): + G = nx.cycle_graph(5) + # _python tolerance 1e-4 + expected_python_tol4 = { + 0: 1, + 1: 0.394512499239852, + 2: 0.5703550452791322, + 3: 0.5703550452791323, + 4: 0.394512499239852, + } + # _numpy tolerance 1e-4 + expected_numpy_tol4 = { + 0: 1.0, + 1: 0.3947180735764555, + 2: 0.570482097206368, + 3: 0.570482097206368, + 4: 0.3947180735764555, + } + actual = nx.simrank_similarity(G, source=0) + assert expected_numpy_tol4 == pytest.approx(actual, abs=1e-7) + # versions differ at 1e-4 level but equal at 1e-3 + assert expected_python_tol4 != pytest.approx(actual, abs=1e-4) + assert expected_python_tol4 == pytest.approx(actual, abs=1e-3) + + actual = nx.similarity._simrank_similarity_python(G, source=0) + assert expected_python_tol4 == pytest.approx(actual, abs=1e-7) + # versions differ at 1e-4 level but equal at 1e-3 + assert expected_numpy_tol4 != pytest.approx(actual, abs=1e-4) + assert expected_numpy_tol4 == pytest.approx(actual, abs=1e-3) + + def test_simrank_numpy_no_source_no_target(self): + G = nx.cycle_graph(5) + expected = np.array( + [ + [ + 1.0, + 0.3947180735764555, + 0.570482097206368, + 0.570482097206368, + 0.3947180735764555, + ], + [ + 0.3947180735764555, + 1.0, + 0.3947180735764555, + 0.570482097206368, + 0.570482097206368, + ], + [ + 0.570482097206368, + 0.3947180735764555, + 1.0, + 0.3947180735764555, + 0.570482097206368, + ], + [ + 0.570482097206368, + 0.570482097206368, + 0.3947180735764555, + 1.0, + 0.3947180735764555, + ], + [ + 0.3947180735764555, + 0.570482097206368, + 0.570482097206368, + 0.3947180735764555, + 1.0, + ], + ] + ) + actual = nx.similarity._simrank_similarity_numpy(G) + np.testing.assert_allclose(expected, actual, atol=1e-7) + + def test_simrank_numpy_source_no_target(self): + G = nx.cycle_graph(5) + expected = np.array( + [ + 1.0, + 0.3947180735764555, + 0.570482097206368, + 0.570482097206368, + 0.3947180735764555, + ] + ) + actual = nx.similarity._simrank_similarity_numpy(G, source=0) + np.testing.assert_allclose(expected, actual, atol=1e-7) + + def test_simrank_numpy_source_and_target(self): + G = nx.cycle_graph(5) + expected = 1.0 + actual = nx.similarity._simrank_similarity_numpy(G, source=0, target=0) + np.testing.assert_allclose(expected, actual, atol=1e-7) + + def test_panther_similarity_unweighted(self): + G = nx.Graph() + G.add_edge(0, 1) + G.add_edge(0, 2) + G.add_edge(0, 3) + G.add_edge(1, 2) + G.add_edge(2, 4) + expected = {3: 0.5, 2: 0.5, 1: 0.5, 4: 0.125} + sim = nx.panther_similarity(G, 0, path_length=2, seed=42) + assert sim == expected + + def test_panther_similarity_weighted(self): + G = nx.Graph() + G.add_edge("v1", "v2", w=5) + G.add_edge("v1", "v3", w=1) + G.add_edge("v1", "v4", w=2) + G.add_edge("v2", "v3", w=0.1) + G.add_edge("v3", "v5", w=1) + expected = {"v3": 0.75, "v4": 0.5, "v2": 0.5, "v5": 0.25} + sim = nx.panther_similarity(G, "v1", path_length=2, weight="w", seed=42) + assert sim == expected + + def test_panther_similarity_source_not_found(self): + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"): + nx.panther_similarity(G, source=10) + + def test_panther_similarity_isolated(self): + G = nx.Graph() + G.add_nodes_from(range(5)) + with pytest.raises( + nx.NetworkXUnfeasible, + match="Panther similarity is not defined for the isolated source node 1.", + ): + nx.panther_similarity(G, source=1) + + @pytest.mark.parametrize("num_paths", (1, 3, 10)) + @pytest.mark.parametrize("source", (0, 1)) + def test_generate_random_paths_with_start(self, num_paths, source): + G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + index_map = {} + + path_gen = nx.generate_random_paths( + G, + num_paths, + path_length=2, + index_map=index_map, + source=source, + ) + paths = list(path_gen) + + # There should be num_paths paths + assert len(paths) == num_paths + # And they should all start with `source` + assert all(p[0] == source for p in paths) + # The index_map for the `source` node should contain the indices for + # all of the generated paths. + assert sorted(index_map[source]) == list(range(num_paths)) + + def test_generate_random_paths_unweighted(self): + index_map = {} + num_paths = 10 + path_length = 2 + G = nx.Graph() + G.add_edge(0, 1) + G.add_edge(0, 2) + G.add_edge(0, 3) + G.add_edge(1, 2) + G.add_edge(2, 4) + paths = nx.generate_random_paths( + G, num_paths, path_length=path_length, index_map=index_map, seed=42 + ) + expected_paths = [ + [3, 0, 3], + [4, 2, 1], + [2, 1, 0], + [2, 0, 3], + [3, 0, 1], + [3, 0, 1], + [4, 2, 0], + [2, 1, 0], + [3, 0, 2], + [2, 1, 2], + ] + expected_map = { + 0: {0, 2, 3, 4, 5, 6, 7, 8}, + 1: {1, 2, 4, 5, 7, 9}, + 2: {1, 2, 3, 6, 7, 8, 9}, + 3: {0, 3, 4, 5, 8}, + 4: {1, 6}, + } + + assert expected_paths == list(paths) + assert expected_map == index_map + + def test_generate_random_paths_weighted(self): + index_map = {} + num_paths = 10 + path_length = 6 + G = nx.Graph() + G.add_edge("a", "b", weight=0.6) + G.add_edge("a", "c", weight=0.2) + G.add_edge("c", "d", weight=0.1) + G.add_edge("c", "e", weight=0.7) + G.add_edge("c", "f", weight=0.9) + G.add_edge("a", "d", weight=0.3) + paths = nx.generate_random_paths( + G, num_paths, path_length=path_length, index_map=index_map, seed=42 + ) + + expected_paths = [ + ["d", "c", "f", "c", "d", "a", "b"], + ["e", "c", "f", "c", "f", "c", "e"], + ["d", "a", "b", "a", "b", "a", "c"], + ["b", "a", "d", "a", "b", "a", "b"], + ["d", "a", "b", "a", "b", "a", "d"], + ["d", "a", "b", "a", "b", "a", "c"], + ["d", "a", "b", "a", "b", "a", "b"], + ["f", "c", "f", "c", "f", "c", "e"], + ["d", "a", "d", "a", "b", "a", "b"], + ["e", "c", "f", "c", "e", "c", "d"], + ] + expected_map = { + "d": {0, 2, 3, 4, 5, 6, 8, 9}, + "c": {0, 1, 2, 5, 7, 9}, + "f": {0, 1, 9, 7}, + "a": {0, 2, 3, 4, 5, 6, 8}, + "b": {0, 2, 3, 4, 5, 6, 8}, + "e": {1, 9, 7}, + } + + assert expected_paths == list(paths) + assert expected_map == index_map + + def test_one_node_one_loop_and_empty_graph(self): + G1 = nx.DiGraph([(0, 0)]) + G2 = nx.DiGraph() + assert nx.graph_edit_distance(G1, G2) == 2 + + def test_one_node_two_loops_and_empty_graph(self): + G1 = nx.MultiDiGraph([(0, 0), (0, 0)]) + assert nx.graph_edit_distance(G1, nx.DiGraph()) == 3 + assert nx.graph_edit_distance(G1, nx.MultiDiGraph()) == 3 + + def test_two_directed_loops(self): + G = nx.DiGraph([(0, 0), (1, 1)]) + assert nx.graph_edit_distance(G, nx.DiGraph()) == 4 + + def test_symmetry_with_custom_matching(self): + """G2 has edge (a,b) and G3 has edge (a,a) but node order for G2 is (a,b) + while for G3 it is (b,a)""" + + a, b = "A", "B" + G2 = nx.Graph() + G2.add_nodes_from((a, b)) + G2.add_edges_from([(a, b)]) + G3 = nx.Graph() + G3.add_nodes_from((b, a)) + G3.add_edges_from([(a, a)]) + for G in (G2, G3): + for n in G: + G.nodes[n]["attr"] = n + for e in G.edges: + G.edges[e]["attr"] = e + + def user_match(x, y): + return x == y + + assert ( + nx.graph_edit_distance(G2, G3, node_match=user_match, edge_match=user_match) + == 1 + ) + assert ( + nx.graph_edit_distance(G3, G2, node_match=user_match, edge_match=user_match) + == 1 + ) + + def test_panther_vector_similarity_basic(self): + """Basic test for panther_vector_similarity function.""" + G = nx.Graph() + G.add_edge(0, 1) + G.add_edge(0, 2) + G.add_edge(0, 3) + G.add_edge(1, 2) + G.add_edge(2, 4) + + sim = nx.panther_vector_similarity(G, 0, D=3, k=4, path_length=2, seed=42) + + assert len(sim) > 0 + assert 0 not in sim # Source node should not be included + assert all(node in [1, 2, 3, 4] for node in sim) # Only valid nodes + assert all(0 <= score <= 1 for score in sim.values()) # Valid scores + + def test_panther_vector_similarity_unweighted(self): + """Test panther_vector_similarity with unweighted graph.""" + G = nx.Graph() + G.add_edge(0, 1) + G.add_edge(0, 2) + G.add_edge(0, 3) + G.add_edge(1, 2) + G.add_edge(2, 4) + + sim = nx.panther_vector_similarity(G, 0, D=3, k=4, path_length=2, seed=42) + + assert len(sim) == 4 + assert 0 not in sim + assert all(node in sim for node in [1, 2, 3, 4]) + assert all(0 <= score <= 1 for score in sim.values()) + + def test_panther_vector_similarity_weighted(self): + """Test panther_vector_similarity with weighted graph.""" + G = nx.Graph() + G.add_edge("v1", "v2", weight=5) + G.add_edge("v1", "v3", weight=1) + G.add_edge("v1", "v4", weight=2) + G.add_edge("v2", "v3", weight=0.1) + G.add_edge("v3", "v5", weight=1) + + sim = nx.panther_vector_similarity( + G, "v1", D=3, k=4, path_length=2, weight="weight", seed=42 + ) + + assert len(sim) == 4 + assert "v1" not in sim + assert all(0 <= score <= 1 for score in sim.values()) + assert all(node in sim for node in ["v2", "v3", "v4"]) + + def test_panther_vector_similarity_source_not_found(self): + """Test panther_vector_similarity with non-existent source node.""" + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + + with pytest.raises(nx.NodeNotFound): + nx.panther_vector_similarity(G, source=10) + + def test_panther_vector_similarity_isolated(self): + """Test panther_vector_similarity with isolated source node.""" + G = nx.Graph() + G.add_nodes_from(range(5)) + G.add_edge(0, 1) + + with pytest.raises(nx.NetworkXUnfeasible): + nx.panther_vector_similarity(G, source=2) + + def test_panther_vector_similarity_too_large_D(self): + """Test raises when D > number of nodes.""" + G = nx.star_graph(3) + + with pytest.raises(nx.NetworkXUnfeasible): + nx.panther_vector_similarity(G, 0, D=5, k=3) + + def test_panther_vector_similarity_too_large_k(self): + """Test raises when k > number of nodes.""" + G = nx.star_graph(3) + + with pytest.raises(nx.NetworkXUnfeasible): + nx.panther_vector_similarity(G, 0, k=5) + + def test_panther_vector_similarity_small_graph(self): + """Test panther_vector_similarity with a very small graph.""" + G = nx.Graph() + G.add_edge(0, 1) + + sim = nx.panther_vector_similarity(G, 0, D=2, k=2, seed=42) + + assert len(sim) == 1 + assert 1 in sim + assert sim[1] > 0 + + def test_panther_vector_similarity_deterministic(self): + """Test that results are deterministic with fixed seed.""" + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + + sim1 = nx.panther_vector_similarity(G, 0, D=3, path_length=2, seed=42) + + sim2 = nx.panther_vector_similarity(G, 0, D=3, path_length=2, seed=42) + + assert sim1 == sim2 + + def test_panther_similarity_string_nodes(self): + """Test panther_similarity with string node names.""" + pytest.importorskip("numpy") + G = nx.Graph() + G.add_edges_from([("A", "B"), ("A", "C"), ("A", "D"), ("B", "C")]) + + sim = nx.panther_similarity(G, "A", k=2, path_length=2, seed=42) + + assert "A" not in sim # Source node should not be included + assert all(isinstance(node, str) for node in sim) # Nodes should remain strings + + def test_panther_vector_similarity_string_nodes(self): + """Test panther_vector_similarity with string node names.""" + pytest.importorskip("numpy") + G = nx.Graph() + G.add_edges_from([("A", "B"), ("A", "C"), ("A", "D"), ("B", "C")]) + + sim = nx.panther_vector_similarity(G, "A", D=3, k=2, path_length=2, seed=42) + + assert "A" not in sim # Source node should not be included + assert all(isinstance(node, str) for node in sim) # Nodes should remain strings + + def test_panther_similarity_k_parameter_returns_k_results(self): + pytest.importorskip("numpy") + G = nx.star_graph(100) + + for k_val in [1, 2, 3, 4, 5, 10]: + result_panther = nx.panther_similarity(G, source=1, k=k_val, seed=42) + assert len(result_panther) == k_val, ( + f"panther_similarity k={k_val} returned {len(result_panther)} results" + ) + assert 1 not in result_panther, "Source node should not be in results" + + result_vector = nx.panther_vector_similarity(G, source=1, k=k_val, seed=42) + assert len(result_vector) == k_val, ( + f"panther_vector_similarity k={k_val} returned {len(result_vector)} results" + ) + assert 1 not in result_vector, "Source node should not be in results" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py new file mode 100644 index 0000000000000000000000000000000000000000..7855bbad27b896750faa932a74062aa2bc8ca143 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_simple_paths.py @@ -0,0 +1,803 @@ +import random + +import pytest + +import networkx as nx +from networkx import convert_node_labels_to_integers as cnlti +from networkx.algorithms.simple_paths import ( + _bidirectional_dijkstra, + _bidirectional_shortest_path, +) +from networkx.utils import arbitrary_element, pairwise + + +class TestIsSimplePath: + """Unit tests for the + :func:`networkx.algorithms.simple_paths.is_simple_path` function. + + """ + + def test_empty_list(self): + """Tests that the empty list is not a valid path, since there + should be a one-to-one correspondence between paths as lists of + nodes and paths as lists of edges. + + """ + G = nx.trivial_graph() + assert not nx.is_simple_path(G, []) + + def test_trivial_path(self): + """Tests that the trivial path, a path of length one, is + considered a simple path in a graph. + + """ + G = nx.trivial_graph() + assert nx.is_simple_path(G, [0]) + + def test_trivial_nonpath(self): + """Tests that a list whose sole element is an object not in the + graph is not considered a simple path. + + """ + G = nx.trivial_graph() + assert not nx.is_simple_path(G, ["not a node"]) + + def test_simple_path(self): + G = nx.path_graph(2) + assert nx.is_simple_path(G, [0, 1]) + + def test_non_simple_path(self): + G = nx.path_graph(2) + assert not nx.is_simple_path(G, [0, 1, 0]) + + def test_cycle(self): + G = nx.cycle_graph(3) + assert not nx.is_simple_path(G, [0, 1, 2, 0]) + + def test_missing_node(self): + G = nx.path_graph(2) + assert not nx.is_simple_path(G, [0, 2]) + + def test_missing_starting_node(self): + G = nx.path_graph(2) + assert not nx.is_simple_path(G, [2, 0]) + + def test_directed_path(self): + G = nx.DiGraph([(0, 1), (1, 2)]) + assert nx.is_simple_path(G, [0, 1, 2]) + + def test_directed_non_path(self): + G = nx.DiGraph([(0, 1), (1, 2)]) + assert not nx.is_simple_path(G, [2, 1, 0]) + + def test_directed_cycle(self): + G = nx.DiGraph([(0, 1), (1, 2), (2, 0)]) + assert not nx.is_simple_path(G, [0, 1, 2, 0]) + + def test_multigraph(self): + G = nx.MultiGraph([(0, 1), (0, 1)]) + assert nx.is_simple_path(G, [0, 1]) + + def test_multidigraph(self): + G = nx.MultiDiGraph([(0, 1), (0, 1), (1, 0), (1, 0)]) + assert nx.is_simple_path(G, [0, 1]) + + +# Tests for all_simple_paths +def test_all_simple_paths(): + G = nx.path_graph(4) + paths = nx.all_simple_paths(G, 0, 3) + assert {tuple(p) for p in paths} == {(0, 1, 2, 3)} + + +def test_all_simple_paths_with_two_targets_emits_two_paths(): + G = nx.path_graph(4) + G.add_edge(2, 4) + paths = nx.all_simple_paths(G, 0, [3, 4]) + assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)} + + +def test_digraph_all_simple_paths_with_two_targets_emits_two_paths(): + G = nx.path_graph(4, create_using=nx.DiGraph()) + G.add_edge(2, 4) + paths = nx.all_simple_paths(G, 0, [3, 4]) + assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)} + + +def test_all_simple_paths_with_two_targets_cutoff(): + G = nx.path_graph(4) + G.add_edge(2, 4) + paths = nx.all_simple_paths(G, 0, [3, 4], cutoff=3) + assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)} + + +def test_digraph_all_simple_paths_with_two_targets_cutoff(): + G = nx.path_graph(4, create_using=nx.DiGraph()) + G.add_edge(2, 4) + paths = nx.all_simple_paths(G, 0, [3, 4], cutoff=3) + assert {tuple(p) for p in paths} == {(0, 1, 2, 3), (0, 1, 2, 4)} + + +def test_all_simple_paths_with_two_targets_in_line_emits_two_paths(): + G = nx.path_graph(4) + paths = nx.all_simple_paths(G, 0, [2, 3]) + assert {tuple(p) for p in paths} == {(0, 1, 2), (0, 1, 2, 3)} + + +def test_all_simple_paths_ignores_cycle(): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(1, 3) + paths = nx.all_simple_paths(G, 0, 3) + assert {tuple(p) for p in paths} == {(0, 1, 3)} + + +def test_all_simple_paths_with_two_targets_inside_cycle_emits_two_paths(): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(1, 3) + paths = nx.all_simple_paths(G, 0, [2, 3]) + assert {tuple(p) for p in paths} == {(0, 1, 2), (0, 1, 3)} + + +def test_all_simple_paths_source_target(): + G = nx.path_graph(4) + assert list(nx.all_simple_paths(G, 1, 1)) == [[1]] + + +def test_all_simple_paths_cutoff(): + G = nx.complete_graph(4) + paths = nx.all_simple_paths(G, 0, 1, cutoff=1) + assert {tuple(p) for p in paths} == {(0, 1)} + paths = nx.all_simple_paths(G, 0, 1, cutoff=2) + assert {tuple(p) for p in paths} == {(0, 1), (0, 2, 1), (0, 3, 1)} + + +def test_all_simple_paths_on_non_trivial_graph(): + """you may need to draw this graph to make sure it is reasonable""" + G = nx.path_graph(5, create_using=nx.DiGraph()) + G.add_edges_from([(0, 5), (1, 5), (1, 3), (5, 4), (4, 2), (4, 3)]) + paths = nx.all_simple_paths(G, 1, [2, 3]) + assert {tuple(p) for p in paths} == { + (1, 2), + (1, 3, 4, 2), + (1, 5, 4, 2), + (1, 3), + (1, 2, 3), + (1, 5, 4, 3), + (1, 5, 4, 2, 3), + } + paths = nx.all_simple_paths(G, 1, [2, 3], cutoff=3) + assert {tuple(p) for p in paths} == { + (1, 2), + (1, 3, 4, 2), + (1, 5, 4, 2), + (1, 3), + (1, 2, 3), + (1, 5, 4, 3), + } + paths = nx.all_simple_paths(G, 1, [2, 3], cutoff=2) + assert {tuple(p) for p in paths} == {(1, 2), (1, 3), (1, 2, 3)} + + +def test_all_simple_paths_multigraph(): + G = nx.MultiGraph([(1, 2), (1, 2)]) + assert list(nx.all_simple_paths(G, 1, 1)) == [[1]] + nx.add_path(G, [3, 1, 10, 2]) + paths = list(nx.all_simple_paths(G, 1, 2)) + assert len(paths) == 3 + assert {tuple(p) for p in paths} == {(1, 2), (1, 2), (1, 10, 2)} + + +def test_all_simple_paths_multigraph_with_cutoff(): + G = nx.MultiGraph([(1, 2), (1, 2), (1, 10), (10, 2)]) + paths = list(nx.all_simple_paths(G, 1, 2, cutoff=1)) + assert len(paths) == 2 + assert {tuple(p) for p in paths} == {(1, 2), (1, 2)} + + # See GitHub issue #6732. + G = nx.MultiGraph([(0, 1), (0, 2)]) + assert list(nx.all_simple_paths(G, 0, {1, 2}, cutoff=1)) == [[0, 1], [0, 2]] + + +def test_all_simple_paths_directed(): + G = nx.DiGraph() + nx.add_path(G, [1, 2, 3]) + nx.add_path(G, [3, 2, 1]) + paths = nx.all_simple_paths(G, 1, 3) + assert {tuple(p) for p in paths} == {(1, 2, 3)} + + +def test_all_simple_paths_empty(): + G = nx.path_graph(4) + paths = nx.all_simple_paths(G, 0, 3, cutoff=2) + assert list(paths) == [] + + +def test_all_simple_paths_corner_cases(): + assert list(nx.all_simple_paths(nx.empty_graph(2), 0, 0)) == [[0]] + assert list(nx.all_simple_paths(nx.empty_graph(2), 0, 1)) == [] + assert list(nx.all_simple_paths(nx.path_graph(9), 0, 8, 0)) == [] + + +def test_all_simple_paths_source_in_targets(): + # See GitHub issue #6690. + G = nx.path_graph(3) + assert list(nx.all_simple_paths(G, 0, {0, 1, 2})) == [[0], [0, 1], [0, 1, 2]] + + +def hamiltonian_path(G, source): + source = arbitrary_element(G) + neighbors = set(G[source]) - {source} + n = len(G) + for target in neighbors: + for path in nx.all_simple_paths(G, source, target): + if len(path) == n: + yield path + + +def test_hamiltonian_path(): + from itertools import permutations + + G = nx.complete_graph(4) + paths = [list(p) for p in hamiltonian_path(G, 0)] + exact = [[0] + list(p) for p in permutations([1, 2, 3], 3)] + assert sorted(paths) == sorted(exact) + + +def test_cutoff_zero(): + G = nx.complete_graph(4) + paths = nx.all_simple_paths(G, 0, 3, cutoff=0) + assert [list(p) for p in paths] == [] + paths = nx.all_simple_paths(nx.MultiGraph(G), 0, 3, cutoff=0) + assert [list(p) for p in paths] == [] + + +def test_source_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.all_simple_paths(nx.MultiGraph(G), 0, 3)) + + +def test_target_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.all_simple_paths(nx.MultiGraph(G), 1, 4)) + + +# Tests for all_simple_edge_paths +def test_all_simple_edge_paths(): + G = nx.path_graph(4) + paths = nx.all_simple_edge_paths(G, 0, 3) + assert {tuple(p) for p in paths} == {((0, 1), (1, 2), (2, 3))} + + +def test_all_simple_edge_paths_empty_path(): + G = nx.empty_graph(1) + assert list(nx.all_simple_edge_paths(G, 0, 0)) == [[]] + + +def test_all_simple_edge_paths_with_two_targets_emits_two_paths(): + G = nx.path_graph(4) + G.add_edge(2, 4) + paths = nx.all_simple_edge_paths(G, 0, [3, 4]) + assert {tuple(p) for p in paths} == { + ((0, 1), (1, 2), (2, 3)), + ((0, 1), (1, 2), (2, 4)), + } + + +def test_digraph_all_simple_edge_paths_with_two_targets_emits_two_paths(): + G = nx.path_graph(4, create_using=nx.DiGraph()) + G.add_edge(2, 4) + paths = nx.all_simple_edge_paths(G, 0, [3, 4]) + assert {tuple(p) for p in paths} == { + ((0, 1), (1, 2), (2, 3)), + ((0, 1), (1, 2), (2, 4)), + } + + +def test_all_simple_edge_paths_with_two_targets_cutoff(): + G = nx.path_graph(4) + G.add_edge(2, 4) + paths = nx.all_simple_edge_paths(G, 0, [3, 4], cutoff=3) + assert {tuple(p) for p in paths} == { + ((0, 1), (1, 2), (2, 3)), + ((0, 1), (1, 2), (2, 4)), + } + + +def test_digraph_all_simple_edge_paths_with_two_targets_cutoff(): + G = nx.path_graph(4, create_using=nx.DiGraph()) + G.add_edge(2, 4) + paths = nx.all_simple_edge_paths(G, 0, [3, 4], cutoff=3) + assert {tuple(p) for p in paths} == { + ((0, 1), (1, 2), (2, 3)), + ((0, 1), (1, 2), (2, 4)), + } + + +def test_all_simple_edge_paths_with_two_targets_in_line_emits_two_paths(): + G = nx.path_graph(4) + paths = nx.all_simple_edge_paths(G, 0, [2, 3]) + assert {tuple(p) for p in paths} == {((0, 1), (1, 2)), ((0, 1), (1, 2), (2, 3))} + + +def test_all_simple_edge_paths_ignores_cycle(): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(1, 3) + paths = nx.all_simple_edge_paths(G, 0, 3) + assert {tuple(p) for p in paths} == {((0, 1), (1, 3))} + + +def test_all_simple_edge_paths_with_two_targets_inside_cycle_emits_two_paths(): + G = nx.cycle_graph(3, create_using=nx.DiGraph()) + G.add_edge(1, 3) + paths = nx.all_simple_edge_paths(G, 0, [2, 3]) + assert {tuple(p) for p in paths} == {((0, 1), (1, 2)), ((0, 1), (1, 3))} + + +def test_all_simple_edge_paths_source_target(): + G = nx.path_graph(4) + paths = nx.all_simple_edge_paths(G, 1, 1) + assert list(paths) == [[]] + + +def test_all_simple_edge_paths_cutoff(): + G = nx.complete_graph(4) + paths = nx.all_simple_edge_paths(G, 0, 1, cutoff=1) + assert {tuple(p) for p in paths} == {((0, 1),)} + paths = nx.all_simple_edge_paths(G, 0, 1, cutoff=2) + assert {tuple(p) for p in paths} == {((0, 1),), ((0, 2), (2, 1)), ((0, 3), (3, 1))} + + +def test_all_simple_edge_paths_on_non_trivial_graph(): + """you may need to draw this graph to make sure it is reasonable""" + G = nx.path_graph(5, create_using=nx.DiGraph()) + G.add_edges_from([(0, 5), (1, 5), (1, 3), (5, 4), (4, 2), (4, 3)]) + paths = nx.all_simple_edge_paths(G, 1, [2, 3]) + assert {tuple(p) for p in paths} == { + ((1, 2),), + ((1, 3), (3, 4), (4, 2)), + ((1, 5), (5, 4), (4, 2)), + ((1, 3),), + ((1, 2), (2, 3)), + ((1, 5), (5, 4), (4, 3)), + ((1, 5), (5, 4), (4, 2), (2, 3)), + } + paths = nx.all_simple_edge_paths(G, 1, [2, 3], cutoff=3) + assert {tuple(p) for p in paths} == { + ((1, 2),), + ((1, 3), (3, 4), (4, 2)), + ((1, 5), (5, 4), (4, 2)), + ((1, 3),), + ((1, 2), (2, 3)), + ((1, 5), (5, 4), (4, 3)), + } + paths = nx.all_simple_edge_paths(G, 1, [2, 3], cutoff=2) + assert {tuple(p) for p in paths} == {((1, 2),), ((1, 3),), ((1, 2), (2, 3))} + + +def test_all_simple_edge_paths_multigraph(): + G = nx.MultiGraph([(1, 2), (1, 2)]) + paths = nx.all_simple_edge_paths(G, 1, 1) + assert list(paths) == [[]] + nx.add_path(G, [3, 1, 10, 2]) + paths = list(nx.all_simple_edge_paths(G, 1, 2)) + assert len(paths) == 3 + assert {tuple(p) for p in paths} == { + ((1, 2, 0),), + ((1, 2, 1),), + ((1, 10, 0), (10, 2, 0)), + } + + +def test_all_simple_edge_paths_multigraph_with_cutoff(): + G = nx.MultiGraph([(1, 2), (1, 2), (1, 10), (10, 2)]) + paths = list(nx.all_simple_edge_paths(G, 1, 2, cutoff=1)) + assert len(paths) == 2 + assert {tuple(p) for p in paths} == {((1, 2, 0),), ((1, 2, 1),)} + + +def test_all_simple_edge_paths_directed(): + G = nx.DiGraph() + nx.add_path(G, [1, 2, 3]) + nx.add_path(G, [3, 2, 1]) + paths = nx.all_simple_edge_paths(G, 1, 3) + assert {tuple(p) for p in paths} == {((1, 2), (2, 3))} + + +def test_all_simple_edge_paths_empty(): + G = nx.path_graph(4) + paths = nx.all_simple_edge_paths(G, 0, 3, cutoff=2) + assert list(paths) == [] + + +def test_all_simple_edge_paths_corner_cases(): + assert list(nx.all_simple_edge_paths(nx.empty_graph(2), 0, 0)) == [[]] + assert list(nx.all_simple_edge_paths(nx.empty_graph(2), 0, 1)) == [] + assert list(nx.all_simple_edge_paths(nx.path_graph(9), 0, 8, 0)) == [] + + +def test_all_simple_edge_paths_ignores_self_loop(): + G = nx.Graph([(0, 0), (0, 1), (1, 1), (1, 2)]) + assert list(nx.all_simple_edge_paths(G, 0, 2)) == [[(0, 1), (1, 2)]] + + +def hamiltonian_edge_path(G, source): + source = arbitrary_element(G) + neighbors = set(G[source]) - {source} + n = len(G) + for target in neighbors: + for path in nx.all_simple_edge_paths(G, source, target): + if len(path) == n - 1: + yield path + + +def test_hamiltonian__edge_path(): + from itertools import permutations + + G = nx.complete_graph(4) + paths = hamiltonian_edge_path(G, 0) + exact = [list(pairwise([0] + list(p))) for p in permutations([1, 2, 3], 3)] + assert sorted(exact) == sorted(paths) + + +def test_edge_cutoff_zero(): + G = nx.complete_graph(4) + paths = nx.all_simple_edge_paths(G, 0, 3, cutoff=0) + assert [list(p) for p in paths] == [] + paths = nx.all_simple_edge_paths(nx.MultiGraph(G), 0, 3, cutoff=0) + assert [list(p) for p in paths] == [] + + +def test_edge_source_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.all_simple_edge_paths(nx.MultiGraph(G), 0, 3)) + + +def test_edge_target_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.all_simple_edge_paths(nx.MultiGraph(G), 1, 4)) + + +# Tests for shortest_simple_paths +def test_shortest_simple_paths(): + G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted") + paths = nx.shortest_simple_paths(G, 1, 12) + assert next(paths) == [1, 2, 3, 4, 8, 12] + assert next(paths) == [1, 5, 6, 7, 8, 12] + assert [len(path) for path in nx.shortest_simple_paths(G, 1, 12)] == sorted( + len(path) for path in nx.all_simple_paths(G, 1, 12) + ) + + +def test_shortest_simple_paths_singleton_path(): + G = nx.empty_graph(3) + assert list(nx.shortest_simple_paths(G, 0, 0)) == [[0]] + + +def test_shortest_simple_paths_directed(): + G = nx.cycle_graph(7, create_using=nx.DiGraph()) + paths = nx.shortest_simple_paths(G, 0, 3) + assert list(paths) == [[0, 1, 2, 3]] + + +def test_shortest_simple_paths_directed_with_weight_function(): + def cost(u, v, x): + return 1 + + G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted") + paths = nx.shortest_simple_paths(G, 1, 12) + assert next(paths) == [1, 2, 3, 4, 8, 12] + assert next(paths) == [1, 5, 6, 7, 8, 12] + assert [ + len(path) for path in nx.shortest_simple_paths(G, 1, 12, weight=cost) + ] == sorted(len(path) for path in nx.all_simple_paths(G, 1, 12)) + + +def test_shortest_simple_paths_with_weight_function(): + def cost(u, v, x): + return 1 + + G = nx.cycle_graph(7, create_using=nx.DiGraph()) + paths = nx.shortest_simple_paths(G, 0, 3, weight=cost) + assert list(paths) == [[0, 1, 2, 3]] + + +def test_shortest_simple_paths_with_none_weight_function(): + def cost(u, v, x): + delta = abs(u - v) + # ignore interior edges + return 1 if (delta == 1 or delta == 4) else None + + G = nx.complete_graph(5) + paths = nx.shortest_simple_paths(G, 0, 2, weight=cost) + assert list(paths) == [[0, 1, 2], [0, 4, 3, 2]] + + +def test_Greg_Bernstein(): + g1 = nx.Graph() + g1.add_nodes_from(["N0", "N1", "N2", "N3", "N4"]) + g1.add_edge("N4", "N1", weight=10.0, capacity=50, name="L5") + g1.add_edge("N4", "N0", weight=7.0, capacity=40, name="L4") + g1.add_edge("N0", "N1", weight=10.0, capacity=45, name="L1") + g1.add_edge("N3", "N0", weight=10.0, capacity=50, name="L0") + g1.add_edge("N2", "N3", weight=12.0, capacity=30, name="L2") + g1.add_edge("N1", "N2", weight=15.0, capacity=42, name="L3") + solution = [["N1", "N0", "N3"], ["N1", "N2", "N3"], ["N1", "N4", "N0", "N3"]] + result = list(nx.shortest_simple_paths(g1, "N1", "N3", weight="weight")) + assert result == solution + + +def test_weighted_shortest_simple_path(): + def cost_func(path): + return sum(G.adj[u][v]["weight"] for (u, v) in zip(path, path[1:])) + + G = nx.complete_graph(5) + weight = {(u, v): random.randint(1, 100) for (u, v) in G.edges()} + nx.set_edge_attributes(G, weight, "weight") + cost = 0 + for path in nx.shortest_simple_paths(G, 0, 3, weight="weight"): + this_cost = cost_func(path) + assert cost <= this_cost + cost = this_cost + + +def test_directed_weighted_shortest_simple_path(): + def cost_func(path): + return sum(G.adj[u][v]["weight"] for (u, v) in zip(path, path[1:])) + + G = nx.complete_graph(5) + G = G.to_directed() + weight = {(u, v): random.randint(1, 100) for (u, v) in G.edges()} + nx.set_edge_attributes(G, weight, "weight") + cost = 0 + for path in nx.shortest_simple_paths(G, 0, 3, weight="weight"): + this_cost = cost_func(path) + assert cost <= this_cost + cost = this_cost + + +def test_weighted_shortest_simple_path_issue2427(): + G = nx.Graph() + G.add_edge("IN", "OUT", weight=2) + G.add_edge("IN", "A", weight=1) + G.add_edge("IN", "B", weight=2) + G.add_edge("B", "OUT", weight=2) + assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [ + ["IN", "OUT"], + ["IN", "B", "OUT"], + ] + G = nx.Graph() + G.add_edge("IN", "OUT", weight=10) + G.add_edge("IN", "A", weight=1) + G.add_edge("IN", "B", weight=1) + G.add_edge("B", "OUT", weight=1) + assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [ + ["IN", "B", "OUT"], + ["IN", "OUT"], + ] + + +def test_directed_weighted_shortest_simple_path_issue2427(): + G = nx.DiGraph() + G.add_edge("IN", "OUT", weight=2) + G.add_edge("IN", "A", weight=1) + G.add_edge("IN", "B", weight=2) + G.add_edge("B", "OUT", weight=2) + assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [ + ["IN", "OUT"], + ["IN", "B", "OUT"], + ] + G = nx.DiGraph() + G.add_edge("IN", "OUT", weight=10) + G.add_edge("IN", "A", weight=1) + G.add_edge("IN", "B", weight=1) + G.add_edge("B", "OUT", weight=1) + assert list(nx.shortest_simple_paths(G, "IN", "OUT", weight="weight")) == [ + ["IN", "B", "OUT"], + ["IN", "OUT"], + ] + + +def test_weight_name(): + G = nx.cycle_graph(7) + nx.set_edge_attributes(G, 1, "weight") + nx.set_edge_attributes(G, 1, "foo") + G.adj[1][2]["foo"] = 7 + paths = list(nx.shortest_simple_paths(G, 0, 3, weight="foo")) + solution = [[0, 6, 5, 4, 3], [0, 1, 2, 3]] + assert paths == solution + + +def test_ssp_source_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.shortest_simple_paths(G, 0, 3)) + + +def test_ssp_target_missing(): + with pytest.raises(nx.NodeNotFound): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + list(nx.shortest_simple_paths(G, 1, 4)) + + +def test_ssp_multigraph(): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.MultiGraph() + nx.add_path(G, [1, 2, 3]) + list(nx.shortest_simple_paths(G, 1, 4)) + + +def test_ssp_source_missing2(): + with pytest.raises(nx.NetworkXNoPath): + G = nx.Graph() + nx.add_path(G, [0, 1, 2]) + nx.add_path(G, [3, 4, 5]) + list(nx.shortest_simple_paths(G, 0, 3)) + + +def test_bidirectional_shortest_path_restricted_cycle(): + cycle = nx.cycle_graph(7) + length, path = _bidirectional_shortest_path(cycle, 0, 3) + assert path == [0, 1, 2, 3] + length, path = _bidirectional_shortest_path(cycle, 0, 3, ignore_nodes=[1]) + assert path == [0, 6, 5, 4, 3] + + +def test_bidirectional_shortest_path_restricted_wheel(): + wheel = nx.wheel_graph(6) + length, path = _bidirectional_shortest_path(wheel, 1, 3) + assert path in [[1, 0, 3], [1, 2, 3]] + length, path = _bidirectional_shortest_path(wheel, 1, 3, ignore_nodes=[0]) + assert path == [1, 2, 3] + length, path = _bidirectional_shortest_path(wheel, 1, 3, ignore_nodes=[0, 2]) + assert path == [1, 5, 4, 3] + length, path = _bidirectional_shortest_path( + wheel, 1, 3, ignore_edges=[(1, 0), (5, 0), (2, 3)] + ) + assert path in [[1, 2, 0, 3], [1, 5, 4, 3]] + + +def test_bidirectional_shortest_path_restricted_directed_cycle(): + directed_cycle = nx.cycle_graph(7, create_using=nx.DiGraph()) + length, path = _bidirectional_shortest_path(directed_cycle, 0, 3) + assert path == [0, 1, 2, 3] + pytest.raises( + nx.NetworkXNoPath, + _bidirectional_shortest_path, + directed_cycle, + 0, + 3, + ignore_nodes=[1], + ) + length, path = _bidirectional_shortest_path( + directed_cycle, 0, 3, ignore_edges=[(2, 1)] + ) + assert path == [0, 1, 2, 3] + pytest.raises( + nx.NetworkXNoPath, + _bidirectional_shortest_path, + directed_cycle, + 0, + 3, + ignore_edges=[(1, 2)], + ) + + +def test_bidirectional_shortest_path_ignore(): + G = nx.Graph() + nx.add_path(G, [1, 2]) + nx.add_path(G, [1, 3]) + nx.add_path(G, [1, 4]) + pytest.raises( + nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[1] + ) + pytest.raises( + nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[2] + ) + G = nx.Graph() + nx.add_path(G, [1, 3]) + nx.add_path(G, [1, 4]) + nx.add_path(G, [3, 2]) + pytest.raises( + nx.NetworkXNoPath, _bidirectional_shortest_path, G, 1, 2, ignore_nodes=[1, 2] + ) + + +def validate_path(G, s, t, soln_len, path): + assert path[0] == s + assert path[-1] == t + assert soln_len == sum( + G[u][v].get("weight", 1) for u, v in zip(path[:-1], path[1:]) + ) + + +def validate_length_path(G, s, t, soln_len, length, path): + assert soln_len == length + validate_path(G, s, t, length, path) + + +def test_bidirectional_dijkstra_restricted(): + XG = nx.DiGraph() + XG.add_weighted_edges_from( + [ + ("s", "u", 10), + ("s", "x", 5), + ("u", "v", 1), + ("u", "x", 2), + ("v", "y", 1), + ("x", "u", 3), + ("x", "v", 5), + ("x", "y", 2), + ("y", "s", 7), + ("y", "v", 6), + ] + ) + + XG3 = nx.Graph() + XG3.add_weighted_edges_from( + [[0, 1, 2], [1, 2, 12], [2, 3, 1], [3, 4, 5], [4, 5, 1], [5, 0, 10]] + ) + validate_length_path(XG, "s", "v", 9, *_bidirectional_dijkstra(XG, "s", "v")) + validate_length_path( + XG, "s", "v", 10, *_bidirectional_dijkstra(XG, "s", "v", ignore_nodes=["u"]) + ) + validate_length_path( + XG, + "s", + "v", + 11, + *_bidirectional_dijkstra(XG, "s", "v", ignore_edges=[("s", "x")]), + ) + pytest.raises( + nx.NetworkXNoPath, + _bidirectional_dijkstra, + XG, + "s", + "v", + ignore_nodes=["u"], + ignore_edges=[("s", "x")], + ) + validate_length_path(XG3, 0, 3, 15, *_bidirectional_dijkstra(XG3, 0, 3)) + validate_length_path( + XG3, 0, 3, 16, *_bidirectional_dijkstra(XG3, 0, 3, ignore_nodes=[1]) + ) + validate_length_path( + XG3, 0, 3, 16, *_bidirectional_dijkstra(XG3, 0, 3, ignore_edges=[(2, 3)]) + ) + pytest.raises( + nx.NetworkXNoPath, + _bidirectional_dijkstra, + XG3, + 0, + 3, + ignore_nodes=[1], + ignore_edges=[(5, 4)], + ) + + +def test_bidirectional_dijkstra_no_path(): + with pytest.raises(nx.NetworkXNoPath): + G = nx.Graph() + nx.add_path(G, [1, 2, 3]) + nx.add_path(G, [4, 5, 6]) + _bidirectional_dijkstra(G, 1, 6) + + +def test_bidirectional_dijkstra_ignore(): + G = nx.Graph() + nx.add_path(G, [1, 2, 10]) + nx.add_path(G, [1, 3, 10]) + pytest.raises(nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[1]) + pytest.raises(nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[2]) + pytest.raises( + nx.NetworkXNoPath, _bidirectional_dijkstra, G, 1, 2, ignore_nodes=[1, 2] + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py new file mode 100644 index 0000000000000000000000000000000000000000..c8e454293b4325a5c7557480182b1e1271a1118e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smallworld.py @@ -0,0 +1,76 @@ +import pytest + +pytest.importorskip("numpy") + +import networkx as nx +from networkx import lattice_reference, omega, random_reference, sigma + +rng = 42 + + +def test_random_reference(): + G = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng) + Gr = random_reference(G, niter=1, seed=rng) + C = nx.average_clustering(G) + Cr = nx.average_clustering(Gr) + assert C > Cr + + with pytest.raises(nx.NetworkXError): + next(random_reference(nx.Graph())) + with pytest.raises(nx.NetworkXNotImplemented): + next(random_reference(nx.DiGraph())) + + H = nx.Graph(((0, 1), (2, 3))) + Hl = random_reference(H, niter=1, seed=rng) + + +def test_lattice_reference(): + G = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng) + Gl = lattice_reference(G, niter=1, seed=rng) + L = nx.average_shortest_path_length(G) + Ll = nx.average_shortest_path_length(Gl) + assert Ll > L + + pytest.raises(nx.NetworkXError, lattice_reference, nx.Graph()) + pytest.raises(nx.NetworkXNotImplemented, lattice_reference, nx.DiGraph()) + + H = nx.Graph(((0, 1), (2, 3))) + Hl = lattice_reference(H, niter=1) + + +def test_sigma(): + Gs = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng) + Gr = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng) + sigmas = sigma(Gs, niter=1, nrand=2, seed=rng) + sigmar = sigma(Gr, niter=1, nrand=2, seed=rng) + assert sigmar < sigmas + + +def test_omega(): + Gl = nx.connected_watts_strogatz_graph(50, 6, 0, seed=rng) + Gr = nx.connected_watts_strogatz_graph(50, 6, 1, seed=rng) + Gs = nx.connected_watts_strogatz_graph(50, 6, 0.1, seed=rng) + omegal = omega(Gl, niter=1, nrand=1, seed=rng) + omegar = omega(Gr, niter=1, nrand=1, seed=rng) + omegas = omega(Gs, niter=1, nrand=1, seed=rng) + assert omegal < omegas and omegas < omegar + + # Test that omega lies within the [-1, 1] bounds + G_barbell = nx.barbell_graph(5, 1) + G_karate = nx.karate_club_graph() + + omega_barbell = nx.omega(G_barbell) + omega_karate = nx.omega(G_karate, nrand=2) + + omegas = (omegal, omegar, omegas, omega_barbell, omega_karate) + + for o in omegas: + assert -1 <= o <= 1 + + +@pytest.mark.parametrize("f", (nx.random_reference, nx.lattice_reference)) +def test_graph_no_edges(f): + G = nx.Graph() + G.add_nodes_from([0, 1, 2, 3]) + with pytest.raises(nx.NetworkXError, match="Graph has fewer that 2 edges"): + f(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py new file mode 100644 index 0000000000000000000000000000000000000000..528dbc8d69bf5dca221c17cd16118cf3ba01a2a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_smetric.py @@ -0,0 +1,8 @@ +import pytest + +import networkx as nx + + +def test_smetric(): + G = nx.Graph([(1, 2), (2, 3), (2, 4), (1, 4)]) + assert nx.s_metric(G) == 19.0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py new file mode 100644 index 0000000000000000000000000000000000000000..e8604e61ae45aca9092226a793a02b082b126738 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_sparsifiers.py @@ -0,0 +1,138 @@ +"""Unit tests for the sparsifier computation functions.""" + +import pytest + +import networkx as nx +from networkx.utils import py_random_state + +_seed = 2 + + +def _test_spanner(G, spanner, stretch, weight=None): + """Test whether a spanner is valid. + + This function tests whether the given spanner is a subgraph of the + given graph G with the same node set. It also tests for all shortest + paths whether they adhere to the given stretch. + + Parameters + ---------- + G : NetworkX graph + The original graph for which the spanner was constructed. + + spanner : NetworkX graph + The spanner to be tested. + + stretch : float + The proclaimed stretch of the spanner. + + weight : object + The edge attribute to use as distance. + """ + # check node set + assert set(G.nodes()) == set(spanner.nodes()) + + # check edge set and weights + for u, v in spanner.edges(): + assert G.has_edge(u, v) + if weight: + assert spanner[u][v][weight] == G[u][v][weight] + + # check connectivity and stretch + original_length = dict(nx.shortest_path_length(G, weight=weight)) + spanner_length = dict(nx.shortest_path_length(spanner, weight=weight)) + for u in G.nodes(): + for v in G.nodes(): + if u in original_length and v in original_length[u]: + assert spanner_length[u][v] <= stretch * original_length[u][v] + + +@py_random_state(1) +def _assign_random_weights(G, seed=None): + """Assigns random weights to the edges of a graph. + + Parameters + ---------- + + G : NetworkX graph + The original graph for which the spanner was constructed. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + """ + for u, v in G.edges(): + G[u][v]["weight"] = seed.random() + + +def test_spanner_trivial(): + """Test a trivial spanner with stretch 1.""" + G = nx.complete_graph(20) + spanner = nx.spanner(G, 1, seed=_seed) + + for u, v in G.edges: + assert spanner.has_edge(u, v) + + +def test_spanner_unweighted_complete_graph(): + """Test spanner construction on a complete unweighted graph.""" + G = nx.complete_graph(20) + + spanner = nx.spanner(G, 4, seed=_seed) + _test_spanner(G, spanner, 4) + + spanner = nx.spanner(G, 10, seed=_seed) + _test_spanner(G, spanner, 10) + + +def test_spanner_weighted_complete_graph(): + """Test spanner construction on a complete weighted graph.""" + G = nx.complete_graph(20) + _assign_random_weights(G, seed=_seed) + + spanner = nx.spanner(G, 4, weight="weight", seed=_seed) + _test_spanner(G, spanner, 4, weight="weight") + + spanner = nx.spanner(G, 10, weight="weight", seed=_seed) + _test_spanner(G, spanner, 10, weight="weight") + + +def test_spanner_unweighted_gnp_graph(): + """Test spanner construction on an unweighted gnp graph.""" + G = nx.gnp_random_graph(20, 0.4, seed=_seed) + + spanner = nx.spanner(G, 4, seed=_seed) + _test_spanner(G, spanner, 4) + + spanner = nx.spanner(G, 10, seed=_seed) + _test_spanner(G, spanner, 10) + + +def test_spanner_weighted_gnp_graph(): + """Test spanner construction on an weighted gnp graph.""" + G = nx.gnp_random_graph(20, 0.4, seed=_seed) + _assign_random_weights(G, seed=_seed) + + spanner = nx.spanner(G, 4, weight="weight", seed=_seed) + _test_spanner(G, spanner, 4, weight="weight") + + spanner = nx.spanner(G, 10, weight="weight", seed=_seed) + _test_spanner(G, spanner, 10, weight="weight") + + +def test_spanner_unweighted_disconnected_graph(): + """Test spanner construction on a disconnected graph.""" + G = nx.disjoint_union(nx.complete_graph(10), nx.complete_graph(10)) + + spanner = nx.spanner(G, 4, seed=_seed) + _test_spanner(G, spanner, 4) + + spanner = nx.spanner(G, 10, seed=_seed) + _test_spanner(G, spanner, 10) + + +def test_spanner_invalid_stretch(): + """Check whether an invalid stretch is caught.""" + with pytest.raises(ValueError): + G = nx.empty_graph() + nx.spanner(G, 0) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py new file mode 100644 index 0000000000000000000000000000000000000000..d0f53cd14e2bb4459197416932d3e297d4bde0f7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_structuralholes.py @@ -0,0 +1,191 @@ +"""Unit tests for the :mod:`networkx.algorithms.structuralholes` module.""" + +import math + +import pytest + +import networkx as nx +from networkx.classes.tests import dispatch_interface + + +class TestStructuralHolesNoScipy: + """Unit tests for computing measures of structural holes. + + The expected values for these functions were originally computed using the + proprietary software `UCINET`_ and the free software `IGraph`_ , and then + computed by hand to make sure that the results are correct. + + .. _UCINET: https://sites.google.com/site/ucinetsoftware/home + .. _IGraph: http://igraph.org/ + + """ + + def setup_method(self): + self.D = nx.DiGraph() + self.D.add_edges_from([(0, 1), (0, 2), (1, 0), (2, 1)]) + self.D_weights = {(0, 1): 2, (0, 2): 2, (1, 0): 1, (2, 1): 1} + # Example from http://www.analytictech.com/connections/v20(1)/holes.htm + self.G = nx.Graph() + self.G.add_edges_from( + [ + ("A", "B"), + ("A", "F"), + ("A", "G"), + ("A", "E"), + ("E", "G"), + ("F", "G"), + ("B", "G"), + ("B", "D"), + ("D", "G"), + ("G", "C"), + ] + ) + self.G_weights = { + ("A", "B"): 2, + ("A", "F"): 3, + ("A", "G"): 5, + ("A", "E"): 2, + ("E", "G"): 8, + ("F", "G"): 3, + ("B", "G"): 4, + ("B", "D"): 1, + ("D", "G"): 3, + ("G", "C"): 10, + } + self.Dnodes = list(self.D) + self.Gnodes = list(self.G) + + def test_constraint_directed(self): + constraint = nx.constraint(self.D, nodes=self.Dnodes) + assert constraint[0] == pytest.approx(1.003, abs=1e-3) + assert constraint[1] == pytest.approx(1.003, abs=1e-3) + assert constraint[2] == pytest.approx(1.389, abs=1e-3) + + def test_effective_size_directed(self): + effective_size = nx.effective_size(self.D, nodes=self.Dnodes) + assert effective_size[0] == pytest.approx(1.167, abs=1e-3) + assert effective_size[1] == pytest.approx(1.167, abs=1e-3) + assert effective_size[2] == pytest.approx(1, abs=1e-3) + + def test_constraint_weighted_directed(self): + D = self.D.copy() + nx.set_edge_attributes(D, self.D_weights, "weight") + constraint = nx.constraint(D, weight="weight", nodes=self.Dnodes) + assert constraint[0] == pytest.approx(0.840, abs=1e-3) + assert constraint[1] == pytest.approx(1.143, abs=1e-3) + assert constraint[2] == pytest.approx(1.378, abs=1e-3) + + def test_effective_size_weighted_directed(self): + D = self.D.copy() + nx.set_edge_attributes(D, self.D_weights, "weight") + effective_size = nx.effective_size(D, weight="weight", nodes=self.Dnodes) + assert effective_size[0] == pytest.approx(1.567, abs=1e-3) + assert effective_size[1] == pytest.approx(1.083, abs=1e-3) + assert effective_size[2] == pytest.approx(1, abs=1e-3) + + def test_constraint_undirected(self): + constraint = nx.constraint(self.G, nodes=self.Gnodes) + assert constraint["G"] == pytest.approx(0.400, abs=1e-3) + assert constraint["A"] == pytest.approx(0.595, abs=1e-3) + assert constraint["C"] == pytest.approx(1, abs=1e-3) + + def test_effective_size_undirected_borgatti(self): + effective_size = nx.effective_size(self.G, nodes=self.Gnodes) + assert effective_size["G"] == pytest.approx(4.67, abs=1e-2) + assert effective_size["A"] == pytest.approx(2.50, abs=1e-2) + assert effective_size["C"] == pytest.approx(1, abs=1e-2) + + def test_effective_size_undirected(self): + G = self.G.copy() + nx.set_edge_attributes(G, 1, "weight") + effective_size = nx.effective_size(G, weight="weight", nodes=self.Gnodes) + assert effective_size["G"] == pytest.approx(4.67, abs=1e-2) + assert effective_size["A"] == pytest.approx(2.50, abs=1e-2) + assert effective_size["C"] == pytest.approx(1, abs=1e-2) + + def test_constraint_weighted_undirected(self): + G = self.G.copy() + nx.set_edge_attributes(G, self.G_weights, "weight") + constraint = nx.constraint(G, weight="weight", nodes=self.Gnodes) + assert constraint["G"] == pytest.approx(0.299, abs=1e-3) + assert constraint["A"] == pytest.approx(0.795, abs=1e-3) + assert constraint["C"] == pytest.approx(1, abs=1e-3) + + def test_effective_size_weighted_undirected(self): + G = self.G.copy() + nx.set_edge_attributes(G, self.G_weights, "weight") + effective_size = nx.effective_size(G, weight="weight", nodes=self.Gnodes) + assert effective_size["G"] == pytest.approx(5.47, abs=1e-2) + assert effective_size["A"] == pytest.approx(2.47, abs=1e-2) + assert effective_size["C"] == pytest.approx(1, abs=1e-2) + + def test_constraint_isolated(self): + G = self.G.copy() + G.add_node(1) + constraint = nx.constraint(G, nodes=self.Gnodes + [1]) + assert math.isnan(constraint[1]) + + def test_effective_size_isolated(self): + G = self.G.copy() + G.add_node(1) + nx.set_edge_attributes(G, self.G_weights, "weight") + effective_size = nx.effective_size(G, weight="weight", nodes=self.Gnodes + [1]) + assert math.isnan(effective_size[1]) + + def test_effective_size_borgatti_isolated(self): + G = self.G.copy() + G.add_node(1) + effective_size = nx.effective_size(G, nodes=self.Gnodes + [1]) + assert math.isnan(effective_size[1]) + + +class TestStructuralHoles(TestStructuralHolesNoScipy): + pytest.importorskip("scipy") + Dnodes = None + Gnodes = None + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +@pytest.mark.parametrize("nodes", (None, [0])) +def test_effective_size_isolated_node_with_selfloop(graph, nodes): + """Behavior consistent with isolated node without self-loop. See gh-6916""" + G = graph([(0, 0)]) # Single node with one self-edge + assert math.isnan(nx.effective_size(G, nodes=nodes)[0]) + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +@pytest.mark.parametrize("nodes", (None, [0])) +def test_effective_size_isolated_node_with_selfloop_weighted(graph, nodes): + """Weighted self-loop. See gh-6916""" + G = graph() + G.add_weighted_edges_from([(0, 0, 10)]) + assert math.isnan(nx.effective_size(G, nodes=nodes)[0]) + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +def test_constraint_isolated_node_with_selfloop(graph): + """Behavior consistent with isolated node without self-loop. See gh-6916""" + G = graph([(0, 0)]) # Single node with one self-edge + assert math.isnan(nx.constraint(G)[0]) + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +def test_constraint_isolated_node_with_selfloop_using_nodes_kwarg(graph): + """Behavior consistent with isolated node without self-loop. See gh-6916""" + G = graph([(0, 0)]) # Single node with one self-edge + assert nx.constraint(G, nodes=[0])[0] == 4 + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +def test_constraint_isolated_node_with_selfloop_weighted(graph): + """Weighted self-loop. See gh-6916""" + G = graph() + G.add_weighted_edges_from([(0, 0, 10)]) + assert math.isnan(nx.constraint(G)[0]) + + +@pytest.mark.parametrize("graph", (nx.Graph, nx.DiGraph)) +def test_constraint_isolated_node_with_selfloop_weighted_using_nodes_kwarg(graph): + G = graph() + G.add_weighted_edges_from([(0, 0, 10)]) + assert nx.constraint(G, nodes=[0])[0] == 4 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py new file mode 100644 index 0000000000000000000000000000000000000000..c3bf82fa53b2564b13e555d994bd73b5885e1915 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_summarization.py @@ -0,0 +1,642 @@ +""" +Unit tests for dedensification and graph summarization +""" + +import pytest + +import networkx as nx + + +class TestDirectedDedensification: + def build_original_graph(self): + original_matrix = [ + ("1", "BC"), + ("2", "ABC"), + ("3", ["A", "B", "6"]), + ("4", "ABC"), + ("5", "AB"), + ("6", ["5"]), + ("A", ["6"]), + ] + graph = nx.DiGraph() + for source, targets in original_matrix: + for target in targets: + graph.add_edge(source, target) + return graph + + def build_compressed_graph(self): + compressed_matrix = [ + ("1", "BC"), + ("2", ["ABC"]), + ("3", ["A", "B", "6"]), + ("4", ["ABC"]), + ("5", "AB"), + ("6", ["5"]), + ("A", ["6"]), + ("ABC", "ABC"), + ] + compressed_graph = nx.DiGraph() + for source, targets in compressed_matrix: + for target in targets: + compressed_graph.add_edge(source, target) + return compressed_graph + + def test_empty(self): + """ + Verify that an empty directed graph results in no compressor nodes + """ + G = nx.DiGraph() + compressed_graph, c_nodes = nx.dedensify(G, threshold=2) + assert c_nodes == set() + + @staticmethod + def densify(G, compressor_nodes, copy=True): + """ + Reconstructs the original graph from a dedensified, directed graph + + Parameters + ---------- + G: dedensified graph + A networkx graph + compressor_nodes: iterable + Iterable of compressor nodes in the dedensified graph + inplace: bool, optional (default: False) + Indicates if densification should be done inplace + + Returns + ------- + G: graph + A densified networkx graph + """ + if copy: + G = G.copy() + for compressor_node in compressor_nodes: + all_neighbors = set(nx.all_neighbors(G, compressor_node)) + out_neighbors = set(G.neighbors(compressor_node)) + for out_neighbor in out_neighbors: + G.remove_edge(compressor_node, out_neighbor) + in_neighbors = all_neighbors - out_neighbors + for in_neighbor in in_neighbors: + G.remove_edge(in_neighbor, compressor_node) + for out_neighbor in out_neighbors: + G.add_edge(in_neighbor, out_neighbor) + G.remove_node(compressor_node) + return G + + def setup_method(self): + self.c_nodes = ("ABC",) + + def test_dedensify_edges(self): + """ + Verifies that dedensify produced the correct edges to/from compressor + nodes in a directed graph + """ + G = self.build_original_graph() + compressed_G = self.build_compressed_graph() + compressed_graph, c_nodes = nx.dedensify(G, threshold=2) + for s, t in compressed_graph.edges(): + o_s = "".join(sorted(s)) + o_t = "".join(sorted(t)) + compressed_graph_exists = compressed_graph.has_edge(s, t) + verified_compressed_exists = compressed_G.has_edge(o_s, o_t) + assert compressed_graph_exists == verified_compressed_exists + assert len(c_nodes) == len(self.c_nodes) + + def test_dedensify_edge_count(self): + """ + Verifies that dedensify produced the correct number of compressor nodes + in a directed graph + """ + G = self.build_original_graph() + original_edge_count = len(G.edges()) + c_G, c_nodes = nx.dedensify(G, threshold=2) + compressed_edge_count = len(c_G.edges()) + assert compressed_edge_count <= original_edge_count + compressed_G = self.build_compressed_graph() + assert compressed_edge_count == len(compressed_G.edges()) + + def test_densify_edges(self): + """ + Verifies that densification produces the correct edges from the + original directed graph + """ + compressed_G = self.build_compressed_graph() + original_graph = self.densify(compressed_G, self.c_nodes, copy=True) + G = self.build_original_graph() + for s, t in G.edges(): + assert G.has_edge(s, t) == original_graph.has_edge(s, t) + + def test_densify_edge_count(self): + """ + Verifies that densification produces the correct number of edges in the + original directed graph + """ + compressed_G = self.build_compressed_graph() + compressed_edge_count = len(compressed_G.edges()) + original_graph = self.densify(compressed_G, self.c_nodes) + original_edge_count = len(original_graph.edges()) + assert compressed_edge_count <= original_edge_count + G = self.build_original_graph() + assert original_edge_count == len(G.edges()) + + +class TestUnDirectedDedensification: + def build_original_graph(self): + """ + Builds graph shown in the original research paper + """ + original_matrix = [ + ("1", "CB"), + ("2", "ABC"), + ("3", ["A", "B", "6"]), + ("4", "ABC"), + ("5", "AB"), + ("6", ["5"]), + ("A", ["6"]), + ] + graph = nx.Graph() + for source, targets in original_matrix: + for target in targets: + graph.add_edge(source, target) + return graph + + def test_empty(self): + """ + Verify that an empty undirected graph results in no compressor nodes + """ + G = nx.Graph() + compressed_G, c_nodes = nx.dedensify(G, threshold=2) + assert c_nodes == set() + + def setup_method(self): + self.c_nodes = ("6AB", "ABC") + + def build_compressed_graph(self): + compressed_matrix = [ + ("1", ["B", "C"]), + ("2", ["ABC"]), + ("3", ["6AB"]), + ("4", ["ABC"]), + ("5", ["6AB"]), + ("6", ["6AB", "A"]), + ("A", ["6AB", "ABC"]), + ("B", ["ABC", "6AB"]), + ("C", ["ABC"]), + ] + compressed_graph = nx.Graph() + for source, targets in compressed_matrix: + for target in targets: + compressed_graph.add_edge(source, target) + return compressed_graph + + def test_dedensify_edges(self): + """ + Verifies that dedensify produced correct compressor nodes and the + correct edges to/from the compressor nodes in an undirected graph + """ + G = self.build_original_graph() + c_G, c_nodes = nx.dedensify(G, threshold=2) + v_compressed_G = self.build_compressed_graph() + for s, t in c_G.edges(): + o_s = "".join(sorted(s)) + o_t = "".join(sorted(t)) + has_compressed_edge = c_G.has_edge(s, t) + verified_has_compressed_edge = v_compressed_G.has_edge(o_s, o_t) + assert has_compressed_edge == verified_has_compressed_edge + assert len(c_nodes) == len(self.c_nodes) + + def test_dedensify_edge_count(self): + """ + Verifies that dedensify produced the correct number of edges in an + undirected graph + """ + G = self.build_original_graph() + c_G, c_nodes = nx.dedensify(G, threshold=2, copy=True) + compressed_edge_count = len(c_G.edges()) + verified_original_edge_count = len(G.edges()) + assert compressed_edge_count <= verified_original_edge_count + verified_compressed_G = self.build_compressed_graph() + verified_compressed_edge_count = len(verified_compressed_G.edges()) + assert compressed_edge_count == verified_compressed_edge_count + + +@pytest.mark.parametrize( + "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +) +def test_summarization_empty(graph_type): + G = graph_type() + summary_graph = nx.snap_aggregation(G, node_attributes=("color",)) + assert nx.is_isomorphic(summary_graph, G) + + +class AbstractSNAP: + node_attributes = ("color",) + + def build_original_graph(self): + pass + + def build_summary_graph(self): + pass + + def test_summary_graph(self): + original_graph = self.build_original_graph() + summary_graph = self.build_summary_graph() + + relationship_attributes = ("type",) + generated_summary_graph = nx.snap_aggregation( + original_graph, self.node_attributes, relationship_attributes + ) + relabeled_summary_graph = self.deterministic_labels(generated_summary_graph) + assert nx.is_isomorphic(summary_graph, relabeled_summary_graph) + + def deterministic_labels(self, G): + node_labels = list(G.nodes) + node_labels = sorted(node_labels, key=lambda n: sorted(G.nodes[n]["group"])[0]) + node_labels.sort() + + label_mapping = {} + for index, node in enumerate(node_labels): + label = f"Supernode-{index}" + label_mapping[node] = label + + return nx.relabel_nodes(G, label_mapping) + + +class TestSNAPNoEdgeTypes(AbstractSNAP): + relationship_attributes = () + + def test_summary_graph(self): + original_graph = self.build_original_graph() + summary_graph = self.build_summary_graph() + + relationship_attributes = ("type",) + generated_summary_graph = nx.snap_aggregation( + original_graph, self.node_attributes + ) + relabeled_summary_graph = self.deterministic_labels(generated_summary_graph) + assert nx.is_isomorphic(summary_graph, relabeled_summary_graph) + + def build_original_graph(self): + nodes = { + "A": {"color": "Red"}, + "B": {"color": "Red"}, + "C": {"color": "Red"}, + "D": {"color": "Red"}, + "E": {"color": "Blue"}, + "F": {"color": "Blue"}, + "G": {"color": "Blue"}, + "H": {"color": "Blue"}, + "I": {"color": "Yellow"}, + "J": {"color": "Yellow"}, + "K": {"color": "Yellow"}, + "L": {"color": "Yellow"}, + } + edges = [ + ("A", "B"), + ("A", "C"), + ("A", "E"), + ("A", "I"), + ("B", "D"), + ("B", "J"), + ("B", "F"), + ("C", "G"), + ("D", "H"), + ("I", "J"), + ("J", "K"), + ("I", "L"), + ] + G = nx.Graph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target in edges: + G.add_edge(source, target) + + return G + + def build_summary_graph(self): + nodes = { + "Supernode-0": {"color": "Red"}, + "Supernode-1": {"color": "Red"}, + "Supernode-2": {"color": "Blue"}, + "Supernode-3": {"color": "Blue"}, + "Supernode-4": {"color": "Yellow"}, + "Supernode-5": {"color": "Yellow"}, + } + edges = [ + ("Supernode-0", "Supernode-0"), + ("Supernode-0", "Supernode-1"), + ("Supernode-0", "Supernode-2"), + ("Supernode-0", "Supernode-4"), + ("Supernode-1", "Supernode-3"), + ("Supernode-4", "Supernode-4"), + ("Supernode-4", "Supernode-5"), + ] + G = nx.Graph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target in edges: + G.add_edge(source, target) + + supernodes = { + "Supernode-0": {"A", "B"}, + "Supernode-1": {"C", "D"}, + "Supernode-2": {"E", "F"}, + "Supernode-3": {"G", "H"}, + "Supernode-4": {"I", "J"}, + "Supernode-5": {"K", "L"}, + } + nx.set_node_attributes(G, supernodes, "group") + return G + + +class TestSNAPUndirected(AbstractSNAP): + def build_original_graph(self): + nodes = { + "A": {"color": "Red"}, + "B": {"color": "Red"}, + "C": {"color": "Red"}, + "D": {"color": "Red"}, + "E": {"color": "Blue"}, + "F": {"color": "Blue"}, + "G": {"color": "Blue"}, + "H": {"color": "Blue"}, + "I": {"color": "Yellow"}, + "J": {"color": "Yellow"}, + "K": {"color": "Yellow"}, + "L": {"color": "Yellow"}, + } + edges = [ + ("A", "B", "Strong"), + ("A", "C", "Weak"), + ("A", "E", "Strong"), + ("A", "I", "Weak"), + ("B", "D", "Weak"), + ("B", "J", "Weak"), + ("B", "F", "Strong"), + ("C", "G", "Weak"), + ("D", "H", "Weak"), + ("I", "J", "Strong"), + ("J", "K", "Strong"), + ("I", "L", "Strong"), + ] + G = nx.Graph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, type in edges: + G.add_edge(source, target, type=type) + + return G + + def build_summary_graph(self): + nodes = { + "Supernode-0": {"color": "Red"}, + "Supernode-1": {"color": "Red"}, + "Supernode-2": {"color": "Blue"}, + "Supernode-3": {"color": "Blue"}, + "Supernode-4": {"color": "Yellow"}, + "Supernode-5": {"color": "Yellow"}, + } + edges = [ + ("Supernode-0", "Supernode-0", "Strong"), + ("Supernode-0", "Supernode-1", "Weak"), + ("Supernode-0", "Supernode-2", "Strong"), + ("Supernode-0", "Supernode-4", "Weak"), + ("Supernode-1", "Supernode-3", "Weak"), + ("Supernode-4", "Supernode-4", "Strong"), + ("Supernode-4", "Supernode-5", "Strong"), + ] + G = nx.Graph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, type in edges: + G.add_edge(source, target, types=[{"type": type}]) + + supernodes = { + "Supernode-0": {"A", "B"}, + "Supernode-1": {"C", "D"}, + "Supernode-2": {"E", "F"}, + "Supernode-3": {"G", "H"}, + "Supernode-4": {"I", "J"}, + "Supernode-5": {"K", "L"}, + } + nx.set_node_attributes(G, supernodes, "group") + return G + + +class TestSNAPDirected(AbstractSNAP): + def build_original_graph(self): + nodes = { + "A": {"color": "Red"}, + "B": {"color": "Red"}, + "C": {"color": "Green"}, + "D": {"color": "Green"}, + "E": {"color": "Blue"}, + "F": {"color": "Blue"}, + "G": {"color": "Yellow"}, + "H": {"color": "Yellow"}, + } + edges = [ + ("A", "C", "Strong"), + ("A", "E", "Strong"), + ("A", "F", "Weak"), + ("B", "D", "Strong"), + ("B", "E", "Weak"), + ("B", "F", "Strong"), + ("C", "G", "Strong"), + ("C", "F", "Strong"), + ("D", "E", "Strong"), + ("D", "H", "Strong"), + ("G", "E", "Strong"), + ("H", "F", "Strong"), + ] + G = nx.DiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, type in edges: + G.add_edge(source, target, type=type) + + return G + + def build_summary_graph(self): + nodes = { + "Supernode-0": {"color": "Red"}, + "Supernode-1": {"color": "Green"}, + "Supernode-2": {"color": "Blue"}, + "Supernode-3": {"color": "Yellow"}, + } + edges = [ + ("Supernode-0", "Supernode-1", [{"type": "Strong"}]), + ("Supernode-0", "Supernode-2", [{"type": "Weak"}, {"type": "Strong"}]), + ("Supernode-1", "Supernode-2", [{"type": "Strong"}]), + ("Supernode-1", "Supernode-3", [{"type": "Strong"}]), + ("Supernode-3", "Supernode-2", [{"type": "Strong"}]), + ] + G = nx.DiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, types in edges: + G.add_edge(source, target, types=types) + + supernodes = { + "Supernode-0": {"A", "B"}, + "Supernode-1": {"C", "D"}, + "Supernode-2": {"E", "F"}, + "Supernode-3": {"G", "H"}, + "Supernode-4": {"I", "J"}, + "Supernode-5": {"K", "L"}, + } + nx.set_node_attributes(G, supernodes, "group") + return G + + +class TestSNAPUndirectedMulti(AbstractSNAP): + def build_original_graph(self): + nodes = { + "A": {"color": "Red"}, + "B": {"color": "Red"}, + "C": {"color": "Red"}, + "D": {"color": "Blue"}, + "E": {"color": "Blue"}, + "F": {"color": "Blue"}, + "G": {"color": "Yellow"}, + "H": {"color": "Yellow"}, + "I": {"color": "Yellow"}, + } + edges = [ + ("A", "D", ["Weak", "Strong"]), + ("B", "E", ["Weak", "Strong"]), + ("D", "I", ["Strong"]), + ("E", "H", ["Strong"]), + ("F", "G", ["Weak"]), + ("I", "G", ["Weak", "Strong"]), + ("I", "H", ["Weak", "Strong"]), + ("G", "H", ["Weak", "Strong"]), + ] + G = nx.MultiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, types in edges: + for type in types: + G.add_edge(source, target, type=type) + + return G + + def build_summary_graph(self): + nodes = { + "Supernode-0": {"color": "Red"}, + "Supernode-1": {"color": "Blue"}, + "Supernode-2": {"color": "Yellow"}, + "Supernode-3": {"color": "Blue"}, + "Supernode-4": {"color": "Yellow"}, + "Supernode-5": {"color": "Red"}, + } + edges = [ + ("Supernode-1", "Supernode-2", [{"type": "Weak"}]), + ("Supernode-2", "Supernode-4", [{"type": "Weak"}, {"type": "Strong"}]), + ("Supernode-3", "Supernode-4", [{"type": "Strong"}]), + ("Supernode-3", "Supernode-5", [{"type": "Weak"}, {"type": "Strong"}]), + ("Supernode-4", "Supernode-4", [{"type": "Weak"}, {"type": "Strong"}]), + ] + G = nx.MultiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, types in edges: + for type in types: + G.add_edge(source, target, type=type) + + supernodes = { + "Supernode-0": {"A", "B"}, + "Supernode-1": {"C", "D"}, + "Supernode-2": {"E", "F"}, + "Supernode-3": {"G", "H"}, + "Supernode-4": {"I", "J"}, + "Supernode-5": {"K", "L"}, + } + nx.set_node_attributes(G, supernodes, "group") + return G + + +class TestSNAPDirectedMulti(AbstractSNAP): + def build_original_graph(self): + nodes = { + "A": {"color": "Red"}, + "B": {"color": "Red"}, + "C": {"color": "Green"}, + "D": {"color": "Green"}, + "E": {"color": "Blue"}, + "F": {"color": "Blue"}, + "G": {"color": "Yellow"}, + "H": {"color": "Yellow"}, + } + edges = [ + ("A", "C", ["Weak", "Strong"]), + ("A", "E", ["Strong"]), + ("A", "F", ["Weak"]), + ("B", "D", ["Weak", "Strong"]), + ("B", "E", ["Weak"]), + ("B", "F", ["Strong"]), + ("C", "G", ["Weak", "Strong"]), + ("C", "F", ["Strong"]), + ("D", "E", ["Strong"]), + ("D", "H", ["Weak", "Strong"]), + ("G", "E", ["Strong"]), + ("H", "F", ["Strong"]), + ] + G = nx.MultiDiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, types in edges: + for type in types: + G.add_edge(source, target, type=type) + + return G + + def build_summary_graph(self): + nodes = { + "Supernode-0": {"color": "Red"}, + "Supernode-1": {"color": "Blue"}, + "Supernode-2": {"color": "Yellow"}, + "Supernode-3": {"color": "Blue"}, + } + edges = [ + ("Supernode-0", "Supernode-1", ["Weak", "Strong"]), + ("Supernode-0", "Supernode-2", ["Weak", "Strong"]), + ("Supernode-1", "Supernode-2", ["Strong"]), + ("Supernode-1", "Supernode-3", ["Weak", "Strong"]), + ("Supernode-3", "Supernode-2", ["Strong"]), + ] + G = nx.MultiDiGraph() + for node in nodes: + attributes = nodes[node] + G.add_node(node, **attributes) + + for source, target, types in edges: + for type in types: + G.add_edge(source, target, type=type) + + supernodes = { + "Supernode-0": {"A", "B"}, + "Supernode-1": {"C", "D"}, + "Supernode-2": {"E", "F"}, + "Supernode-3": {"G", "H"}, + } + nx.set_node_attributes(G, supernodes, "group") + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py new file mode 100644 index 0000000000000000000000000000000000000000..e765bd5e11496841072990aa792b90ca8772b4d3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_swap.py @@ -0,0 +1,179 @@ +import pytest + +import networkx as nx + +cycle = nx.cycle_graph(5, create_using=nx.DiGraph) +tree = nx.DiGraph() +tree.add_edges_from(nx.random_labeled_tree(10, seed=42).edges) +path = nx.path_graph(5, create_using=nx.DiGraph) +binomial = nx.binomial_tree(3, create_using=nx.DiGraph) +HH = nx.directed_havel_hakimi_graph([1, 2, 1, 2, 2, 2], [3, 1, 0, 1, 2, 3]) +balanced_tree = nx.balanced_tree(2, 3, create_using=nx.DiGraph) + + +@pytest.mark.parametrize("G", [path, binomial, HH, cycle, tree, balanced_tree]) +def test_directed_edge_swap(G): + in_degree = set(G.in_degree) + out_degree = set(G.out_degree) + edges = set(G.edges) + nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1) + assert in_degree == set(G.in_degree) + assert out_degree == set(G.out_degree) + assert edges != set(G.edges) + assert 3 == sum(e not in edges for e in G.edges) + + +def test_directed_edge_swap_undo_previous_swap(): + G = nx.DiGraph(nx.path_graph(4).edges) # only 1 swap possible + edges = set(G.edges) + nx.directed_edge_swap(G, nswap=2, max_tries=100) + assert edges == set(G.edges) + + nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1) + assert {(0, 2), (1, 3), (2, 1)} == set(G.edges) + nx.directed_edge_swap(G, nswap=1, max_tries=100, seed=1) + assert edges == set(G.edges) + + +def test_edge_cases_directed_edge_swap(): + # Tests cases when swaps are impossible, either too few edges exist, or self loops/cycles are unavoidable + # TODO: Rewrite function to explicitly check for impossible swaps and raise error + e = ( + "Maximum number of swap attempts \\(11\\) exceeded " + "before desired swaps achieved \\(\\d\\)." + ) + graph = nx.DiGraph([(0, 0), (0, 1), (1, 0), (2, 3), (3, 2)]) + with pytest.raises(nx.NetworkXAlgorithmError, match=e): + nx.directed_edge_swap(graph, nswap=1, max_tries=10, seed=1) + + +def test_double_edge_swap(): + graph = nx.barabasi_albert_graph(200, 1) + degrees = sorted(d for n, d in graph.degree()) + G = nx.double_edge_swap(graph, 40) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_double_edge_swap_seed(): + graph = nx.barabasi_albert_graph(200, 1) + degrees = sorted(d for n, d in graph.degree()) + G = nx.double_edge_swap(graph, 40, seed=1) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_connected_double_edge_swap(): + graph = nx.barabasi_albert_graph(200, 1) + degrees = sorted(d for n, d in graph.degree()) + G = nx.connected_double_edge_swap(graph, 40, seed=1) + assert nx.is_connected(graph) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_connected_double_edge_swap_low_window_threshold(): + graph = nx.barabasi_albert_graph(200, 1) + degrees = sorted(d for n, d in graph.degree()) + G = nx.connected_double_edge_swap(graph, 40, _window_threshold=0, seed=1) + assert nx.is_connected(graph) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_connected_double_edge_swap_star(): + # Testing ui==xi in connected_double_edge_swap + graph = nx.star_graph(40) + degrees = sorted(d for n, d in graph.degree()) + G = nx.connected_double_edge_swap(graph, 1, seed=4) + assert nx.is_connected(graph) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_connected_double_edge_swap_star_low_window_threshold(): + # Testing ui==xi in connected_double_edge_swap with low window threshold + graph = nx.star_graph(40) + degrees = sorted(d for n, d in graph.degree()) + G = nx.connected_double_edge_swap(graph, 1, _window_threshold=0, seed=4) + assert nx.is_connected(graph) + assert degrees == sorted(d for n, d in graph.degree()) + + +def test_directed_edge_swap_small(): + with pytest.raises(nx.NetworkXError): + G = nx.directed_edge_swap(nx.path_graph(3, create_using=nx.DiGraph)) + + +def test_directed_edge_swap_tries(): + with pytest.raises(nx.NetworkXError): + G = nx.directed_edge_swap( + nx.path_graph(3, create_using=nx.DiGraph), nswap=1, max_tries=0 + ) + + +def test_directed_exception_undirected(): + graph = nx.Graph([(0, 1), (2, 3)]) + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.directed_edge_swap(graph) + + +def test_directed_edge_max_tries(): + with pytest.raises(nx.NetworkXAlgorithmError): + G = nx.directed_edge_swap( + nx.complete_graph(4, nx.DiGraph()), nswap=1, max_tries=5 + ) + + +def test_double_edge_swap_small(): + with pytest.raises(nx.NetworkXError): + G = nx.double_edge_swap(nx.path_graph(3)) + + +def test_double_edge_swap_tries(): + with pytest.raises(nx.NetworkXError): + G = nx.double_edge_swap(nx.path_graph(10), nswap=1, max_tries=0) + + +def test_double_edge_directed(): + graph = nx.DiGraph([(0, 1), (2, 3)]) + with pytest.raises(nx.NetworkXError, match="not defined for directed graphs."): + G = nx.double_edge_swap(graph) + + +def test_double_edge_max_tries(): + with pytest.raises(nx.NetworkXAlgorithmError): + G = nx.double_edge_swap(nx.complete_graph(4), nswap=1, max_tries=5) + + +def test_connected_double_edge_swap_small(): + with pytest.raises(nx.NetworkXError): + G = nx.connected_double_edge_swap(nx.path_graph(3)) + + +def test_connected_double_edge_swap_not_connected(): + with pytest.raises(nx.NetworkXError): + G = nx.path_graph(3) + nx.add_path(G, [10, 11, 12]) + G = nx.connected_double_edge_swap(G) + + +def test_degree_seq_c4(): + G = nx.cycle_graph(4) + degrees = sorted(d for n, d in G.degree()) + G = nx.double_edge_swap(G, 1, 100) + assert degrees == sorted(d for n, d in G.degree()) + + +def test_fewer_than_4_nodes(): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2]) + with pytest.raises(nx.NetworkXError, match=".*fewer than four nodes."): + nx.directed_edge_swap(G) + + +def test_less_than_3_edges(): + G = nx.DiGraph([(0, 1), (1, 2)]) + G.add_nodes_from([3, 4]) + with pytest.raises(nx.NetworkXError, match=".*fewer than 3 edges"): + nx.directed_edge_swap(G) + + G = nx.Graph() + G.add_nodes_from([0, 1, 2, 3]) + with pytest.raises(nx.NetworkXError, match=".*fewer than 2 edges"): + nx.double_edge_swap(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py new file mode 100644 index 0000000000000000000000000000000000000000..d8806fd61df539d97e814e2a5cb92bc33dc71559 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_threshold.py @@ -0,0 +1,270 @@ +""" +Threshold Graphs +================ +""" + +import pytest + +import networkx as nx +import networkx.algorithms.threshold as nxt + +cnlti = nx.convert_node_labels_to_integers + + +def test_threshold_graph_invalid_creation_sequence(): + bad_creation_sequence = [2.0, 2, 1, 0] # floats are not allowed + with pytest.raises(ValueError, match="not a valid creation sequence"): + nxt.threshold_graph(bad_creation_sequence) + + +class TestGeneratorThreshold: + def test_threshold_sequence_graph_test(self): + G = nx.star_graph(10) + assert nxt.is_threshold_graph(G) + assert nxt.is_threshold_sequence([d for n, d in G.degree()]) + + G = nx.complete_graph(10) + assert nxt.is_threshold_graph(G) + assert nxt.is_threshold_sequence([d for n, d in G.degree()]) + + deg = [3, 2, 2, 1, 1, 1] + assert not nxt.is_threshold_sequence(deg) + + deg = [3, 2, 2, 1] + assert nxt.is_threshold_sequence(deg) + + G = nx.generators.havel_hakimi_graph(deg) + assert nxt.is_threshold_graph(G) + + def test_creation_sequences(self): + deg = [3, 2, 2, 1] + G = nx.generators.havel_hakimi_graph(deg) + + with pytest.raises(ValueError): + nxt.creation_sequence(deg, with_labels=True, compact=True) + + cs0 = nxt.creation_sequence(deg) + H0 = nxt.threshold_graph(cs0) + assert "".join(cs0) == "ddid" + + cs1 = nxt.creation_sequence(deg, with_labels=True) + H1 = nxt.threshold_graph(cs1) + assert cs1 == [(1, "d"), (2, "d"), (3, "i"), (0, "d")] + + cs2 = nxt.creation_sequence(deg, compact=True) + H2 = nxt.threshold_graph(cs2) + assert cs2 == [2, 1, 1] + assert "".join(nxt.uncompact(cs2)) == "ddid" + assert nx.could_be_isomorphic(H0, G) + assert nx.could_be_isomorphic(H0, H1) + assert nx.could_be_isomorphic(H0, H2) + + def test_make_compact(self): + assert nxt.make_compact(["d", "d", "d", "i", "d", "d"]) == [3, 1, 2] + assert nxt.make_compact([3, 1, 2]) == [3, 1, 2] + pytest.raises(TypeError, nxt.make_compact, [3.0, 1.0, 2.0]) + + def test_uncompact(self): + assert nxt.uncompact([3, 1, 2]) == ["d", "d", "d", "i", "d", "d"] + assert nxt.uncompact(["d", "d", "i", "d"]) == ["d", "d", "i", "d"] + assert nxt.uncompact( + nxt.uncompact([(1, "d"), (2, "d"), (3, "i"), (0, "d")]) + ) == nxt.uncompact([(1, "d"), (2, "d"), (3, "i"), (0, "d")]) + pytest.raises(TypeError, nxt.uncompact, [3.0, 1.0, 2.0]) + + def test_creation_sequence_to_weights(self): + assert nxt.creation_sequence_to_weights([3, 1, 2]) == [ + 0.5, + 0.5, + 0.5, + 0.25, + 0.75, + 0.75, + ] + pytest.raises(TypeError, nxt.creation_sequence_to_weights, [3.0, 1.0, 2.0]) + + def test_weights_to_creation_sequence(self): + deg = [3, 2, 2, 1] + with pytest.raises(ValueError): + nxt.weights_to_creation_sequence(deg, with_labels=True, compact=True) + assert nxt.weights_to_creation_sequence(deg, with_labels=True) == [ + (3, "d"), + (1, "d"), + (2, "d"), + (0, "d"), + ] + assert nxt.weights_to_creation_sequence(deg, compact=True) == [4] + + def test_find_alternating_4_cycle(self): + G = nx.Graph() + G.add_edge(1, 2) + assert not nxt.find_alternating_4_cycle(G) + + def test_shortest_path(self): + deg = [3, 2, 2, 1] + G = nx.generators.havel_hakimi_graph(deg) + cs1 = nxt.creation_sequence(deg, with_labels=True) + for n, m in [(3, 0), (0, 3), (0, 2), (0, 1), (1, 3), (3, 1), (1, 2), (2, 3)]: + assert nxt.shortest_path(cs1, n, m) == nx.shortest_path(G, n, m) + + spl = nxt.shortest_path_length(cs1, 3) + spl2 = nxt.shortest_path_length([t for v, t in cs1], 2) + assert spl == spl2 + + spld = {} + for j, pl in enumerate(spl): + n = cs1[j][0] + spld[n] = pl + assert spld == nx.single_source_shortest_path_length(G, 3) + + assert nxt.shortest_path(["d", "d", "d", "i", "d", "d"], 1, 2) == [1, 2] + assert nxt.shortest_path([3, 1, 2], 1, 2) == [1, 2] + pytest.raises(TypeError, nxt.shortest_path, [3.0, 1.0, 2.0], 1, 2) + pytest.raises(ValueError, nxt.shortest_path, [3, 1, 2], "a", 2) + pytest.raises(ValueError, nxt.shortest_path, [3, 1, 2], 1, "b") + assert nxt.shortest_path([3, 1, 2], 1, 1) == [1] + + def test_shortest_path_length(self): + assert nxt.shortest_path_length([3, 1, 2], 1) == [1, 0, 1, 2, 1, 1] + assert nxt.shortest_path_length(["d", "d", "d", "i", "d", "d"], 1) == [ + 1, + 0, + 1, + 2, + 1, + 1, + ] + assert nxt.shortest_path_length(("d", "d", "d", "i", "d", "d"), 1) == [ + 1, + 0, + 1, + 2, + 1, + 1, + ] + pytest.raises(TypeError, nxt.shortest_path, [3.0, 1.0, 2.0], 1) + + def test_random_threshold_sequence(self): + assert len(nxt.random_threshold_sequence(10, 0.5)) == 10 + assert nxt.random_threshold_sequence(10, 0.5, seed=42) == [ + "d", + "i", + "d", + "d", + "d", + "i", + "i", + "i", + "d", + "d", + ] + pytest.raises(ValueError, nxt.random_threshold_sequence, 10, 1.5) + + def test_right_d_threshold_sequence(self): + assert nxt.right_d_threshold_sequence(3, 2) == ["d", "i", "d"] + pytest.raises(ValueError, nxt.right_d_threshold_sequence, 2, 3) + + def test_left_d_threshold_sequence(self): + assert nxt.left_d_threshold_sequence(3, 2) == ["d", "i", "d"] + pytest.raises(ValueError, nxt.left_d_threshold_sequence, 2, 3) + + def test_weights_thresholds(self): + wseq = [3, 4, 3, 3, 5, 6, 5, 4, 5, 6] + cs = nxt.weights_to_creation_sequence(wseq, threshold=10) + wseq = nxt.creation_sequence_to_weights(cs) + cs2 = nxt.weights_to_creation_sequence(wseq) + assert cs == cs2 + + wseq = nxt.creation_sequence_to_weights(nxt.uncompact([3, 1, 2, 3, 3, 2, 3])) + assert wseq == [ + s * 0.125 for s in [4, 4, 4, 3, 5, 5, 2, 2, 2, 6, 6, 6, 1, 1, 7, 7, 7] + ] + + wseq = nxt.creation_sequence_to_weights([3, 1, 2, 3, 3, 2, 3]) + assert wseq == [ + s * 0.125 for s in [4, 4, 4, 3, 5, 5, 2, 2, 2, 6, 6, 6, 1, 1, 7, 7, 7] + ] + + wseq = nxt.creation_sequence_to_weights(list(enumerate("ddidiiidididi"))) + assert wseq == [s * 0.1 for s in [5, 5, 4, 6, 3, 3, 3, 7, 2, 8, 1, 9, 0]] + + wseq = nxt.creation_sequence_to_weights("ddidiiidididi") + assert wseq == [s * 0.1 for s in [5, 5, 4, 6, 3, 3, 3, 7, 2, 8, 1, 9, 0]] + + wseq = nxt.creation_sequence_to_weights("ddidiiidididid") + ws = [s / 12 for s in [6, 6, 5, 7, 4, 4, 4, 8, 3, 9, 2, 10, 1, 11]] + assert sum(abs(c - d) for c, d in zip(wseq, ws)) < 1e-14 + + def test_finding_routines(self): + G = nx.Graph({1: [2], 2: [3], 3: [4], 4: [5], 5: [6]}) + G.add_edge(2, 4) + G.add_edge(2, 5) + G.add_edge(2, 7) + G.add_edge(3, 6) + G.add_edge(4, 6) + + # Alternating 4 cycle + assert nxt.find_alternating_4_cycle(G) == [1, 2, 3, 6] + + # Threshold graph + TG = nxt.find_threshold_graph(G) + assert nxt.is_threshold_graph(TG) + assert sorted(TG.nodes()) == [1, 2, 3, 4, 5, 7] + + cs = nxt.creation_sequence(dict(TG.degree()), with_labels=True) + assert nxt.find_creation_sequence(G) == cs + + def test_fast_versions_properties_threshold_graphs(self): + cs = "ddiiddid" + G = nxt.threshold_graph(cs) + assert nxt.density("ddiiddid") == nx.density(G) + assert sorted(nxt.degree_sequence(cs)) == sorted(d for n, d in G.degree()) + + ts = nxt.triangle_sequence(cs) + assert ts == list(nx.triangles(G).values()) + assert sum(ts) // 3 == nxt.triangles(cs) + + c1 = nxt.cluster_sequence(cs) + c2 = list(nx.clustering(G).values()) + assert sum(abs(c - d) for c, d in zip(c1, c2)) == pytest.approx(0, abs=1e-7) + + b1 = nx.betweenness_centrality(G).values() + b2 = nxt.betweenness_sequence(cs) + assert sum(abs(c - d) for c, d in zip(b1, b2)) < 1e-7 + + assert nxt.eigenvalues(cs) == [0, 1, 3, 3, 5, 7, 7, 8] + + # Degree Correlation + assert abs(nxt.degree_correlation(cs) + 0.593038821954) < 1e-12 + assert nxt.degree_correlation("diiiddi") == -0.8 + assert nxt.degree_correlation("did") == -1.0 + assert nxt.degree_correlation("ddd") == 1.0 + assert nxt.eigenvalues("dddiii") == [0, 0, 0, 0, 3, 3] + assert nxt.eigenvalues("dddiiid") == [0, 1, 1, 1, 4, 4, 7] + + def test_tg_creation_routines(self): + s = nxt.left_d_threshold_sequence(5, 7) + s = nxt.right_d_threshold_sequence(5, 7) + + def test_eigenvectors(self): + np = pytest.importorskip("numpy") + eigenval = np.linalg.eigvals + pytest.importorskip("scipy") + + cs = "ddiiddid" + G = nxt.threshold_graph(cs) + (tgeval, tgevec) = nxt.eigenvectors(cs) + np.testing.assert_allclose([np.dot(lv, lv) for lv in tgevec], 1.0, rtol=1e-9) + lapl = nx.laplacian_matrix(G) + + def test_create_using(self): + cs = "ddiiddid" + G = nxt.threshold_graph(cs) + pytest.raises( + nx.exception.NetworkXError, + nxt.threshold_graph, + cs, + create_using=nx.DiGraph(), + ) + MG = nxt.threshold_graph(cs, create_using=nx.MultiGraph()) + assert sorted(MG.edges()) == sorted(G.edges()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py new file mode 100644 index 0000000000000000000000000000000000000000..1e256f4bc69389464cfa164f209bc2db713b79ee --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_time_dependent.py @@ -0,0 +1,431 @@ +"""Unit testing for time dependent algorithms.""" + +from datetime import datetime, timedelta + +import pytest + +import networkx as nx + +_delta = timedelta(days=5 * 365) + + +class TestCdIndex: + """Unit testing for the cd index function.""" + + def test_common_graph(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 1) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(1997, 1, 1)}, + 6: {"time": datetime(1998, 1, 1)}, + 7: {"time": datetime(1999, 1, 1)}, + 8: {"time": datetime(1999, 1, 1)}, + 9: {"time": datetime(1998, 1, 1)}, + 10: {"time": datetime(1997, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 4, time_delta=_delta) == 0.17 + + def test_common_graph_with_given_attributes(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 1) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"date": datetime(1992, 1, 1)}, + 1: {"date": datetime(1992, 1, 1)}, + 2: {"date": datetime(1993, 1, 1)}, + 3: {"date": datetime(1993, 1, 1)}, + 4: {"date": datetime(1995, 1, 1)}, + 5: {"date": datetime(1997, 1, 1)}, + 6: {"date": datetime(1998, 1, 1)}, + 7: {"date": datetime(1999, 1, 1)}, + 8: {"date": datetime(1999, 1, 1)}, + 9: {"date": datetime(1998, 1, 1)}, + 10: {"date": datetime(1997, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 4, time_delta=_delta, time="date") == 0.17 + + def test_common_graph_with_int_attributes(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 1) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": 20}, + 1: {"time": 20}, + 2: {"time": 30}, + 3: {"time": 30}, + 4: {"time": 50}, + 5: {"time": 70}, + 6: {"time": 80}, + 7: {"time": 90}, + 8: {"time": 90}, + 9: {"time": 80}, + 10: {"time": 74}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 4, time_delta=50) == 0.17 + + def test_common_graph_with_float_attributes(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 1) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": 20.2}, + 1: {"time": 20.2}, + 2: {"time": 30.7}, + 3: {"time": 30.7}, + 4: {"time": 50.9}, + 5: {"time": 70.1}, + 6: {"time": 80.6}, + 7: {"time": 90.7}, + 8: {"time": 90.7}, + 9: {"time": 80.6}, + 10: {"time": 74.2}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 4, time_delta=50) == 0.17 + + def test_common_graph_with_weights(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 1) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(1997, 1, 1)}, + 6: {"time": datetime(1998, 1, 1), "weight": 5}, + 7: {"time": datetime(1999, 1, 1), "weight": 2}, + 8: {"time": datetime(1999, 1, 1), "weight": 6}, + 9: {"time": datetime(1998, 1, 1), "weight": 3}, + 10: {"time": datetime(1997, 4, 1), "weight": 10}, + } + + nx.set_node_attributes(G, node_attrs) + assert nx.cd_index(G, 4, time_delta=_delta, weight="weight") == 0.04 + + def test_node_with_no_predecessors(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(2005, 1, 1)}, + 6: {"time": datetime(2010, 1, 1)}, + 7: {"time": datetime(2001, 1, 1)}, + 8: {"time": datetime(2020, 1, 1)}, + 9: {"time": datetime(2017, 1, 1)}, + 10: {"time": datetime(2004, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + assert nx.cd_index(G, 4, time_delta=_delta) == 0.0 + + def test_node_with_no_successors(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(8, 2) + G.add_edge(6, 0) + G.add_edge(6, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(1997, 1, 1)}, + 6: {"time": datetime(1998, 1, 1)}, + 7: {"time": datetime(1999, 1, 1)}, + 8: {"time": datetime(1999, 1, 1)}, + 9: {"time": datetime(1998, 1, 1)}, + 10: {"time": datetime(1997, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + assert nx.cd_index(G, 4, time_delta=_delta) == 1.0 + + def test_n_equals_zero(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 3) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(2005, 1, 1)}, + 6: {"time": datetime(2010, 1, 1)}, + 7: {"time": datetime(2001, 1, 1)}, + 8: {"time": datetime(2020, 1, 1)}, + 9: {"time": datetime(2017, 1, 1)}, + 10: {"time": datetime(2004, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + with pytest.raises( + nx.NetworkXError, match="The cd index cannot be defined." + ) as ve: + nx.cd_index(G, 4, time_delta=_delta) + + def test_time_timedelta_compatibility(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(4, 2) + G.add_edge(4, 0) + G.add_edge(4, 3) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": 20.2}, + 1: {"time": 20.2}, + 2: {"time": 30.7}, + 3: {"time": 30.7}, + 4: {"time": 50.9}, + 5: {"time": 70.1}, + 6: {"time": 80.6}, + 7: {"time": 90.7}, + 8: {"time": 90.7}, + 9: {"time": 80.6}, + 10: {"time": 74.2}, + } + + nx.set_node_attributes(G, node_attrs) + + with pytest.raises( + nx.NetworkXError, + match="Addition and comparison are not supported between", + ) as ve: + nx.cd_index(G, 4, time_delta=_delta) + + def test_node_with_no_time(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + G.add_edge(8, 2) + G.add_edge(6, 0) + G.add_edge(6, 3) + G.add_edge(5, 2) + G.add_edge(6, 2) + G.add_edge(6, 4) + G.add_edge(7, 4) + G.add_edge(8, 4) + G.add_edge(9, 4) + G.add_edge(9, 1) + G.add_edge(9, 3) + G.add_edge(10, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 6: {"time": datetime(1998, 1, 1)}, + 7: {"time": datetime(1999, 1, 1)}, + 8: {"time": datetime(1999, 1, 1)}, + 9: {"time": datetime(1998, 1, 1)}, + 10: {"time": datetime(1997, 4, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + with pytest.raises( + nx.NetworkXError, match="Not all nodes have a 'time' attribute." + ) as ve: + nx.cd_index(G, 4, time_delta=_delta) + + def test_maximally_consolidating(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + G.add_edge(5, 1) + G.add_edge(5, 2) + G.add_edge(5, 3) + G.add_edge(5, 4) + G.add_edge(6, 1) + G.add_edge(6, 5) + G.add_edge(7, 1) + G.add_edge(7, 5) + G.add_edge(8, 2) + G.add_edge(8, 5) + G.add_edge(9, 5) + G.add_edge(9, 3) + G.add_edge(10, 5) + G.add_edge(10, 3) + G.add_edge(10, 4) + G.add_edge(11, 5) + G.add_edge(11, 4) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(1997, 1, 1)}, + 6: {"time": datetime(1998, 1, 1)}, + 7: {"time": datetime(1999, 1, 1)}, + 8: {"time": datetime(1999, 1, 1)}, + 9: {"time": datetime(1998, 1, 1)}, + 10: {"time": datetime(1997, 4, 1)}, + 11: {"time": datetime(1998, 5, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 5, time_delta=_delta) == -1 + + def test_maximally_destabilizing(self): + G = nx.DiGraph() + G.add_nodes_from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + G.add_edge(5, 1) + G.add_edge(5, 2) + G.add_edge(5, 3) + G.add_edge(5, 4) + G.add_edge(6, 5) + G.add_edge(7, 5) + G.add_edge(8, 5) + G.add_edge(9, 5) + G.add_edge(10, 5) + G.add_edge(11, 5) + + node_attrs = { + 0: {"time": datetime(1992, 1, 1)}, + 1: {"time": datetime(1992, 1, 1)}, + 2: {"time": datetime(1993, 1, 1)}, + 3: {"time": datetime(1993, 1, 1)}, + 4: {"time": datetime(1995, 1, 1)}, + 5: {"time": datetime(1997, 1, 1)}, + 6: {"time": datetime(1998, 1, 1)}, + 7: {"time": datetime(1999, 1, 1)}, + 8: {"time": datetime(1999, 1, 1)}, + 9: {"time": datetime(1998, 1, 1)}, + 10: {"time": datetime(1997, 4, 1)}, + 11: {"time": datetime(1998, 5, 1)}, + } + + nx.set_node_attributes(G, node_attrs) + + assert nx.cd_index(G, 5, time_delta=_delta) == 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py new file mode 100644 index 0000000000000000000000000000000000000000..34d9b22a65a3b46b91eb1f0b6bbb9780373bbdc9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_tournament.py @@ -0,0 +1,161 @@ +"""Unit tests for the :mod:`networkx.algorithms.tournament` module.""" + +from itertools import combinations + +import pytest + +from networkx import DiGraph +from networkx.algorithms.tournament import ( + hamiltonian_path, + index_satisfying, + is_reachable, + is_strongly_connected, + is_tournament, + random_tournament, + score_sequence, + tournament_matrix, +) + + +def test_condition_not_satisfied(): + iter_in = [0] + assert index_satisfying(iter_in, lambda x: x > 0) == 1 + + +def test_empty_iterable(): + with pytest.raises(ValueError): + index_satisfying([], lambda x: x > 0) + + +def test_is_tournament(): + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)]) + assert is_tournament(G) + + +def test_self_loops(): + """A tournament must have no self-loops.""" + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)]) + G.add_edge(0, 0) + assert not is_tournament(G) + + +def test_missing_edges(): + """A tournament must not have any pair of nodes without at least + one edge joining the pair. + + """ + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3)]) + assert not is_tournament(G) + + +def test_bidirectional_edges(): + """A tournament must not have any pair of nodes with greater + than one edge joining the pair. + + """ + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)]) + G.add_edge(1, 0) + assert not is_tournament(G) + + +def test_graph_is_tournament(): + for _ in range(10): + G = random_tournament(5) + assert is_tournament(G) + + +def test_graph_is_tournament_seed(): + for _ in range(10): + G = random_tournament(5, seed=1) + assert is_tournament(G) + + +def test_graph_is_tournament_one_node(): + G = random_tournament(1) + assert is_tournament(G) + + +def test_graph_is_tournament_zero_node(): + G = random_tournament(0) + assert is_tournament(G) + + +def test_hamiltonian_empty_graph(): + path = hamiltonian_path(DiGraph()) + assert len(path) == 0 + + +def test_path_is_hamiltonian(): + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)]) + path = hamiltonian_path(G) + assert len(path) == 4 + assert all(v in G[u] for u, v in zip(path, path[1:])) + + +def test_hamiltonian_cycle(): + """Tests that :func:`networkx.tournament.hamiltonian_path` + returns a Hamiltonian cycle when provided a strongly connected + tournament. + + """ + G = DiGraph() + G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (0, 2)]) + path = hamiltonian_path(G) + assert len(path) == 4 + assert all(v in G[u] for u, v in zip(path, path[1:])) + assert path[0] in G[path[-1]] + + +def test_score_sequence_edge(): + G = DiGraph([(0, 1)]) + assert score_sequence(G) == [0, 1] + + +def test_score_sequence_triangle(): + G = DiGraph([(0, 1), (1, 2), (2, 0)]) + assert score_sequence(G) == [1, 1, 1] + + +def test_tournament_matrix(): + np = pytest.importorskip("numpy") + pytest.importorskip("scipy") + npt = np.testing + G = DiGraph([(0, 1)]) + m = tournament_matrix(G) + npt.assert_array_equal(m.todense(), np.array([[0, 1], [-1, 0]])) + + +def test_reachable_pair(): + """Tests for a reachable pair of nodes.""" + G = DiGraph([(0, 1), (1, 2), (2, 0)]) + assert is_reachable(G, 0, 2) + + +def test_same_node_is_reachable(): + """Tests that a node is always reachable from it.""" + # G is an arbitrary tournament on ten nodes. + G = DiGraph(sorted(p) for p in combinations(range(10), 2)) + assert all(is_reachable(G, v, v) for v in G) + + +def test_unreachable_pair(): + """Tests for an unreachable pair of nodes.""" + G = DiGraph([(0, 1), (0, 2), (1, 2)]) + assert not is_reachable(G, 1, 0) + + +def test_is_strongly_connected(): + """Tests for a strongly connected tournament.""" + G = DiGraph([(0, 1), (1, 2), (2, 0)]) + assert is_strongly_connected(G) + + +def test_not_strongly_connected(): + """Tests for a tournament that is not strongly connected.""" + G = DiGraph([(0, 1), (0, 2), (1, 2)]) + assert not is_strongly_connected(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py new file mode 100644 index 0000000000000000000000000000000000000000..cdfaf3be2de9ce9bba69bf0d5734c4c8e3716d3f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_triads.py @@ -0,0 +1,248 @@ +"""Tests for the :mod:`networkx.algorithms.triads` module.""" + +import itertools +from collections import defaultdict +from random import sample + +import pytest + +import networkx as nx + + +def test_triadic_census(): + """Tests the triadic_census function.""" + G = nx.DiGraph() + G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"]) + expected = { + "030T": 2, + "120C": 1, + "210": 0, + "120U": 0, + "012": 9, + "102": 3, + "021U": 0, + "111U": 0, + "003": 8, + "030C": 0, + "021D": 9, + "201": 0, + "111D": 1, + "300": 0, + "120D": 0, + "021C": 2, + } + actual = nx.triadic_census(G) + assert expected == actual + + +def test_is_triad(): + """Tests the is_triad function""" + G = nx.karate_club_graph() + G = G.to_directed() + for i in range(100): + nodes = sample(sorted(G.nodes()), 3) + G2 = G.subgraph(nodes) + assert nx.is_triad(G2) + + +def test_all_triads(): + """Tests the all_triads function.""" + G = nx.DiGraph() + G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"]) + expected = [ + f"{i},{j},{k}" + for i in range(7) + for j in range(i + 1, 7) + for k in range(j + 1, 7) + ] + expected = [G.subgraph(x.split(",")) for x in expected] + actual = list(nx.all_triads(G)) + assert all(any(nx.is_isomorphic(G1, G2) for G1 in expected) for G2 in actual) + + +def test_triad_type(): + """Tests the triad_type function.""" + # 0 edges (1 type) + G = nx.DiGraph({0: [], 1: [], 2: []}) + assert nx.triad_type(G) == "003" + # 1 edge (1 type) + G = nx.DiGraph({0: [1], 1: [], 2: []}) + assert nx.triad_type(G) == "012" + # 2 edges (4 types) + G = nx.DiGraph([(0, 1), (0, 2)]) + assert nx.triad_type(G) == "021D" + G = nx.DiGraph({0: [1], 1: [0], 2: []}) + assert nx.triad_type(G) == "102" + G = nx.DiGraph([(0, 1), (2, 1)]) + assert nx.triad_type(G) == "021U" + G = nx.DiGraph([(0, 1), (1, 2)]) + assert nx.triad_type(G) == "021C" + # 3 edges (4 types) + G = nx.DiGraph([(0, 1), (1, 0), (2, 1)]) + assert nx.triad_type(G) == "111D" + G = nx.DiGraph([(0, 1), (1, 0), (1, 2)]) + assert nx.triad_type(G) == "111U" + G = nx.DiGraph([(0, 1), (1, 2), (0, 2)]) + assert nx.triad_type(G) == "030T" + G = nx.DiGraph([(0, 1), (1, 2), (2, 0)]) + assert nx.triad_type(G) == "030C" + # 4 edges (4 types) + G = nx.DiGraph([(0, 1), (1, 0), (2, 0), (0, 2)]) + assert nx.triad_type(G) == "201" + G = nx.DiGraph([(0, 1), (1, 0), (2, 0), (2, 1)]) + assert nx.triad_type(G) == "120D" + G = nx.DiGraph([(0, 1), (1, 0), (0, 2), (1, 2)]) + assert nx.triad_type(G) == "120U" + G = nx.DiGraph([(0, 1), (1, 0), (0, 2), (2, 1)]) + assert nx.triad_type(G) == "120C" + # 5 edges (1 type) + G = nx.DiGraph([(0, 1), (1, 0), (2, 1), (1, 2), (0, 2)]) + assert nx.triad_type(G) == "210" + # 6 edges (1 type) + G = nx.DiGraph([(0, 1), (1, 0), (1, 2), (2, 1), (0, 2), (2, 0)]) + assert nx.triad_type(G) == "300" + + +def test_triads_by_type(): + G = nx.DiGraph() + G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"]) + all_triads = nx.all_triads(G) + expected = defaultdict(list) + for triad in all_triads: + name = nx.triad_type(triad) + expected[name].append(triad) + actual = nx.triads_by_type(G) + assert set(actual.keys()) == set(expected.keys()) + for tri_type, actual_Gs in actual.items(): + expected_Gs = expected[tri_type] + for a in actual_Gs: + assert any(nx.is_isomorphic(a, e) for e in expected_Gs) + + +def test_triadic_census_short_path_nodelist(): + G = nx.path_graph("abc", create_using=nx.DiGraph) + expected = {"021C": 1} + for nl in ["a", "b", "c", "ab", "ac", "bc", "abc"]: + triad_census = nx.triadic_census(G, nodelist=nl) + assert expected == {typ: cnt for typ, cnt in triad_census.items() if cnt > 0} + + +def test_triadic_census_correct_nodelist_values(): + G = nx.path_graph(5, create_using=nx.DiGraph) + msg = r"nodelist includes duplicate nodes or nodes not in G" + with pytest.raises(ValueError, match=msg): + nx.triadic_census(G, [1, 2, 2, 3]) + with pytest.raises(ValueError, match=msg): + nx.triadic_census(G, [1, 2, "a", 3]) + + +def test_triadic_census_tiny_graphs(): + tc = nx.triadic_census(nx.empty_graph(0, create_using=nx.DiGraph)) + assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0} + tc = nx.triadic_census(nx.empty_graph(1, create_using=nx.DiGraph)) + assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0} + tc = nx.triadic_census(nx.empty_graph(2, create_using=nx.DiGraph)) + assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0} + tc = nx.triadic_census(nx.DiGraph([(1, 2)])) + assert {} == {typ: cnt for typ, cnt in tc.items() if cnt > 0} + + +def test_triadic_census_selfloops(): + GG = nx.path_graph("abc", create_using=nx.DiGraph) + expected = {"021C": 1} + for n in GG: + G = GG.copy() + G.add_edge(n, n) + tc = nx.triadic_census(G) + assert expected == {typ: cnt for typ, cnt in tc.items() if cnt > 0} + + GG = nx.path_graph("abcde", create_using=nx.DiGraph) + tbt = nx.triads_by_type(GG) + for n in GG: + GG.add_edge(n, n) + tc = nx.triadic_census(GG) + assert tc == {tt: len(tbt[tt]) for tt in tc} + + +def test_triadic_census_four_path(): + G = nx.path_graph("abcd", create_using=nx.DiGraph) + expected = {"012": 2, "021C": 2} + triad_census = nx.triadic_census(G) + assert expected == {typ: cnt for typ, cnt in triad_census.items() if cnt > 0} + + +def test_triadic_census_four_path_nodelist(): + G = nx.path_graph("abcd", create_using=nx.DiGraph) + expected_end = {"012": 2, "021C": 1} + expected_mid = {"012": 1, "021C": 2} + a_triad_census = nx.triadic_census(G, nodelist=["a"]) + assert expected_end == {typ: cnt for typ, cnt in a_triad_census.items() if cnt > 0} + b_triad_census = nx.triadic_census(G, nodelist=["b"]) + assert expected_mid == {typ: cnt for typ, cnt in b_triad_census.items() if cnt > 0} + c_triad_census = nx.triadic_census(G, nodelist=["c"]) + assert expected_mid == {typ: cnt for typ, cnt in c_triad_census.items() if cnt > 0} + d_triad_census = nx.triadic_census(G, nodelist=["d"]) + assert expected_end == {typ: cnt for typ, cnt in d_triad_census.items() if cnt > 0} + + +def test_triadic_census_nodelist(): + """Tests the triadic_census function.""" + G = nx.DiGraph() + G.add_edges_from(["01", "02", "03", "04", "05", "12", "16", "51", "56", "65"]) + expected = { + "030T": 2, + "120C": 1, + "210": 0, + "120U": 0, + "012": 9, + "102": 3, + "021U": 0, + "111U": 0, + "003": 8, + "030C": 0, + "021D": 9, + "201": 0, + "111D": 1, + "300": 0, + "120D": 0, + "021C": 2, + } + actual = {k: 0 for k in expected} + for node in G.nodes(): + node_triad_census = nx.triadic_census(G, nodelist=[node]) + for triad_key in expected: + actual[triad_key] += node_triad_census[triad_key] + # Divide all counts by 3 + for k, v in actual.items(): + actual[k] //= 3 + assert expected == actual + + +@pytest.mark.parametrize("N", [5, 10]) +def test_triadic_census_on_random_graph(N): + G = nx.binomial_graph(N, 0.3, directed=True, seed=42) + tc1 = nx.triadic_census(G) + tbt = nx.triads_by_type(G) + tc2 = {tt: len(tbt[tt]) for tt in tc1} + assert tc1 == tc2 + + for n in G: + tc1 = nx.triadic_census(G, nodelist={n}) + tc2 = {tt: sum(1 for t in tbt.get(tt, []) if n in t) for tt in tc1} + assert tc1 == tc2 + + for ns in itertools.combinations(G, 2): + ns = set(ns) + tc1 = nx.triadic_census(G, nodelist=ns) + tc2 = { + tt: sum(1 for t in tbt.get(tt, []) if any(n in ns for n in t)) for tt in tc1 + } + assert tc1 == tc2 + + for ns in itertools.combinations(G, 3): + ns = set(ns) + tc1 = nx.triadic_census(G, nodelist=ns) + tc2 = { + tt: sum(1 for t in tbt.get(tt, []) if any(n in ns for n in t)) for tt in tc1 + } + assert tc1 == tc2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py new file mode 100644 index 0000000000000000000000000000000000000000..248206e670fa911f62177bb6727d6a7a6df1e6b9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_vitality.py @@ -0,0 +1,41 @@ +import networkx as nx + + +class TestClosenessVitality: + def test_unweighted(self): + G = nx.cycle_graph(3) + vitality = nx.closeness_vitality(G) + assert vitality == {0: 2, 1: 2, 2: 2} + + def test_weighted(self): + G = nx.Graph() + nx.add_cycle(G, [0, 1, 2], weight=2) + vitality = nx.closeness_vitality(G, weight="weight") + assert vitality == {0: 4, 1: 4, 2: 4} + + def test_unweighted_digraph(self): + G = nx.DiGraph(nx.cycle_graph(3)) + vitality = nx.closeness_vitality(G) + assert vitality == {0: 4, 1: 4, 2: 4} + + def test_weighted_digraph(self): + G = nx.DiGraph() + nx.add_cycle(G, [0, 1, 2], weight=2) + nx.add_cycle(G, [2, 1, 0], weight=2) + vitality = nx.closeness_vitality(G, weight="weight") + assert vitality == {0: 8, 1: 8, 2: 8} + + def test_weighted_multidigraph(self): + G = nx.MultiDiGraph() + nx.add_cycle(G, [0, 1, 2], weight=2) + nx.add_cycle(G, [2, 1, 0], weight=2) + vitality = nx.closeness_vitality(G, weight="weight") + assert vitality == {0: 8, 1: 8, 2: 8} + + def test_disconnecting_graph(self): + """Tests that the closeness vitality of a node whose removal + disconnects the graph is negative infinity. + + """ + G = nx.path_graph(3) + assert nx.closeness_vitality(G, node=1) == -float("inf") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py new file mode 100644 index 0000000000000000000000000000000000000000..3269ae62a023ff0cf9fdc55122cb6e7c8d2ba319 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_voronoi.py @@ -0,0 +1,103 @@ +import networkx as nx +from networkx.utils import pairwise + + +class TestVoronoiCells: + """Unit tests for the Voronoi cells function.""" + + def test_isolates(self): + """Tests that a graph with isolated nodes has all isolates in + one block of the partition. + + """ + G = nx.empty_graph(5) + cells = nx.voronoi_cells(G, {0, 2, 4}) + expected = {0: {0}, 2: {2}, 4: {4}, "unreachable": {1, 3}} + assert expected == cells + + def test_undirected_unweighted(self): + G = nx.cycle_graph(6) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0, 1, 5}, 3: {2, 3, 4}} + assert expected == cells + + def test_directed_unweighted(self): + # This is the singly-linked directed cycle graph on six nodes. + G = nx.DiGraph(pairwise(range(6), cyclic=True)) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0, 1, 2}, 3: {3, 4, 5}} + assert expected == cells + + def test_directed_inward(self): + """Tests that reversing the graph gives the "inward" Voronoi + partition. + + """ + # This is the singly-linked reverse directed cycle graph on six nodes. + G = nx.DiGraph(pairwise(range(6), cyclic=True)) + G = G.reverse(copy=False) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0, 4, 5}, 3: {1, 2, 3}} + assert expected == cells + + def test_undirected_weighted(self): + edges = [(0, 1, 10), (1, 2, 1), (2, 3, 1)] + G = nx.Graph() + G.add_weighted_edges_from(edges) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0}, 3: {1, 2, 3}} + assert expected == cells + + def test_directed_weighted(self): + edges = [(0, 1, 10), (1, 2, 1), (2, 3, 1), (3, 2, 1), (2, 1, 1)] + G = nx.DiGraph() + G.add_weighted_edges_from(edges) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0}, 3: {1, 2, 3}} + assert expected == cells + + def test_multigraph_unweighted(self): + """Tests that the Voronoi cells for a multigraph are the same as + for a simple graph. + + """ + edges = [(0, 1), (1, 2), (2, 3)] + G = nx.MultiGraph(2 * edges) + H = nx.Graph(G) + G_cells = nx.voronoi_cells(G, {0, 3}) + H_cells = nx.voronoi_cells(H, {0, 3}) + assert G_cells == H_cells + + def test_multidigraph_unweighted(self): + # This is the twice-singly-linked directed cycle graph on six nodes. + edges = list(pairwise(range(6), cyclic=True)) + G = nx.MultiDiGraph(2 * edges) + H = nx.DiGraph(G) + G_cells = nx.voronoi_cells(G, {0, 3}) + H_cells = nx.voronoi_cells(H, {0, 3}) + assert G_cells == H_cells + + def test_multigraph_weighted(self): + edges = [(0, 1, 10), (0, 1, 10), (1, 2, 1), (1, 2, 100), (2, 3, 1), (2, 3, 100)] + G = nx.MultiGraph() + G.add_weighted_edges_from(edges) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0}, 3: {1, 2, 3}} + assert expected == cells + + def test_multidigraph_weighted(self): + edges = [ + (0, 1, 10), + (0, 1, 10), + (1, 2, 1), + (2, 3, 1), + (3, 2, 10), + (3, 2, 1), + (2, 1, 10), + (2, 1, 1), + ] + G = nx.MultiDiGraph() + G.add_weighted_edges_from(edges) + cells = nx.voronoi_cells(G, {0, 3}) + expected = {0: {0}, 3: {1, 2, 3}} + assert expected == cells diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py new file mode 100644 index 0000000000000000000000000000000000000000..7a6b323932988e1b9513118162df62e9613ee65b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_walks.py @@ -0,0 +1,54 @@ +"""Unit tests for the :mod:`networkx.algorithms.walks` module.""" + +import pytest + +import networkx as nx + +pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +def test_directed(): + G = nx.DiGraph([(0, 1), (1, 2), (2, 0)]) + num_walks = nx.number_of_walks(G, 3) + expected = {0: {0: 1, 1: 0, 2: 0}, 1: {0: 0, 1: 1, 2: 0}, 2: {0: 0, 1: 0, 2: 1}} + assert num_walks == expected + + +def test_undirected(): + G = nx.cycle_graph(3) + num_walks = nx.number_of_walks(G, 3) + expected = {0: {0: 2, 1: 3, 2: 3}, 1: {0: 3, 1: 2, 2: 3}, 2: {0: 3, 1: 3, 2: 2}} + assert num_walks == expected + + +def test_non_integer_nodes(): + G = nx.DiGraph([("A", "B"), ("B", "C"), ("C", "A")]) + num_walks = nx.number_of_walks(G, 2) + expected = { + "A": {"A": 0, "B": 0, "C": 1}, + "B": {"A": 1, "B": 0, "C": 0}, + "C": {"A": 0, "B": 1, "C": 0}, + } + assert num_walks == expected + + +def test_zero_length(): + G = nx.cycle_graph(3) + num_walks = nx.number_of_walks(G, 0) + expected = {0: {0: 1, 1: 0, 2: 0}, 1: {0: 0, 1: 1, 2: 0}, 2: {0: 0, 1: 0, 2: 1}} + assert num_walks == expected + + +def test_negative_length_exception(): + G = nx.cycle_graph(3) + with pytest.raises(ValueError): + nx.number_of_walks(G, -1) + + +def test_hidden_weight_attr(): + G = nx.cycle_graph(3) + G.add_edge(1, 2, weight=5) + num_walks = nx.number_of_walks(G, 3) + expected = {0: {0: 2, 1: 3, 2: 3}, 1: {0: 3, 1: 2, 2: 3}, 2: {0: 3, 1: 3, 2: 2}} + assert num_walks == expected diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py new file mode 100644 index 0000000000000000000000000000000000000000..2bb1f85b1d7f932a1050b43bdc1f422bc7be8aa3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tests/test_wiener.py @@ -0,0 +1,157 @@ +import networkx as nx + + +def test_wiener_index_of_disconnected_graph(): + assert nx.wiener_index(nx.empty_graph(2)) == float("inf") + + +def test_wiener_index_of_directed_graph(): + G = nx.complete_graph(3) + H = nx.DiGraph(G) + assert (2 * nx.wiener_index(G)) == nx.wiener_index(H) + + +def test_wiener_index_of_complete_graph(): + n = 10 + G = nx.complete_graph(n) + assert nx.wiener_index(G) == (n * (n - 1) / 2) + + +def test_wiener_index_of_path_graph(): + # In P_n, there are n - 1 pairs of vertices at distance one, n - + # 2 pairs at distance two, n - 3 at distance three, ..., 1 at + # distance n - 1, so the Wiener index should be + # + # 1 * (n - 1) + 2 * (n - 2) + ... + (n - 2) * 2 + (n - 1) * 1 + # + # For example, in P_5, + # + # 1 * 4 + 2 * 3 + 3 * 2 + 4 * 1 = 2 (1 * 4 + 2 * 3) + # + # and in P_6, + # + # 1 * 5 + 2 * 4 + 3 * 3 + 4 * 2 + 5 * 1 = 2 (1 * 5 + 2 * 4) + 3 * 3 + # + # assuming n is *odd*, this gives the formula + # + # 2 \sum_{i = 1}^{(n - 1) / 2} [i * (n - i)] + # + # assuming n is *even*, this gives the formula + # + # 2 \sum_{i = 1}^{n / 2} [i * (n - i)] - (n / 2) ** 2 + # + n = 9 + G = nx.path_graph(n) + expected = 2 * sum(i * (n - i) for i in range(1, (n // 2) + 1)) + actual = nx.wiener_index(G) + assert expected == actual + + +def test_schultz_and_gutman_index_of_disconnected_graph(): + n = 4 + G = nx.Graph() + G.add_nodes_from(list(range(1, n + 1))) + expected = float("inf") + + G.add_edge(1, 2) + G.add_edge(3, 4) + + actual_1 = nx.schultz_index(G) + actual_2 = nx.gutman_index(G) + + assert expected == actual_1 + assert expected == actual_2 + + +def test_schultz_and_gutman_index_of_complete_bipartite_graph_1(): + n = 3 + m = 3 + cbg = nx.complete_bipartite_graph(n, m) + + expected_1 = n * m * (n + m) + 2 * n * (n - 1) * m + 2 * m * (m - 1) * n + actual_1 = nx.schultz_index(cbg) + + expected_2 = n * m * (n * m) + n * (n - 1) * m * m + m * (m - 1) * n * n + actual_2 = nx.gutman_index(cbg) + + assert expected_1 == actual_1 + assert expected_2 == actual_2 + + +def test_schultz_and_gutman_index_of_complete_bipartite_graph_2(): + n = 2 + m = 5 + cbg = nx.complete_bipartite_graph(n, m) + + expected_1 = n * m * (n + m) + 2 * n * (n - 1) * m + 2 * m * (m - 1) * n + actual_1 = nx.schultz_index(cbg) + + expected_2 = n * m * (n * m) + n * (n - 1) * m * m + m * (m - 1) * n * n + actual_2 = nx.gutman_index(cbg) + + assert expected_1 == actual_1 + assert expected_2 == actual_2 + + +def test_schultz_and_gutman_index_of_complete_graph(): + n = 5 + cg = nx.complete_graph(n) + + expected_1 = n * (n - 1) * (n - 1) + actual_1 = nx.schultz_index(cg) + + assert expected_1 == actual_1 + + expected_2 = n * (n - 1) * (n - 1) * (n - 1) / 2 + actual_2 = nx.gutman_index(cg) + + assert expected_2 == actual_2 + + +def test_schultz_and_gutman_index_of_odd_cycle_graph(): + k = 5 + n = 2 * k + 1 + ocg = nx.cycle_graph(n) + + expected_1 = 2 * n * k * (k + 1) + actual_1 = nx.schultz_index(ocg) + + expected_2 = 2 * n * k * (k + 1) + actual_2 = nx.gutman_index(ocg) + + assert expected_1 == actual_1 + assert expected_2 == actual_2 + + +def test_hyper_wiener_of_complete_graph(): + # In a complete graph K_n, the distance is always 1. + # For K_n, this term is always (1 + 1^2) = 2. + # + # The number of ordered pairs is n * (n - 1). + # The total sum before division is (n * (n - 1)) * 2. + # The final result is therefore ((n * (n - 1)) * 2) / 2, which + # simplifies to n * (n - 1). + n = 5 + G = nx.complete_graph(n) + assert nx.hyper_wiener_index(G) == n * (n - 1) + + +def test_hyper_wiener_of_path_graph(): + G = nx.path_graph(4) + assert nx.hyper_wiener_index(G) == 30.0 + + +def test_hyper_wiener_of_cycle_graph(): + G = nx.cycle_graph(4) + assert nx.hyper_wiener_index(G) == 20.0 + + +def test_hyper_wiener_of_disconnected_graph(): + G = nx.Graph([(0, 1), (2, 3)]) + assert nx.hyper_wiener_index(G) == float("inf") + + +def test_hyper_wiener_of_weighted_graph(): + G = nx.path_graph(3) + G.edges[0, 1]["weight"] = 2 + assert nx.hyper_wiener_index(G, weight="weight") == 20.0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/threshold.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/threshold.py new file mode 100644 index 0000000000000000000000000000000000000000..9d08de1c45b5e49f34c75515defa48d8b1f385a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/threshold.py @@ -0,0 +1,981 @@ +""" +Threshold Graphs - Creation, manipulation and identification. +""" + +from math import sqrt + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = ["is_threshold_graph", "find_threshold_graph"] + + +@nx._dispatchable +def is_threshold_graph(G): + """ + Returns `True` if `G` is a threshold graph. + + Parameters + ---------- + G : NetworkX graph instance + An instance of `Graph`, `DiGraph`, `MultiGraph` or `MultiDiGraph` + + Returns + ------- + bool + `True` if `G` is a threshold graph, `False` otherwise. + + Examples + -------- + >>> from networkx.algorithms.threshold import is_threshold_graph + >>> G = nx.path_graph(3) + >>> is_threshold_graph(G) + True + >>> G = nx.barbell_graph(3, 3) + >>> is_threshold_graph(G) + False + + References + ---------- + .. [1] Threshold graphs: https://en.wikipedia.org/wiki/Threshold_graph + """ + return is_threshold_sequence([d for n, d in G.degree()]) + + +def is_threshold_sequence(degree_sequence): + """ + Returns True if the sequence is a threshold degree sequence. + + Uses the property that a threshold graph must be constructed by + adding either dominating or isolated nodes. Thus, it can be + deconstructed iteratively by removing a node of degree zero or a + node that connects to the remaining nodes. If this deconstruction + fails then the sequence is not a threshold sequence. + """ + ds = degree_sequence[:] # get a copy so we don't destroy original + ds.sort() + while ds: + if ds[0] == 0: # if isolated node + ds.pop(0) # remove it + continue + if ds[-1] != len(ds) - 1: # is the largest degree node dominating? + return False # no, not a threshold degree sequence + ds.pop() # yes, largest is the dominating node + ds = [d - 1 for d in ds] # remove it and decrement all degrees + return True + + +def creation_sequence(degree_sequence, with_labels=False, compact=False): + """ + Determines the creation sequence for the given threshold degree sequence. + + The creation sequence is a list of single characters 'd' + or 'i': 'd' for dominating or 'i' for isolated vertices. + Dominating vertices are connected to all vertices present when it + is added. The first node added is by convention 'd'. + This list can be converted to a string if desired using "".join(cs) + + If with_labels==True: + Returns a list of 2-tuples containing the vertex number + and a character 'd' or 'i' which describes the type of vertex. + + If compact==True: + Returns the creation sequence in a compact form that is the number + of 'i's and 'd's alternating. + Examples: + [1,2,2,3] represents d,i,i,d,d,i,i,i + [3,1,2] represents d,d,d,i,d,d + + Notice that the first number is the first vertex to be used for + construction and so is always 'd'. + + with_labels and compact cannot both be True. + + Returns None if the sequence is not a threshold sequence + """ + if with_labels and compact: + raise ValueError("compact sequences cannot be labeled") + + # make an indexed copy + if isinstance(degree_sequence, dict): # labeled degree sequence + ds = [[degree, label] for (label, degree) in degree_sequence.items()] + else: + ds = [[d, i] for i, d in enumerate(degree_sequence)] + ds.sort() + cs = [] # creation sequence + while ds: + if ds[0][0] == 0: # isolated node + (d, v) = ds.pop(0) + if len(ds) > 0: # make sure we start with a d + cs.insert(0, (v, "i")) + else: + cs.insert(0, (v, "d")) + continue + if ds[-1][0] != len(ds) - 1: # Not dominating node + return None # not a threshold degree sequence + (d, v) = ds.pop() + cs.insert(0, (v, "d")) + ds = [[d[0] - 1, d[1]] for d in ds] # decrement due to removing node + + if with_labels: + return cs + if compact: + return make_compact(cs) + return [v[1] for v in cs] # not labeled + + +def make_compact(creation_sequence): + """ + Returns the creation sequence in a compact form + that is the number of 'i's and 'd's alternating. + + Examples + -------- + >>> from networkx.algorithms.threshold import make_compact + >>> make_compact(["d", "i", "i", "d", "d", "i", "i", "i"]) + [1, 2, 2, 3] + >>> make_compact(["d", "d", "d", "i", "d", "d"]) + [3, 1, 2] + + Notice that the first number is the first vertex + to be used for construction and so is always 'd'. + + Labeled creation sequences lose their labels in the + compact representation. + + >>> make_compact([3, 1, 2]) + [3, 1, 2] + """ + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + cs = creation_sequence[:] + elif isinstance(first, tuple): # labeled creation sequence + cs = [s[1] for s in creation_sequence] + elif isinstance(first, int): # compact creation sequence + return creation_sequence + else: + raise TypeError("Not a valid creation sequence type") + + ccs = [] + count = 1 # count the run lengths of d's or i's. + for i in range(1, len(cs)): + if cs[i] == cs[i - 1]: + count += 1 + else: + ccs.append(count) + count = 1 + ccs.append(count) # don't forget the last one + return ccs + + +def uncompact(creation_sequence): + """ + Converts a compact creation sequence for a threshold + graph to a standard creation sequence (unlabeled). + If the creation_sequence is already standard, return it. + See creation_sequence. + """ + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + return creation_sequence + elif isinstance(first, tuple): # labeled creation sequence + return creation_sequence + elif isinstance(first, int): # compact creation sequence + ccscopy = creation_sequence[:] + else: + raise TypeError("Not a valid creation sequence type") + cs = [] + while ccscopy: + cs.extend(ccscopy.pop(0) * ["d"]) + if ccscopy: + cs.extend(ccscopy.pop(0) * ["i"]) + return cs + + +def creation_sequence_to_weights(creation_sequence): + """ + Returns a list of node weights which create the threshold + graph designated by the creation sequence. The weights + are scaled so that the threshold is 1.0. The order of the + nodes is the same as that in the creation sequence. + """ + # Turn input sequence into a labeled creation sequence + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + if isinstance(creation_sequence, list): + wseq = creation_sequence[:] + else: + wseq = list(creation_sequence) # string like 'ddidid' + elif isinstance(first, tuple): # labeled creation sequence + wseq = [v[1] for v in creation_sequence] + elif isinstance(first, int): # compact creation sequence + wseq = uncompact(creation_sequence) + else: + raise TypeError("Not a valid creation sequence type") + # pass through twice--first backwards + wseq.reverse() + w = 0 + prev = "i" + for j, s in enumerate(wseq): + if s == "i": + wseq[j] = w + prev = s + elif prev == "i": + prev = s + w += 1 + wseq.reverse() # now pass through forwards + for j, s in enumerate(wseq): + if s == "d": + wseq[j] = w + prev = s + elif prev == "d": + prev = s + w += 1 + # Now scale weights + if prev == "d": + w += 1 + wscale = 1 / w + return [ww * wscale for ww in wseq] + # return wseq + + +def weights_to_creation_sequence( + weights, threshold=1, with_labels=False, compact=False +): + """ + Returns a creation sequence for a threshold graph + determined by the weights and threshold given as input. + If the sum of two node weights is greater than the + threshold value, an edge is created between these nodes. + + The creation sequence is a list of single characters 'd' + or 'i': 'd' for dominating or 'i' for isolated vertices. + Dominating vertices are connected to all vertices present + when it is added. The first node added is by convention 'd'. + + If with_labels==True: + Returns a list of 2-tuples containing the vertex number + and a character 'd' or 'i' which describes the type of vertex. + + If compact==True: + Returns the creation sequence in a compact form that is the number + of 'i's and 'd's alternating. + Examples: + [1,2,2,3] represents d,i,i,d,d,i,i,i + [3,1,2] represents d,d,d,i,d,d + + Notice that the first number is the first vertex to be used for + construction and so is always 'd'. + + with_labels and compact cannot both be True. + """ + if with_labels and compact: + raise ValueError("compact sequences cannot be labeled") + + # make an indexed copy + if isinstance(weights, dict): # labeled weights + wseq = [[w, label] for (label, w) in weights.items()] + else: + wseq = [[w, i] for i, w in enumerate(weights)] + wseq.sort() + cs = [] # creation sequence + cutoff = threshold - wseq[-1][0] + while wseq: + if wseq[0][0] < cutoff: # isolated node + (w, label) = wseq.pop(0) + cs.append((label, "i")) + else: + (w, label) = wseq.pop() + cs.append((label, "d")) + cutoff = threshold - wseq[-1][0] + if len(wseq) == 1: # make sure we start with a d + (w, label) = wseq.pop() + cs.append((label, "d")) + # put in correct order + cs.reverse() + + if with_labels: + return cs + if compact: + return make_compact(cs) + return [v[1] for v in cs] # not labeled + + +# Manipulating NetworkX.Graphs in context of threshold graphs +@nx._dispatchable(graphs=None, returns_graph=True) +def threshold_graph(creation_sequence, create_using=None): + """ + Create a threshold graph from the creation sequence or compact + creation_sequence. + + The input sequence can be a + + creation sequence (e.g. ['d','i','d','d','d','i']) + labeled creation sequence (e.g. [(0,'d'),(2,'d'),(1,'i')]) + compact creation sequence (e.g. [2,1,1,2,0]) + + Use cs=creation_sequence(degree_sequence,labeled=True) + to convert a degree sequence to a creation sequence. + + Returns None if the sequence is not valid + """ + # Turn input sequence into a labeled creation sequence + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + ci = list(enumerate(creation_sequence)) + elif isinstance(first, tuple): # labeled creation sequence + ci = creation_sequence[:] + elif isinstance(first, int): # compact creation sequence + cs = uncompact(creation_sequence) + ci = list(enumerate(cs)) + else: + raise ValueError("not a valid creation sequence") + + G = nx.empty_graph(0, create_using) + if G.is_directed(): + raise nx.NetworkXError("Directed Graph not supported") + + G.name = "Threshold Graph" + + # add nodes and edges + # if type is 'i' just add nodea + # if type is a d connect to everything previous + while ci: + (v, node_type) = ci.pop(0) + if node_type == "d": # dominating type, connect to all existing nodes + # We use `for u in list(G):` instead of + # `for u in G:` because we edit the graph `G` in + # the loop. Hence using an iterator will result in + # `RuntimeError: dictionary changed size during iteration` + for u in list(G): + G.add_edge(v, u) + G.add_node(v) + return G + + +@nx._dispatchable +def find_alternating_4_cycle(G): + """ + Returns False if there aren't any alternating 4 cycles. + Otherwise returns the cycle as [a,b,c,d] where (a,b) + and (c,d) are edges and (a,c) and (b,d) are not. + """ + for u, v in G.edges(): + for w in G.nodes(): + if not G.has_edge(u, w) and u != w: + for x in G.neighbors(w): + if not G.has_edge(v, x) and v != x: + return [u, v, w, x] + return False + + +@nx._dispatchable(returns_graph=True) +def find_threshold_graph(G, create_using=None): + """ + Returns a threshold subgraph that is close to largest in `G`. + + The threshold graph will contain the largest degree node in G. + + Parameters + ---------- + G : NetworkX graph instance + An instance of `Graph`, or `MultiDiGraph` + create_using : NetworkX graph class or `None` (default), optional + Type of graph to use when constructing the threshold graph. + If `None`, infer the appropriate graph type from the input. + + Returns + ------- + graph : + A graph instance representing the threshold graph + + Examples + -------- + >>> from networkx.algorithms.threshold import find_threshold_graph + >>> G = nx.barbell_graph(3, 3) + >>> T = find_threshold_graph(G) + >>> T.nodes # may vary + NodeView((7, 8, 5, 6)) + + References + ---------- + .. [1] Threshold graphs: https://en.wikipedia.org/wiki/Threshold_graph + """ + return threshold_graph(find_creation_sequence(G), create_using) + + +@nx._dispatchable +def find_creation_sequence(G): + """ + Find a threshold subgraph that is close to largest in G. + Returns the labeled creation sequence of that threshold graph. + """ + cs = [] + # get a local pointer to the working part of the graph + H = G + while H.order() > 0: + # get new degree sequence on subgraph + dsdict = dict(H.degree()) + ds = [(d, v) for v, d in dsdict.items()] + ds.sort() + # Update threshold graph nodes + if ds[-1][0] == 0: # all are isolated + cs.extend(zip(dsdict, ["i"] * (len(ds) - 1) + ["d"])) + break # Done! + # pull off isolated nodes + while ds[0][0] == 0: + (d, iso) = ds.pop(0) + cs.append((iso, "i")) + # find new biggest node + (d, bigv) = ds.pop() + # add edges of star to t_g + cs.append((bigv, "d")) + # form subgraph of neighbors of big node + H = H.subgraph(H.neighbors(bigv)) + cs.reverse() + return cs + + +# Properties of Threshold Graphs +def triangles(creation_sequence): + """ + Compute number of triangles in the threshold graph with the + given creation sequence. + """ + # shortcut algorithm that doesn't require computing number + # of triangles at each node. + cs = creation_sequence # alias + dr = cs.count("d") # number of d's in sequence + ntri = dr * (dr - 1) * (dr - 2) / 6 # number of triangles in clique of nd d's + # now add dr choose 2 triangles for every 'i' in sequence where + # dr is the number of d's to the right of the current i + for i, typ in enumerate(cs): + if typ == "i": + ntri += dr * (dr - 1) / 2 + else: + dr -= 1 + return ntri + + +def triangle_sequence(creation_sequence): + """ + Return triangle sequence for the given threshold graph creation sequence. + + """ + cs = creation_sequence + seq = [] + dr = cs.count("d") # number of d's to the right of the current pos + dcur = (dr - 1) * (dr - 2) // 2 # number of triangles through a node of clique dr + irun = 0 # number of i's in the last run + drun = 0 # number of d's in the last run + for i, sym in enumerate(cs): + if sym == "d": + drun += 1 + tri = dcur + (dr - 1) * irun # new triangles at this d + else: # cs[i]="i": + if prevsym == "d": # new string of i's + dcur += (dr - 1) * irun # accumulate shared shortest paths + irun = 0 # reset i run counter + dr -= drun # reduce number of d's to right + drun = 0 # reset d run counter + irun += 1 + tri = dr * (dr - 1) // 2 # new triangles at this i + seq.append(tri) + prevsym = sym + return seq + + +def cluster_sequence(creation_sequence): + """ + Return cluster sequence for the given threshold graph creation sequence. + """ + triseq = triangle_sequence(creation_sequence) + degseq = degree_sequence(creation_sequence) + cseq = [] + for i, deg in enumerate(degseq): + tri = triseq[i] + if deg <= 1: # isolated vertex or single pair gets cc 0 + cseq.append(0) + continue + max_size = (deg * (deg - 1)) // 2 + cseq.append(tri / max_size) + return cseq + + +def degree_sequence(creation_sequence): + """ + Return degree sequence for the threshold graph with the given + creation sequence + """ + cs = creation_sequence # alias + seq = [] + rd = cs.count("d") # number of d to the right + for i, sym in enumerate(cs): + if sym == "d": + rd -= 1 + seq.append(rd + i) + else: + seq.append(rd) + return seq + + +def density(creation_sequence): + """ + Return the density of the graph with this creation_sequence. + The density is the fraction of possible edges present. + """ + N = len(creation_sequence) + two_size = sum(degree_sequence(creation_sequence)) + two_possible = N * (N - 1) + den = two_size / two_possible + return den + + +def degree_correlation(creation_sequence): + """ + Return the degree-degree correlation over all edges. + """ + cs = creation_sequence + s1 = 0 # deg_i*deg_j + s2 = 0 # deg_i^2+deg_j^2 + s3 = 0 # deg_i+deg_j + m = 0 # number of edges + rd = cs.count("d") # number of d nodes to the right + rdi = [i for i, sym in enumerate(cs) if sym == "d"] # index of "d"s + ds = degree_sequence(cs) + for i, sym in enumerate(cs): + if sym == "d": + rdi.pop(0) + degi = ds[i] + for dj in rdi: + degj = ds[dj] + s1 += degj * degi + s2 += degi**2 + degj**2 + s3 += degi + degj + m += 1 + denom = 2 * m * s2 - s3 * s3 + numer = 4 * m * s1 - s3 * s3 + if denom == 0: + if numer == 0: + return 1 + raise ValueError(f"Zero Denominator but Numerator is {numer}") + return numer / denom + + +def shortest_path(creation_sequence, u, v): + """ + Find the shortest path between u and v in a + threshold graph G with the given creation_sequence. + + For an unlabeled creation_sequence, the vertices + u and v must be integers in (0,len(sequence)) referring + to the position of the desired vertices in the sequence. + + For a labeled creation_sequence, u and v are labels of vertices. + + Use cs=creation_sequence(degree_sequence,with_labels=True) + to convert a degree sequence to a creation sequence. + + Returns a list of vertices from u to v. + Example: if they are neighbors, it returns [u,v] + """ + # Turn input sequence into a labeled creation sequence + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + cs = [(i, creation_sequence[i]) for i in range(len(creation_sequence))] + elif isinstance(first, tuple): # labeled creation sequence + cs = creation_sequence[:] + elif isinstance(first, int): # compact creation sequence + ci = uncompact(creation_sequence) + cs = [(i, ci[i]) for i in range(len(ci))] + else: + raise TypeError("Not a valid creation sequence type") + + verts = [s[0] for s in cs] + if v not in verts: + raise ValueError(f"Vertex {v} not in graph from creation_sequence") + if u not in verts: + raise ValueError(f"Vertex {u} not in graph from creation_sequence") + # Done checking + if u == v: + return [u] + + uindex = verts.index(u) + vindex = verts.index(v) + bigind = max(uindex, vindex) + if cs[bigind][1] == "d": + return [u, v] + # must be that cs[bigind][1]=='i' + cs = cs[bigind:] + while cs: + vert = cs.pop() + if vert[1] == "d": + return [u, vert[0], v] + # All after u are type 'i' so no connection + return -1 + + +def shortest_path_length(creation_sequence, i): + """ + Return the shortest path length from indicated node to + every other node for the threshold graph with the given + creation sequence. + Node is indicated by index i in creation_sequence unless + creation_sequence is labeled in which case, i is taken to + be the label of the node. + + Paths lengths in threshold graphs are at most 2. + Length to unreachable nodes is set to -1. + """ + # Turn input sequence into a labeled creation sequence + first = creation_sequence[0] + if isinstance(first, str): # creation sequence + if isinstance(creation_sequence, list): + cs = creation_sequence[:] + else: + cs = list(creation_sequence) + elif isinstance(first, tuple): # labeled creation sequence + cs = [v[1] for v in creation_sequence] + i = [v[0] for v in creation_sequence].index(i) + elif isinstance(first, int): # compact creation sequence + cs = uncompact(creation_sequence) + else: + raise TypeError("Not a valid creation sequence type") + + # Compute + N = len(cs) + spl = [2] * N # length 2 to every node + spl[i] = 0 # except self which is 0 + # 1 for all d's to the right + for j in range(i + 1, N): + if cs[j] == "d": + spl[j] = 1 + if cs[i] == "d": # 1 for all nodes to the left + for j in range(i): + spl[j] = 1 + # and -1 for any trailing i to indicate unreachable + for j in range(N - 1, 0, -1): + if cs[j] == "d": + break + spl[j] = -1 + return spl + + +def betweenness_sequence(creation_sequence, normalized=True): + """ + Return betweenness for the threshold graph with the given creation + sequence. The result is unscaled. To scale the values + to the interval [0,1] divide by (n-1)*(n-2). + """ + cs = creation_sequence + seq = [] # betweenness + lastchar = "d" # first node is always a 'd' + dr = float(cs.count("d")) # number of d's to the right of current pos + irun = 0 # number of i's in the last run + drun = 0 # number of d's in the last run + dlast = 0.0 # betweenness of last d + for i, c in enumerate(cs): + if c == "d": # cs[i]=="d": + # betweenness = amt shared with earlier d's and i's + # + new isolated nodes covered + # + new paths to all previous nodes + b = dlast + (irun - 1) * irun / dr + 2 * irun * (i - drun - irun) / dr + drun += 1 # update counter + else: # cs[i]="i": + if lastchar == "d": # if this is a new run of i's + dlast = b # accumulate betweenness + dr -= drun # update number of d's to the right + drun = 0 # reset d counter + irun = 0 # reset i counter + b = 0 # isolated nodes have zero betweenness + irun += 1 # add another i to the run + seq.append(float(b)) + lastchar = c + + # normalize by the number of possible shortest paths + if normalized: + order = len(cs) + scale = 1.0 / ((order - 1) * (order - 2)) + seq = [s * scale for s in seq] + + return seq + + +def eigenvectors(creation_sequence): + """ + Return a 2-tuple of Laplacian eigenvalues and eigenvectors + for the threshold network with creation_sequence. + The first value is a list of eigenvalues. + The second value is a list of eigenvectors. + The lists are in the same order so corresponding eigenvectors + and eigenvalues are in the same position in the two lists. + + Notice that the order of the eigenvalues returned by eigenvalues(cs) + may not correspond to the order of these eigenvectors. + """ + ccs = make_compact(creation_sequence) + N = sum(ccs) + vec = [0] * N + val = vec[:] + # get number of type d nodes to the right (all for first node) + dr = sum(ccs[::2]) + + nn = ccs[0] + vec[0] = [1.0 / sqrt(N)] * N + val[0] = 0 + e = dr + dr -= nn + type_d = True + i = 1 + dd = 1 + while dd < nn: + scale = 1.0 / sqrt(dd * dd + i) + vec[i] = i * [-scale] + [dd * scale] + [0] * (N - i - 1) + val[i] = e + i += 1 + dd += 1 + if len(ccs) == 1: + return (val, vec) + for nn in ccs[1:]: + scale = 1.0 / sqrt(nn * i * (i + nn)) + vec[i] = i * [-nn * scale] + nn * [i * scale] + [0] * (N - i - nn) + # find eigenvalue + type_d = not type_d + if type_d: + e = i + dr + dr -= nn + else: + e = dr + val[i] = e + st = i + i += 1 + dd = 1 + while dd < nn: + scale = 1.0 / sqrt(i - st + dd * dd) + vec[i] = [0] * st + (i - st) * [-scale] + [dd * scale] + [0] * (N - i - 1) + val[i] = e + i += 1 + dd += 1 + return (val, vec) + + +def spectral_projection(u, eigenpairs): + """ + Returns the coefficients of each eigenvector + in a projection of the vector u onto the normalized + eigenvectors which are contained in eigenpairs. + + eigenpairs should be a list of two objects. The + first is a list of eigenvalues and the second a list + of eigenvectors. The eigenvectors should be lists. + + There's not a lot of error checking on lengths of + arrays, etc. so be careful. + """ + coeff = [] + evect = eigenpairs[1] + for ev in evect: + c = sum(evv * uv for (evv, uv) in zip(ev, u)) + coeff.append(c) + return coeff + + +def eigenvalues(creation_sequence): + """ + Return sequence of eigenvalues of the Laplacian of the threshold + graph for the given creation_sequence. + + Based on the Ferrer's diagram method. The spectrum is integral + and is the conjugate of the degree sequence. + + See:: + + @Article{degree-merris-1994, + author = {Russel Merris}, + title = {Degree maximal graphs are Laplacian integral}, + journal = {Linear Algebra Appl.}, + year = {1994}, + volume = {199}, + pages = {381--389}, + } + + """ + degseq = degree_sequence(creation_sequence) + degseq.sort() + eiglist = [] # zero is always one eigenvalue + eig = 0 + row = len(degseq) + bigdeg = degseq.pop() + while row: + if bigdeg < row: + eiglist.append(eig) + row -= 1 + else: + eig += 1 + if degseq: + bigdeg = degseq.pop() + else: + bigdeg = 0 + return eiglist + + +# Threshold graph creation routines + + +@py_random_state(2) +def random_threshold_sequence(n, p, seed=None): + """ + Create a random threshold sequence of size n. + A creation sequence is built by randomly choosing d's with + probability p and i's with probability 1-p. + + s=nx.random_threshold_sequence(10,0.5) + + returns a threshold sequence of length 10 with equal + probably of an i or a d at each position. + + A "random" threshold graph can be built with + + G=nx.threshold_graph(s) + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + """ + if not (0 <= p <= 1): + raise ValueError("p must be in [0,1]") + + cs = ["d"] # threshold sequences always start with a d + for i in range(1, n): + if seed.random() < p: + cs.append("d") + else: + cs.append("i") + return cs + + +# maybe *_d_threshold_sequence routines should +# be (or be called from) a single routine with a more descriptive name +# and a keyword parameter? +def right_d_threshold_sequence(n, m): + """ + Returns a "right-dominated" threshold sequence with `n` vertices and `m` edges. + + Each vertex in the sequence is either dominant or isolated. + In the "right-dominated" version, once the basic sequence is formed, + isolated vertices may be flipped to dominant from the right in order + to reach the target number of edges. + + Parameters + ---------- + n : int + Number of vertices. + m : int + Number of edges. + + Returns + ------- + A list of 'd' (dominant) and 'i' (isolated) forming a right-dominated threshold sequence. + + Raises + ------ + ValueError + If `m` exceeds the maximum number of edges. + + Examples + -------- + >>> from networkx.algorithms.threshold import right_d_threshold_sequence + >>> right_d_threshold_sequence(5, 3) + ['d', 'i', 'i', 'd', 'i'] + """ + + cs = ["d"] + ["i"] * (n - 1) # create sequence with n insolated nodes + + # m n * (n - 1) / 2: + raise ValueError("Too many edges for this many nodes.") + + # connected case m >n-1 + ind = n - 1 + sum = n - 1 + while sum < m: + cs[ind] = "d" + ind -= 1 + sum += ind + ind = m - (sum - ind) + cs[ind] = "d" + return cs + + +def left_d_threshold_sequence(n, m): + """ + Returns a "left-dominated" threshold sequence with `n` vertices and `m` edges. + + Each vertex in the sequence is either dominant or isolated. + In the "left-dominated" version, once the basic sequence is formed, + isolated vertices may be flipped to dominant from the left in order + to reach the target number of edges. + + Parameters + ---------- + n : int + Number of vertices. + m : int + Number of edges. + + Returns + ------- + A list of 'd' (dominant) and 'i' (isolated) forming a left-dominated threshold sequence. + + Raises + ------ + ValueError + If `m` exceeds the maximum number of edges. + + Examples + -------- + For certain small cases, both left and right dominated versions produce + the same sequence. However, for larger values of `m`, the difference in + flipping order becomes evident. For instance, compare the sequences for + ``n=6, m=8``: + + >>> from networkx.algorithms.threshold import left_d_threshold_sequence + >>> seq = left_d_threshold_sequence(6, 8) + >>> seq + ['d', 'd', 'd', 'i', 'i', 'd'] + + In contrast, the right-dominated version yields: + + >>> from networkx.algorithms.threshold import right_d_threshold_sequence + >>> right_seq = right_d_threshold_sequence(6, 8) + >>> right_seq + ['d', 'i', 'i', 'd', 'i', 'd'] + """ + + cs = ["d"] + ["i"] * (n - 1) # create sequence with n insolated nodes + + # m n * (n - 1) / 2: + raise ValueError("Too many edges for this many nodes.") + + # Connected case when M>N-1 + cs[n - 1] = "d" + sum = n - 1 + ind = 1 + while sum < m: + cs[ind] = "d" + sum += ind + ind += 1 + if sum > m: # be sure not to change the first vertex + cs[sum - m] = "i" + return cs diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/time_dependent.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/time_dependent.py new file mode 100644 index 0000000000000000000000000000000000000000..d67cdcf0b8eaecdef8497c77edd3144e96501173 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/time_dependent.py @@ -0,0 +1,142 @@ +"""Time dependent algorithms.""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["cd_index"] + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(node_attrs={"time": None, "weight": 1}) +def cd_index(G, node, time_delta, *, time="time", weight=None): + r"""Compute the CD index for `node` within the graph `G`. + + Calculates the CD index for the given node of the graph, + considering only its predecessors who have the `time` attribute + smaller than or equal to the `time` attribute of the `node` + plus `time_delta`. + + Parameters + ---------- + G : graph + A directed networkx graph whose nodes have `time` attributes and optionally + `weight` attributes (if a weight is not given, it is considered 1). + node : node + The node for which the CD index is calculated. + time_delta : numeric or timedelta + Amount of time after the `time` attribute of the `node`. The value of + `time_delta` must support comparison with the `time` node attribute. For + example, if the `time` attribute of the nodes are `datetime.datetime` + objects, then `time_delta` should be a `datetime.timedelta` object. + time : string (Optional, default is "time") + The name of the node attribute that will be used for the calculations. + weight : string (Optional, default is None) + The name of the node attribute used as weight. + + Returns + ------- + float + The CD index calculated for the node `node` within the graph `G`. + + Raises + ------ + NetworkXError + If not all nodes have a `time` attribute or + `time_delta` and `time` attribute types are not compatible or + `n` equals 0. + + NetworkXNotImplemented + If `G` is a non-directed graph or a multigraph. + + Examples + -------- + >>> from datetime import datetime, timedelta + >>> G = nx.DiGraph() + >>> nodes = { + ... 1: {"time": datetime(2015, 1, 1)}, + ... 2: {"time": datetime(2012, 1, 1), "weight": 4}, + ... 3: {"time": datetime(2010, 1, 1)}, + ... 4: {"time": datetime(2008, 1, 1)}, + ... 5: {"time": datetime(2014, 1, 1)}, + ... } + >>> G.add_nodes_from([(n, nodes[n]) for n in nodes]) + >>> edges = [(1, 3), (1, 4), (2, 3), (3, 4), (3, 5)] + >>> G.add_edges_from(edges) + >>> delta = timedelta(days=5 * 365) + >>> nx.cd_index(G, 3, time_delta=delta, time="time") + 0.5 + >>> nx.cd_index(G, 3, time_delta=delta, time="time", weight="weight") + 0.12 + + Integers can also be used for the time values: + >>> node_times = {1: 2015, 2: 2012, 3: 2010, 4: 2008, 5: 2014} + >>> nx.set_node_attributes(G, node_times, "new_time") + >>> nx.cd_index(G, 3, time_delta=4, time="new_time") + 0.5 + >>> nx.cd_index(G, 3, time_delta=4, time="new_time", weight="weight") + 0.12 + + Notes + ----- + This method implements the algorithm for calculating the CD index, + as described in the paper by Funk and Owen-Smith [1]_. The CD index + is used in order to check how consolidating or destabilizing a patent + is, hence the nodes of the graph represent patents and the edges show + the citations between these patents. The mathematical model is given + below: + + .. math:: + CD_{t}=\frac{1}{n_{t}}\sum_{i=1}^{n}\frac{-2f_{it}b_{it}+f_{it}}{w_{it}}, + + where `f_{it}` equals 1 if `i` cites the focal patent else 0, `b_{it}` equals + 1 if `i` cites any of the focal patents successors else 0, `n_{t}` is the number + of forward citations in `i` and `w_{it}` is a matrix of weight for patent `i` + at time `t`. + + The `datetime.timedelta` package can lead to off-by-one issues when converting + from years to days. In the example above `timedelta(days=5 * 365)` looks like + 5 years, but it isn't because of leap year days. So it gives the same result + as `timedelta(days=4 * 365)`. But using `timedelta(days=5 * 365 + 1)` gives + a 5 year delta **for this choice of years** but may not if the 5 year gap has + more than 1 leap year. To avoid these issues, use integers to represent years, + or be very careful when you convert units of time. + + References + ---------- + .. [1] Funk, Russell J., and Jason Owen-Smith. + "A dynamic network measure of technological change." + Management science 63, no. 3 (2017): 791-817. + http://russellfunk.org/cdindex/static/papers/funk_ms_2017.pdf + + """ + if not all(time in G.nodes[n] for n in G): + raise nx.NetworkXError("Not all nodes have a 'time' attribute.") + + try: + # get target_date + target_date = G.nodes[node][time] + time_delta + # keep the predecessors that existed before the target date + pred = {i for i in G.pred[node] if G.nodes[i][time] <= target_date} + except: + raise nx.NetworkXError( + "Addition and comparison are not supported between 'time_delta' " + "and 'time' types." + ) + + # -1 if any edge between node's predecessors and node's successors, else 1 + b = [-1 if any(j in G[i] for j in G[node]) else 1 for i in pred] + + # n is size of the union of the focal node's predecessors and its successors' predecessors + n = len(pred.union(*(G.pred[s].keys() - {node} for s in G[node]))) + if n == 0: + raise nx.NetworkXError("The cd index cannot be defined.") + + # calculate cd index + if weight is None: + return round(sum(bi for bi in b) / n, 2) + else: + # If a node has the specified weight attribute, its weight is used in the calculation + # otherwise, a weight of 1 is assumed for that node + weights = [G.nodes[i].get(weight, 1) for i in pred] + return round(sum(bi / wt for bi, wt in zip(b, weights)) / n, 2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tournament.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tournament.py new file mode 100644 index 0000000000000000000000000000000000000000..bafaae226e6488c67f0022c2c1cc3808abbca5cf --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/tournament.py @@ -0,0 +1,406 @@ +"""Functions concerning tournament graphs. + +A `tournament graph`_ is a complete oriented graph. In other words, it +is a directed graph in which there is exactly one directed edge joining +each pair of distinct nodes. For each function in this module that +accepts a graph as input, you must provide a tournament graph. The +responsibility is on the caller to ensure that the graph is a tournament +graph: + + >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 0)]) + >>> nx.is_tournament(G) + True + +To access the functions in this module, you must access them through the +:mod:`networkx.tournament` module:: + + >>> nx.tournament.is_reachable(G, 0, 1) + True + +.. _tournament graph: https://en.wikipedia.org/wiki/Tournament_%28graph_theory%29 + +""" + +from itertools import combinations + +import networkx as nx +from networkx.utils import arbitrary_element, not_implemented_for, py_random_state + +__all__ = [ + "hamiltonian_path", + "is_reachable", + "is_strongly_connected", + "is_tournament", + "random_tournament", + "score_sequence", + "tournament_matrix", +] + + +def index_satisfying(iterable, condition): + """Returns the index of the first element in `iterable` that + satisfies the given condition. + + If no such element is found (that is, when the iterable is + exhausted), this returns the length of the iterable (that is, one + greater than the last index of the iterable). + + `iterable` must not be empty. If `iterable` is empty, this + function raises :exc:`ValueError`. + + """ + # Pre-condition: iterable must not be empty. + for i, x in enumerate(iterable): + if condition(x): + return i + # If we reach the end of the iterable without finding an element + # that satisfies the condition, return the length of the iterable, + # which is one greater than the index of its last element. If the + # iterable was empty, `i` will not be defined, so we raise an + # exception. + try: + return i + 1 + except NameError as err: + raise ValueError("iterable must be non-empty") from err + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_tournament(G): + """Returns True if and only if `G` is a tournament. + + A tournament is a directed graph, with neither self-loops nor + multi-edges, in which there is exactly one directed edge joining + each pair of distinct nodes. + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + Returns + ------- + bool + Whether the given graph is a tournament graph. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (1, 2), (2, 0)]) + >>> nx.is_tournament(G) + True + + Notes + ----- + Some definitions require a self-loop on each node, but that is not + the convention used here. + + """ + # In a tournament, there is exactly one directed edge joining each pair. + return ( + all((v in G[u]) ^ (u in G[v]) for u, v in combinations(G, 2)) + and nx.number_of_selfloops(G) == 0 + ) + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable +def hamiltonian_path(G): + """Returns a Hamiltonian path in the given tournament graph. + + Each tournament has a Hamiltonian path. If furthermore, the + tournament is strongly connected, then the returned Hamiltonian path + is a Hamiltonian cycle (by joining the endpoints of the path). + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + Returns + ------- + path : list + A list of nodes which form a Hamiltonian path in `G`. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]) + >>> nx.is_tournament(G) + True + >>> nx.tournament.hamiltonian_path(G) + [0, 1, 2, 3] + + Notes + ----- + This is a recursive implementation with an asymptotic running time + of $O(n^2)$, ignoring multiplicative polylogarithmic factors, where + $n$ is the number of nodes in the graph. + + """ + if len(G) == 0: + return [] + if len(G) == 1: + return [arbitrary_element(G)] + v = arbitrary_element(G) + hampath = hamiltonian_path(G.subgraph(set(G) - {v})) + # Get the index of the first node in the path that does *not* have + # an edge to `v`, then insert `v` before that node. + index = index_satisfying(hampath, lambda u: v not in G[u]) + hampath.insert(index, v) + return hampath + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_tournament(n, seed=None): + r"""Returns a random tournament graph on `n` nodes. + + Parameters + ---------- + n : int + The number of nodes in the returned graph. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : DiGraph + A tournament on `n` nodes, with exactly one directed edge joining + each pair of distinct nodes. + + Notes + ----- + This algorithm adds, for each pair of distinct nodes, an edge with + uniformly random orientation. In other words, `\binom{n}{2}` flips + of an unbiased coin decide the orientations of the edges in the + graph. + + """ + # Flip an unbiased coin for each pair of distinct nodes. + coins = (seed.random() for i in range((n * (n - 1)) // 2)) + pairs = combinations(range(n), 2) + edges = ((u, v) if r < 0.5 else (v, u) for (u, v), r in zip(pairs, coins)) + return nx.DiGraph(edges) + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable +def score_sequence(G): + """Returns the score sequence for the given tournament graph. + + The score sequence is the sorted list of the out-degrees of the + nodes of the graph. + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + Returns + ------- + list + A sorted list of the out-degrees of the nodes of `G`. + + Examples + -------- + >>> G = nx.DiGraph([(1, 0), (1, 3), (0, 2), (0, 3), (2, 1), (3, 2)]) + >>> nx.is_tournament(G) + True + >>> nx.tournament.score_sequence(G) + [1, 1, 2, 2] + + """ + return sorted(d for v, d in G.out_degree()) + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}}) +def tournament_matrix(G): + r"""Returns the tournament matrix for the given tournament graph. + + This function requires SciPy. + + The *tournament matrix* of a tournament graph with edge set *E* is + the matrix *T* defined by + + .. math:: + + T_{i j} = + \begin{cases} + +1 & \text{if } (i, j) \in E \\ + -1 & \text{if } (j, i) \in E \\ + 0 & \text{if } i == j. + \end{cases} + + An equivalent definition is `T = A - A^T`, where *A* is the + adjacency matrix of the graph `G`. + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + Returns + ------- + SciPy sparse array + The tournament matrix of the tournament graph `G`. + + Raises + ------ + ImportError + If SciPy is not available. + + """ + A = nx.adjacency_matrix(G) + return A - A.T + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable +def is_reachable(G, s, t): + """Decides whether there is a path from `s` to `t` in the + tournament. + + This function is more theoretically efficient than the reachability + checks than the shortest path algorithms in + :mod:`networkx.algorithms.shortest_paths`. + + The given graph **must** be a tournament, otherwise this function's + behavior is undefined. + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + s : node + A node in the graph. + + t : node + A node in the graph. + + Returns + ------- + bool + Whether there is a path from `s` to `t` in `G`. + + Examples + -------- + >>> G = nx.DiGraph([(1, 0), (1, 3), (1, 2), (2, 3), (2, 0), (3, 0)]) + >>> nx.is_tournament(G) + True + >>> nx.tournament.is_reachable(G, 1, 3) + True + >>> nx.tournament.is_reachable(G, 3, 2) + False + + Notes + ----- + Although this function is more theoretically efficient than the + generic shortest path functions, a speedup requires the use of + parallelism. Though it may in the future, the current implementation + does not use parallelism, thus you may not see much of a speedup. + + This algorithm comes from [1]. + + References + ---------- + .. [1] Tantau, Till. + "A note on the complexity of the reachability problem for + tournaments." + *Electronic Colloquium on Computational Complexity*. 2001. + + """ + + def two_neighborhood(G, v): + """Returns the set of nodes at distance at most two from `v`. + + `G` must be a graph and `v` a node in that graph. + + The returned set includes the nodes at distance zero (that is, + the node `v` itself), the nodes at distance one (that is, the + out-neighbors of `v`), and the nodes at distance two. + + """ + v_adj = G._adj[v] + return { + x + for x, x_pred in G._pred.items() + if x == v or x in v_adj or any(z in v_adj for z in x_pred) + } + + def is_closed(G, S): + """Decides whether the given set of nodes is closed. + + A set *S* of nodes is *closed* if for each node *u* in the graph + not in *S* and for each node *v* in *S*, there is an edge from + *u* to *v*. + + """ + return all(u in S or all(v in unbrs for v in S) for u, unbrs in G._adj.items()) + + neighborhoods = (two_neighborhood(G, v) for v in G) + return not any(s in S and t not in S and is_closed(G, S) for S in neighborhoods) + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(name="tournament_is_strongly_connected") +def is_strongly_connected(G): + """Decides whether the given tournament is strongly connected. + + This function is more theoretically efficient than the + :func:`~networkx.algorithms.components.is_strongly_connected` + function. + + The given graph **must** be a tournament, otherwise this function's + behavior is undefined. + + Parameters + ---------- + G : NetworkX graph + A directed graph representing a tournament. + + Returns + ------- + bool + Whether the tournament is strongly connected. + + Examples + -------- + >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (3, 0)]) + >>> nx.is_tournament(G) + True + >>> nx.tournament.is_strongly_connected(G) + True + >>> G.remove_edge(3, 0) + >>> G.add_edge(0, 3) + >>> nx.is_tournament(G) + True + >>> nx.tournament.is_strongly_connected(G) + False + + Notes + ----- + Although this function is more theoretically efficient than the + generic strong connectivity function, a speedup requires the use of + parallelism. Though it may in the future, the current implementation + does not use parallelism, thus you may not see much of a speedup. + + This algorithm comes from [1]. + + References + ---------- + .. [1] Tantau, Till. + "A note on the complexity of the reachability problem for + tournaments." + *Electronic Colloquium on Computational Complexity*. 2001. + + + """ + return all(is_reachable(G, u, v) for u in G for v in G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/triads.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/triads.py new file mode 100644 index 0000000000000000000000000000000000000000..b6d36a75cff3995b79bc189bd6b8cb7a7f186309 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/triads.py @@ -0,0 +1,500 @@ +# See https://github.com/networkx/networkx/pull/1474 +# Copyright 2011 Reya Group +# Copyright 2011 Alex Levenson +# Copyright 2011 Diederik van Liere +"""Functions for analyzing triads of a graph.""" + +from collections import defaultdict +from itertools import combinations, permutations + +import networkx as nx +from networkx.utils import not_implemented_for, py_random_state + +__all__ = [ + "triadic_census", + "is_triad", + "all_triads", + "triads_by_type", + "triad_type", +] + +#: The integer codes representing each type of triad. +#: +#: Triads that are the same up to symmetry have the same code. +TRICODES = ( + 1, + 2, + 2, + 3, + 2, + 4, + 6, + 8, + 2, + 6, + 5, + 7, + 3, + 8, + 7, + 11, + 2, + 6, + 4, + 8, + 5, + 9, + 9, + 13, + 6, + 10, + 9, + 14, + 7, + 14, + 12, + 15, + 2, + 5, + 6, + 7, + 6, + 9, + 10, + 14, + 4, + 9, + 9, + 12, + 8, + 13, + 14, + 15, + 3, + 7, + 8, + 11, + 7, + 12, + 14, + 15, + 8, + 14, + 13, + 15, + 11, + 15, + 15, + 16, +) + +#: The names of each type of triad. The order of the elements is +#: important: it corresponds to the tricodes given in :data:`TRICODES`. +TRIAD_NAMES = ( + "003", + "012", + "102", + "021D", + "021U", + "021C", + "111D", + "111U", + "030T", + "030C", + "201", + "120D", + "120U", + "120C", + "210", + "300", +) + + +#: A dictionary mapping triad code to triad name. +TRICODE_TO_NAME = {i: TRIAD_NAMES[code - 1] for i, code in enumerate(TRICODES)} + + +def _tricode(G, v, u, w): + """Returns the integer code of the given triad. + + This is some fancy magic that comes from Batagelj and Mrvar's paper. It + treats each edge joining a pair of `v`, `u`, and `w` as a bit in + the binary representation of an integer. + + """ + combos = ((v, u, 1), (u, v, 2), (v, w, 4), (w, v, 8), (u, w, 16), (w, u, 32)) + return sum(x for u, v, x in combos if v in G[u]) + + +@not_implemented_for("undirected") +@nx._dispatchable +def triadic_census(G, nodelist=None): + """Determines the triadic census of a directed graph. + + The triadic census is a count of how many of the 16 possible types of + triads are present in a directed graph. If a list of nodes is passed, then + only those triads are taken into account which have elements of nodelist in them. + + Parameters + ---------- + G : digraph + A NetworkX DiGraph + nodelist : list + List of nodes for which you want to calculate triadic census + + Returns + ------- + census : dict + Dictionary with triad type as keys and number of occurrences as values. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)]) + >>> triadic_census = nx.triadic_census(G) + >>> for key, value in triadic_census.items(): + ... print(f"{key}: {value}") + 003: 0 + 012: 0 + 102: 0 + 021D: 0 + 021U: 0 + 021C: 0 + 111D: 0 + 111U: 0 + 030T: 2 + 030C: 2 + 201: 0 + 120D: 0 + 120U: 0 + 120C: 0 + 210: 0 + 300: 0 + + Notes + ----- + This algorithm has complexity $O(m)$ where $m$ is the number of edges in + the graph. + + For undirected graphs, the triadic census can be computed by first converting + the graph into a directed graph using the ``G.to_directed()`` method. + After this conversion, only the triad types 003, 102, 201 and 300 will be + present in the undirected scenario. + + Raises + ------ + ValueError + If `nodelist` contains duplicate nodes or nodes not in `G`. + If you want to ignore this you can preprocess with `set(nodelist) & G.nodes` + + See also + -------- + triad_graph + + References + ---------- + .. [1] Vladimir Batagelj and Andrej Mrvar, A subquadratic triad census + algorithm for large sparse networks with small maximum degree, + University of Ljubljana, + http://vlado.fmf.uni-lj.si/pub/networks/doc/triads/triads.pdf + + """ + nodeset = set(G.nbunch_iter(nodelist)) + if nodelist is not None and len(nodelist) != len(nodeset): + raise ValueError("nodelist includes duplicate nodes or nodes not in G") + + N = len(G) + Nnot = N - len(nodeset) # can signal special counting for subset of nodes + + # create an ordering of nodes with nodeset nodes first + m = {n: i for i, n in enumerate(nodeset)} + if Nnot: + # add non-nodeset nodes later in the ordering + not_nodeset = G.nodes - nodeset + m.update((n, i + N) for i, n in enumerate(not_nodeset)) + + # build all_neighbor dicts for easy counting + # After Python 3.8 can leave off these keys(). Speedup also using G._pred + # nbrs = {n: G._pred[n].keys() | G._succ[n].keys() for n in G} + nbrs = {n: G.pred[n].keys() | G.succ[n].keys() for n in G} + dbl_nbrs = {n: G.pred[n].keys() & G.succ[n].keys() for n in G} + + if Nnot: + sgl_nbrs = {n: G.pred[n].keys() ^ G.succ[n].keys() for n in not_nodeset} + # find number of edges not incident to nodes in nodeset + sgl = sum(1 for n in not_nodeset for nbr in sgl_nbrs[n] if nbr not in nodeset) + sgl_edges_outside = sgl // 2 + dbl = sum(1 for n in not_nodeset for nbr in dbl_nbrs[n] if nbr not in nodeset) + dbl_edges_outside = dbl // 2 + + # Initialize the count for each triad to be zero. + census = {name: 0 for name in TRIAD_NAMES} + # Main loop over nodes + for v in nodeset: + vnbrs = nbrs[v] + dbl_vnbrs = dbl_nbrs[v] + if Nnot: + # set up counts of edges attached to v. + sgl_unbrs_bdy = sgl_unbrs_out = dbl_unbrs_bdy = dbl_unbrs_out = 0 + for u in vnbrs: + if m[u] <= m[v]: + continue + unbrs = nbrs[u] + neighbors = (vnbrs | unbrs) - {u, v} + # Count connected triads. + for w in neighbors: + if m[u] < m[w] or (m[v] < m[w] < m[u] and v not in nbrs[w]): + code = _tricode(G, v, u, w) + census[TRICODE_TO_NAME[code]] += 1 + + # Use a formula for dyadic triads with edge incident to v + if u in dbl_vnbrs: + census["102"] += N - len(neighbors) - 2 + else: + census["012"] += N - len(neighbors) - 2 + + # Count edges attached to v. Subtract later to get triads with v isolated + # _out are (u,unbr) for unbrs outside boundary of nodeset + # _bdy are (u,unbr) for unbrs on boundary of nodeset (get double counted) + if Nnot and u not in nodeset: + sgl_unbrs = sgl_nbrs[u] + sgl_unbrs_bdy += len(sgl_unbrs & vnbrs - nodeset) + sgl_unbrs_out += len(sgl_unbrs - vnbrs - nodeset) + dbl_unbrs = dbl_nbrs[u] + dbl_unbrs_bdy += len(dbl_unbrs & vnbrs - nodeset) + dbl_unbrs_out += len(dbl_unbrs - vnbrs - nodeset) + # if nodeset == G.nodes, skip this b/c we will find the edge later. + if Nnot: + # Count edges outside nodeset not connected with v (v isolated triads) + census["012"] += sgl_edges_outside - (sgl_unbrs_out + sgl_unbrs_bdy // 2) + census["102"] += dbl_edges_outside - (dbl_unbrs_out + dbl_unbrs_bdy // 2) + + # calculate null triads: "003" + # null triads = total number of possible triads - all found triads + total_triangles = (N * (N - 1) * (N - 2)) // 6 + triangles_without_nodeset = (Nnot * (Nnot - 1) * (Nnot - 2)) // 6 + total_census = total_triangles - triangles_without_nodeset + census["003"] = total_census - sum(census.values()) + + return census + + +@nx._dispatchable +def is_triad(G): + """Returns True if the graph G is a triad, else False. + + Parameters + ---------- + G : graph + A NetworkX Graph + + Returns + ------- + istriad : boolean + Whether G is a valid triad + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + >>> nx.is_triad(G) + True + >>> G.add_edge(0, 1) + >>> nx.is_triad(G) + False + """ + if isinstance(G, nx.Graph): + if G.order() == 3 and nx.is_directed(G): + if not any((n, n) in G.edges() for n in G.nodes()): + return True + return False + + +@not_implemented_for("undirected") +@nx._dispatchable(returns_graph=True) +def all_triads(G): + """A generator of all possible triads in G. + + Parameters + ---------- + G : digraph + A NetworkX DiGraph + + Returns + ------- + all_triads : generator of DiGraphs + Generator of triads (order-3 DiGraphs) + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)]) + >>> for triad in nx.all_triads(G): + ... print(triad.edges) + [(1, 2), (2, 3), (3, 1)] + [(1, 2), (4, 1), (4, 2)] + [(3, 1), (3, 4), (4, 1)] + [(2, 3), (3, 4), (4, 2)] + + """ + triplets = combinations(G.nodes(), 3) + for triplet in triplets: + yield G.subgraph(triplet).copy() + + +@not_implemented_for("undirected") +@nx._dispatchable +def triads_by_type(G): + """Returns a list of all triads for each triad type in a directed graph. + There are exactly 16 different types of triads possible. Suppose 1, 2, 3 are three + nodes, they will be classified as a particular triad type if their connections + are as follows: + + - 003: 1, 2, 3 + - 012: 1 -> 2, 3 + - 102: 1 <-> 2, 3 + - 021D: 1 <- 2 -> 3 + - 021U: 1 -> 2 <- 3 + - 021C: 1 -> 2 -> 3 + - 111D: 1 <-> 2 <- 3 + - 111U: 1 <-> 2 -> 3 + - 030T: 1 -> 2 -> 3, 1 -> 3 + - 030C: 1 <- 2 <- 3, 1 -> 3 + - 201: 1 <-> 2 <-> 3 + - 120D: 1 <- 2 -> 3, 1 <-> 3 + - 120U: 1 -> 2 <- 3, 1 <-> 3 + - 120C: 1 -> 2 -> 3, 1 <-> 3 + - 210: 1 -> 2 <-> 3, 1 <-> 3 + - 300: 1 <-> 2 <-> 3, 1 <-> 3 + + Refer to the :doc:`example gallery ` + for visual examples of the triad types. + + Parameters + ---------- + G : digraph + A NetworkX DiGraph + + Returns + ------- + tri_by_type : dict + Dictionary with triad types as keys and lists of triads as values. + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 1), (5, 6), (5, 4), (6, 7)]) + >>> dict = nx.triads_by_type(G) + >>> dict["120C"][0].edges() + OutEdgeView([(1, 2), (1, 3), (2, 3), (3, 1)]) + >>> dict["012"][0].edges() + OutEdgeView([(1, 2)]) + + References + ---------- + .. [1] Snijders, T. (2012). "Transitivity and triads." University of + Oxford. + https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf + """ + # num_triads = o * (o - 1) * (o - 2) // 6 + # if num_triads > TRIAD_LIMIT: print(WARNING) + all_tri = all_triads(G) + tri_by_type = defaultdict(list) + for triad in all_tri: + name = triad_type(triad) + tri_by_type[name].append(triad) + return tri_by_type + + +@not_implemented_for("undirected") +@nx._dispatchable +def triad_type(G): + """Returns the sociological triad type for a triad. + + Parameters + ---------- + G : digraph + A NetworkX DiGraph with 3 nodes + + Returns + ------- + triad_type : str + A string identifying the triad type + + Examples + -------- + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) + >>> nx.triad_type(G) + '030C' + >>> G.add_edge(1, 3) + >>> nx.triad_type(G) + '120C' + + Notes + ----- + There can be 6 unique edges in a triad (order-3 DiGraph) (so 2^^6=64 unique + triads given 3 nodes). These 64 triads each display exactly 1 of 16 + topologies of triads (topologies can be permuted). These topologies are + identified by the following notation: + + {m}{a}{n}{type} (for example: 111D, 210, 102) + + Here: + + {m} = number of mutual ties (takes 0, 1, 2, 3); a mutual tie is (0,1) + AND (1,0) + {a} = number of asymmetric ties (takes 0, 1, 2, 3); an asymmetric tie + is (0,1) BUT NOT (1,0) or vice versa + {n} = number of null ties (takes 0, 1, 2, 3); a null tie is NEITHER + (0,1) NOR (1,0) + {type} = a letter (takes U, D, C, T) corresponding to up, down, cyclical + and transitive. This is only used for topologies that can have + more than one form (eg: 021D and 021U). + + References + ---------- + .. [1] Snijders, T. (2012). "Transitivity and triads." University of + Oxford. + https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf + """ + if not is_triad(G): + raise nx.NetworkXAlgorithmError("G is not a triad (order-3 DiGraph)") + num_edges = len(G.edges()) + if num_edges == 0: + return "003" + elif num_edges == 1: + return "012" + elif num_edges == 2: + e1, e2 = G.edges() + if set(e1) == set(e2): + return "102" + elif e1[0] == e2[0]: + return "021D" + elif e1[1] == e2[1]: + return "021U" + elif e1[1] == e2[0] or e2[1] == e1[0]: + return "021C" + elif num_edges == 3: + for e1, e2, e3 in permutations(G.edges(), 3): + if set(e1) == set(e2): + if e3[0] in e1: + return "111U" + # e3[1] in e1: + return "111D" + elif set(e1).symmetric_difference(set(e2)) == set(e3): + if {e1[0], e2[0], e3[0]} == {e1[0], e2[0], e3[0]} == set(G.nodes()): + return "030C" + # e3 == (e1[0], e2[1]) and e2 == (e1[1], e3[1]): + return "030T" + elif num_edges == 4: + for e1, e2, e3, e4 in permutations(G.edges(), 4): + if set(e1) == set(e2): + # identify pair of symmetric edges (which necessarily exists) + if set(e3) == set(e4): + return "201" + if {e3[0]} == {e4[0]} == set(e3).intersection(set(e4)): + return "120D" + if {e3[1]} == {e4[1]} == set(e3).intersection(set(e4)): + return "120U" + if e3[1] == e4[0]: + return "120C" + elif num_edges == 5: + return "210" + elif num_edges == 6: + return "300" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/vitality.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/vitality.py new file mode 100644 index 0000000000000000000000000000000000000000..bf4b016e78dc7429810bb48f948f40212e542eca --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/vitality.py @@ -0,0 +1,76 @@ +""" +Vitality measures. +""" + +from functools import partial + +import networkx as nx + +__all__ = ["closeness_vitality"] + + +@nx._dispatchable(edge_attrs="weight") +def closeness_vitality(G, node=None, weight=None, wiener_index=None): + """Returns the closeness vitality for nodes in the graph. + + The *closeness vitality* of a node, defined in Section 3.6.2 of [1], + is the change in the sum of distances between all node pairs when + excluding that node. + + Parameters + ---------- + G : NetworkX graph + A strongly-connected graph. + + weight : string + The name of the edge attribute used as weight. This is passed + directly to the :func:`~networkx.wiener_index` function. + + node : object + If specified, only the closeness vitality for this node will be + returned. Otherwise, a dictionary mapping each node to its + closeness vitality will be returned. + + Other parameters + ---------------- + wiener_index : number + If you have already computed the Wiener index of the graph + `G`, you can provide that value here. Otherwise, it will be + computed for you. + + Returns + ------- + dictionary or float + If `node` is None, this function returns a dictionary + with nodes as keys and closeness vitality as the + value. Otherwise, it returns only the closeness vitality for the + specified `node`. + + The closeness vitality of a node may be negative infinity if + removing that node would disconnect the graph. + + Examples + -------- + >>> G = nx.cycle_graph(3) + >>> nx.closeness_vitality(G) + {0: 2.0, 1: 2.0, 2: 2.0} + + See Also + -------- + closeness_centrality + + References + ---------- + .. [1] Ulrik Brandes, Thomas Erlebach (eds.). + *Network Analysis: Methodological Foundations*. + Springer, 2005. + + + """ + if wiener_index is None: + wiener_index = nx.wiener_index(G, weight=weight) + if node is not None: + after = nx.wiener_index(G.subgraph(set(G) - {node}), weight=weight) + return wiener_index - after + vitality = partial(closeness_vitality, G, weight=weight, wiener_index=wiener_index) + return {v: vitality(node=v) for v in G} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/voronoi.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/voronoi.py new file mode 100644 index 0000000000000000000000000000000000000000..609a68deff89620e0e022020c33863107decced4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/voronoi.py @@ -0,0 +1,86 @@ +"""Functions for computing the Voronoi cells of a graph.""" + +import networkx as nx +from networkx.utils import groups + +__all__ = ["voronoi_cells"] + + +@nx._dispatchable(edge_attrs="weight") +def voronoi_cells(G, center_nodes, weight="weight"): + """Returns the Voronoi cells centered at `center_nodes` with respect + to the shortest-path distance metric. + + If $C$ is a set of nodes in the graph and $c$ is an element of $C$, + the *Voronoi cell* centered at a node $c$ is the set of all nodes + $v$ that are closer to $c$ than to any other center node in $C$ with + respect to the shortest-path distance metric. [1]_ + + For directed graphs, this will compute the "outward" Voronoi cells, + as defined in [1]_, in which distance is measured from the center + nodes to the target node. For the "inward" Voronoi cells, use the + :meth:`DiGraph.reverse` method to reverse the orientation of the + edges before invoking this function on the directed graph. + + Parameters + ---------- + G : NetworkX graph + + center_nodes : set + A nonempty set of nodes in the graph `G` that represent the + center of the Voronoi cells. + + weight : string or function + The edge attribute (or an arbitrary function) representing the + weight of an edge. This keyword argument is as described in the + documentation for :func:`~networkx.multi_source_dijkstra_path`, + for example. + + Returns + ------- + dictionary + A mapping from center node to set of all nodes in the graph + closer to that center node than to any other center node. The + keys of the dictionary are the element of `center_nodes`, and + the values of the dictionary form a partition of the nodes of + `G`. + + Examples + -------- + To get only the partition of the graph induced by the Voronoi cells, + take the collection of all values in the returned dictionary:: + + >>> G = nx.path_graph(6) + >>> center_nodes = {0, 3} + >>> cells = nx.voronoi_cells(G, center_nodes) + >>> partition = set(map(frozenset, cells.values())) + >>> sorted(map(sorted, partition)) + [[0, 1], [2, 3, 4, 5]] + + Raises + ------ + ValueError + If `center_nodes` is empty. + + References + ---------- + .. [1] Erwig, Martin. (2000),"The graph Voronoi diagram with applications." + *Networks*, 36: 156--163. + https://doi.org/10.1002/1097-0037(200010)36:3<156::AID-NET2>3.0.CO;2-L + + """ + # Determine the shortest paths from any one of the center nodes to + # every node in the graph. + # + # This raises `ValueError` if `center_nodes` is an empty set. + paths = nx.multi_source_dijkstra_path(G, center_nodes, weight=weight) + # Determine the center node from which the shortest path originates. + nearest = {v: p[0] for v, p in paths.items()} + # Get the mapping from center node to all nodes closer to it than to + # any other center node. + cells = groups(nearest) + # We collect all unreachable nodes under a special key, if there are any. + unreachable = set(G) - set(nearest) + if unreachable: + cells["unreachable"] = unreachable + return cells diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/walks.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/walks.py new file mode 100644 index 0000000000000000000000000000000000000000..97e5bb0b5b635bf5cebd5a0e1191374a6d7bd6c3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/walks.py @@ -0,0 +1,77 @@ +"""Function for computing walks in a graph.""" + +import networkx as nx + +__all__ = ["number_of_walks"] + + +@nx._dispatchable +def number_of_walks(G, walk_length): + """Returns the number of walks connecting each pair of nodes in `G` + + A *walk* is a sequence of nodes in which each adjacent pair of nodes + in the sequence is adjacent in the graph. A walk can repeat the same + edge and go in the opposite direction just as people can walk on a + set of paths, but standing still is not counted as part of the walk. + + This function only counts the walks with `walk_length` edges. Note that + the number of nodes in the walk sequence is one more than `walk_length`. + The number of walks can grow very quickly on a larger graph + and with a larger walk length. + + Parameters + ---------- + G : NetworkX graph + + walk_length : int + A nonnegative integer representing the length of a walk. + + Returns + ------- + dict + A dictionary of dictionaries in which outer keys are source + nodes, inner keys are target nodes, and inner values are the + number of walks of length `walk_length` connecting those nodes. + + Raises + ------ + ValueError + If `walk_length` is negative + + Examples + -------- + + >>> G = nx.Graph([(0, 1), (1, 2)]) + >>> walks = nx.number_of_walks(G, 2) + >>> walks + {0: {0: 1, 1: 0, 2: 1}, 1: {0: 0, 1: 2, 2: 0}, 2: {0: 1, 1: 0, 2: 1}} + >>> total_walks = sum(sum(tgts.values()) for _, tgts in walks.items()) + + You can also get the number of walks from a specific source node using the + returned dictionary. For example, number of walks of length 1 from node 0 + can be found as follows: + + >>> walks = nx.number_of_walks(G, 1) + >>> walks[0] + {0: 0, 1: 1, 2: 0} + >>> sum(walks[0].values()) # walks from 0 of length 1 + 1 + + Similarly, a target node can also be specified: + + >>> walks[0][1] + 1 + + """ + import scipy as sp + + if walk_length < 0: + raise ValueError(f"`walk_length` cannot be negative: {walk_length}") + + A = nx.adjacency_matrix(G, weight=None) + power = sp.sparse.linalg.matrix_power(A, walk_length).tocsr() + result = { + u: {v: power[u_idx, v_idx].item() for v_idx, v in enumerate(G)} + for u_idx, u in enumerate(G) + } + return result diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/wiener.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/wiener.py new file mode 100644 index 0000000000000000000000000000000000000000..d097aa629088e305bdb3e221dcb6da09d69bb3dd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/algorithms/wiener.py @@ -0,0 +1,278 @@ +"""Functions related to the Wiener Index of a graph. + +The Wiener Index is a topological measure of a graph +related to the distance between nodes and their degree. +The Schultz Index and Gutman Index are similar measures. +They are used categorize molecules via the network of +atoms connected by chemical bonds. The indices are +correlated with functional aspects of the molecules. + +References +---------- +.. [1] `Wikipedia: Wiener Index `_ +.. [2] M.V. Diudeaa and I. Gutman, Wiener-Type Topological Indices, + Croatica Chemica Acta, 71 (1998), 21-51. + https://hrcak.srce.hr/132323 +""" + +import itertools as it + +import networkx as nx + +__all__ = ["wiener_index", "schultz_index", "gutman_index", "hyper_wiener_index"] + + +@nx._dispatchable(edge_attrs="weight") +def wiener_index(G, weight=None): + """Returns the Wiener index of the given graph. + + The *Wiener index* of a graph is the sum of the shortest-path + (weighted) distances between each pair of reachable nodes. + For pairs of nodes in undirected graphs, only one orientation + of the pair is counted. + + Parameters + ---------- + G : NetworkX graph + + weight : string or None, optional (default: None) + If None, every edge has weight 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + The edge weights are used to computing shortest-path distances. + + Returns + ------- + number + The Wiener index of the graph `G`. + + Raises + ------ + NetworkXError + If the graph `G` is not connected. + + Notes + ----- + If a pair of nodes is not reachable, the distance is assumed to be + infinity. This means that for graphs that are not + strongly-connected, this function returns ``inf``. + + The Wiener index is not usually defined for directed graphs, however + this function uses the natural generalization of the Wiener index to + directed graphs. + + Examples + -------- + The Wiener index of the (unweighted) complete graph on *n* nodes + equals the number of pairs of the *n* nodes, since each pair of + nodes is at distance one:: + + >>> n = 10 + >>> G = nx.complete_graph(n) + >>> nx.wiener_index(G) == n * (n - 1) / 2 + True + + Graphs that are not strongly-connected have infinite Wiener index:: + + >>> G = nx.empty_graph(2) + >>> nx.wiener_index(G) + inf + + References + ---------- + .. [1] `Wikipedia: Wiener Index `_ + """ + connected = nx.is_strongly_connected(G) if G.is_directed() else nx.is_connected(G) + if not connected: + return float("inf") + + spl = nx.shortest_path_length(G, weight=weight) + total = sum(it.chain.from_iterable(nbrs.values() for node, nbrs in spl)) + # Need to account for double counting pairs of nodes in undirected graphs. + return total if G.is_directed() else total / 2 + + +@nx.utils.not_implemented_for("directed") +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def schultz_index(G, weight=None): + r"""Returns the Schultz Index (of the first kind) of `G` + + The *Schultz Index* [3]_ of a graph is the sum over all node pairs of + distances times the sum of degrees. Consider an undirected graph `G`. + For each node pair ``(u, v)`` compute ``dist(u, v) * (deg(u) + deg(v)`` + where ``dist`` is the shortest path length between two nodes and ``deg`` + is the degree of a node. + + The Schultz Index is the sum of these quantities over all (unordered) + pairs of nodes. + + Parameters + ---------- + G : NetworkX graph + The undirected graph of interest. + weight : string or None, optional (default: None) + If None, every edge has weight 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + The edge weights are used to computing shortest-path distances. + + Returns + ------- + number + The first kind of Schultz Index of the graph `G`. + + Examples + -------- + The Schultz Index of the (unweighted) complete graph on *n* nodes + equals the number of pairs of the *n* nodes times ``2 * (n - 1)``, + since each pair of nodes is at distance one and the sum of degree + of two nodes is ``2 * (n - 1)``. + + >>> n = 10 + >>> G = nx.complete_graph(n) + >>> nx.schultz_index(G) == (n * (n - 1) / 2) * (2 * (n - 1)) + True + + Graph that is disconnected + + >>> nx.schultz_index(nx.empty_graph(2)) + inf + + References + ---------- + .. [1] I. Gutman, Selected properties of the Schultz molecular topological index, + J. Chem. Inf. Comput. Sci. 34 (1994), 1087–1089. + https://doi.org/10.1021/ci00021a009 + .. [2] M.V. Diudeaa and I. Gutman, Wiener-Type Topological Indices, + Croatica Chemica Acta, 71 (1998), 21-51. + https://hrcak.srce.hr/132323 + .. [3] H. P. Schultz, Topological organic chemistry. 1. + Graph theory and topological indices of alkanes,i + J. Chem. Inf. Comput. Sci. 29 (1989), 239–257. + + """ + if not nx.is_connected(G): + return float("inf") + + spl = nx.shortest_path_length(G, weight=weight) + d = dict(G.degree, weight=weight) + return sum(dist * (d[u] + d[v]) for u, info in spl for v, dist in info.items()) / 2 + + +@nx.utils.not_implemented_for("directed") +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def gutman_index(G, weight=None): + r"""Returns the Gutman Index for the graph `G`. + + The *Gutman Index* measures the topology of networks, especially for molecule + networks of atoms connected by bonds [1]_. It is also called the Schultz Index + of the second kind [2]_. + + Consider an undirected graph `G` with node set ``V``. + The Gutman Index of a graph is the sum over all (unordered) pairs of nodes + of nodes ``(u, v)``, with distance ``dist(u, v)`` and degrees ``deg(u)`` + and ``deg(v)``, of ``dist(u, v) * deg(u) * deg(v)`` + + Parameters + ---------- + G : NetworkX graph + + weight : string or None, optional (default: None) + If None, every edge has weight 1. + If a string, use this edge attribute as the edge weight. + Any edge attribute not present defaults to 1. + The edge weights are used to computing shortest-path distances. + + Returns + ------- + number + The Gutman Index of the graph `G`. + + Examples + -------- + The Gutman Index of the (unweighted) complete graph on *n* nodes + equals the number of pairs of the *n* nodes times ``(n - 1) * (n - 1)``, + since each pair of nodes is at distance one and the product of degree of two + vertices is ``(n - 1) * (n - 1)``. + + >>> n = 10 + >>> G = nx.complete_graph(n) + >>> nx.gutman_index(G) == (n * (n - 1) / 2) * ((n - 1) * (n - 1)) + True + + Graphs that are disconnected + + >>> G = nx.empty_graph(2) + >>> nx.gutman_index(G) + inf + + References + ---------- + .. [1] M.V. Diudeaa and I. Gutman, Wiener-Type Topological Indices, + Croatica Chemica Acta, 71 (1998), 21-51. + https://hrcak.srce.hr/132323 + .. [2] I. Gutman, Selected properties of the Schultz molecular topological index, + J. Chem. Inf. Comput. Sci. 34 (1994), 1087–1089. + https://doi.org/10.1021/ci00021a009 + + """ + if not nx.is_connected(G): + return float("inf") + + spl = nx.shortest_path_length(G, weight=weight) + d = dict(G.degree, weight=weight) + return sum(dist * d[u] * d[v] for u, vinfo in spl for v, dist in vinfo.items()) / 2 + + +@nx.utils.not_implemented_for("directed") +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def hyper_wiener_index(G, weight=None): + r"""Returns the Hyper-Wiener index of the graph `G`. + + The Hyper-Wiener index of a connected graph `G` is defined as + + .. math:: + WW(G) = \frac{1}{2} \sum_{u,v \in V(G)} (d(u,v) + d(u,v)^2) + + where ``d(u, v)`` is the shortest-path distance between nodes ``u`` and ``v``. + + Parameters + ---------- + G : NetworkX graph + An undirected, connected graph. + + weight : string or None, optional (default: None) + The edge attribute to use for calculating shortest-path distances. + If None, all edges are considered to have a weight of 1. + + Returns + ------- + float + The Hyper-Wiener index of the graph G. + Returns float("inf") if the graph is not connected. + + References + ---------- + .. [1] M. Randić, "Novel molecular descriptor for structure-property studies," + Chemical Physics Letters, vol. 211, pp. 478-483, 1993. + .. [2] `Wikipedia: Hyper-Wiener Index `_ + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.hyper_wiener_index(G) + 30.0 + + >>> G = nx.cycle_graph(4) + >>> nx.hyper_wiener_index(G) + 20.0 + """ + if not nx.is_connected(G): + return float("inf") + + spl = nx.shortest_path_length(G, weight=weight) + total = sum(dist + dist**2 for _, lengths in spl for dist in lengths.values()) + return total / 2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..721fa8b4767233bc2b624f6b2ce4d10533a4d66c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__init__.py @@ -0,0 +1,13 @@ +from .graph import Graph +from .digraph import DiGraph +from .multigraph import MultiGraph +from .multidigraph import MultiDiGraph + +from .function import * +from .graphviews import subgraph_view, reverse_view + +from networkx.classes import filters + +from networkx.classes import coreviews +from networkx.classes import graphviews +from networkx.classes import reportviews diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..242cd24003055fea2a757f64d10944c1f27c2e50 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/coreviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/coreviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b888e3574d91eef9c21a15210782a9c04c2901de Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/coreviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/digraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/digraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ea5398db7d19f7d6f7ae8c14a764168157e9a44 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/digraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/filters.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..176ba5b881e96f8eec27e544773a34f948cff489 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/filters.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/function.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/function.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba008525b867f4cc9ede60853a3ac54a7f621d1c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/function.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3aff065523b2c10894dcf3e55a6b0db23c6ecd73 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graphviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graphviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ddacf2658124efa4fac5e5a20aaa031c95e5e99 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/graphviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multidigraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multidigraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b753eb20ad06ac309c2378844bb2784f3955f88f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multidigraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multigraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multigraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cc1a7179870897c4ce3685fea06b4b5b65f2eb3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/multigraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/reportviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/reportviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26f919b0dddedb29b69f24c94fdd25482d42b837 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/__pycache__/reportviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/coreviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/coreviews.py new file mode 100644 index 0000000000000000000000000000000000000000..4769ffa71ab823c154e6f7b990f0cb07299090a6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/coreviews.py @@ -0,0 +1,435 @@ +"""Views of core data structures such as nested Mappings (e.g. dict-of-dicts). +These ``Views`` often restrict element access, with either the entire view or +layers of nested mappings being read-only. +""" + +from collections.abc import Mapping + +__all__ = [ + "AtlasView", + "AdjacencyView", + "MultiAdjacencyView", + "UnionAtlas", + "UnionAdjacency", + "UnionMultiInner", + "UnionMultiAdjacency", + "FilterAtlas", + "FilterAdjacency", + "FilterMultiInner", + "FilterMultiAdjacency", +] + + +class AtlasView(Mapping): + """An AtlasView is a Read-only Mapping of Mappings. + + It is a View into a dict-of-dict data structure. + The inner level of dict is read-write. But the + outer level is read-only. + + See Also + ======== + AdjacencyView: View into dict-of-dict-of-dict + MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict + """ + + __slots__ = ("_atlas",) + + def __getstate__(self): + return {"_atlas": self._atlas} + + def __setstate__(self, state): + self._atlas = state["_atlas"] + + def __init__(self, d): + self._atlas = d + + def __len__(self): + return len(self._atlas) + + def __iter__(self): + return iter(self._atlas) + + def __getitem__(self, key): + return self._atlas[key] + + def copy(self): + return {n: self[n].copy() for n in self._atlas} + + def __str__(self): + return str(self._atlas) # {nbr: self[nbr] for nbr in self}) + + def __repr__(self): + return f"{self.__class__.__name__}({self._atlas!r})" + + +class AdjacencyView(AtlasView): + """An AdjacencyView is a Read-only Map of Maps of Maps. + + It is a View into a dict-of-dict-of-dict data structure. + The inner level of dict is read-write. But the + outer levels are read-only. + + See Also + ======== + AtlasView: View into dict-of-dict + MultiAdjacencyView: View into dict-of-dict-of-dict-of-dict + """ + + __slots__ = () # Still uses AtlasView slots names _atlas + + def __getitem__(self, name): + return AtlasView(self._atlas[name]) + + def copy(self): + return {n: self[n].copy() for n in self._atlas} + + +class MultiAdjacencyView(AdjacencyView): + """An MultiAdjacencyView is a Read-only Map of Maps of Maps of Maps. + + It is a View into a dict-of-dict-of-dict-of-dict data structure. + The inner level of dict is read-write. But the + outer levels are read-only. + + See Also + ======== + AtlasView: View into dict-of-dict + AdjacencyView: View into dict-of-dict-of-dict + """ + + __slots__ = () # Still uses AtlasView slots names _atlas + + def __getitem__(self, name): + return AdjacencyView(self._atlas[name]) + + def copy(self): + return {n: self[n].copy() for n in self._atlas} + + +class UnionAtlas(Mapping): + """A read-only union of two atlases (dict-of-dict). + + The two dict-of-dicts represent the inner dict of + an Adjacency: `G.succ[node]` and `G.pred[node]`. + The inner level of dict of both hold attribute key:value + pairs and is read-write. But the outer level is read-only. + + See Also + ======== + UnionAdjacency: View into dict-of-dict-of-dict + UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict + """ + + __slots__ = ("_succ", "_pred") + + def __getstate__(self): + return {"_succ": self._succ, "_pred": self._pred} + + def __setstate__(self, state): + self._succ = state["_succ"] + self._pred = state["_pred"] + + def __init__(self, succ, pred): + self._succ = succ + self._pred = pred + + def __len__(self): + return len(self._succ.keys() | self._pred.keys()) + + def __iter__(self): + return iter(set(self._succ.keys()) | set(self._pred.keys())) + + def __getitem__(self, key): + try: + return self._succ[key] + except KeyError: + return self._pred[key] + + def copy(self): + result = {nbr: dd.copy() for nbr, dd in self._succ.items()} + for nbr, dd in self._pred.items(): + if nbr in result: + result[nbr].update(dd) + else: + result[nbr] = dd.copy() + return result + + def __str__(self): + return str({nbr: self[nbr] for nbr in self}) + + def __repr__(self): + return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})" + + +class UnionAdjacency(Mapping): + """A read-only union of dict Adjacencies as a Map of Maps of Maps. + + The two input dict-of-dict-of-dicts represent the union of + `G.succ` and `G.pred`. Return values are UnionAtlas + The inner level of dict is read-write. But the + middle and outer levels are read-only. + + succ : a dict-of-dict-of-dict {node: nbrdict} + pred : a dict-of-dict-of-dict {node: nbrdict} + The keys for the two dicts should be the same + + See Also + ======== + UnionAtlas: View into dict-of-dict + UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict + """ + + __slots__ = ("_succ", "_pred") + + def __getstate__(self): + return {"_succ": self._succ, "_pred": self._pred} + + def __setstate__(self, state): + self._succ = state["_succ"] + self._pred = state["_pred"] + + def __init__(self, succ, pred): + # keys must be the same for two input dicts + assert len(set(succ.keys()) ^ set(pred.keys())) == 0 + self._succ = succ + self._pred = pred + + def __len__(self): + return len(self._succ) # length of each dict should be the same + + def __iter__(self): + return iter(self._succ) + + def __getitem__(self, nbr): + return UnionAtlas(self._succ[nbr], self._pred[nbr]) + + def copy(self): + return {n: self[n].copy() for n in self._succ} + + def __str__(self): + return str({nbr: self[nbr] for nbr in self}) + + def __repr__(self): + return f"{self.__class__.__name__}({self._succ!r}, {self._pred!r})" + + +class UnionMultiInner(UnionAtlas): + """A read-only union of two inner dicts of MultiAdjacencies. + + The two input dict-of-dict-of-dicts represent the union of + `G.succ[node]` and `G.pred[node]` for MultiDiGraphs. + Return values are UnionAtlas. + The inner level of dict is read-write. But the outer levels are read-only. + + See Also + ======== + UnionAtlas: View into dict-of-dict + UnionAdjacency: View into dict-of-dict-of-dict + UnionMultiAdjacency: View into dict-of-dict-of-dict-of-dict + """ + + __slots__ = () # Still uses UnionAtlas slots names _succ, _pred + + def __getitem__(self, node): + in_succ = node in self._succ + in_pred = node in self._pred + if in_succ: + if in_pred: + return UnionAtlas(self._succ[node], self._pred[node]) + return UnionAtlas(self._succ[node], {}) + return UnionAtlas({}, self._pred[node]) + + def copy(self): + nodes = set(self._succ.keys()) | set(self._pred.keys()) + return {n: self[n].copy() for n in nodes} + + +class UnionMultiAdjacency(UnionAdjacency): + """A read-only union of two dict MultiAdjacencies. + + The two input dict-of-dict-of-dict-of-dicts represent the union of + `G.succ` and `G.pred` for MultiDiGraphs. Return values are UnionAdjacency. + The inner level of dict is read-write. But the outer levels are read-only. + + See Also + ======== + UnionAtlas: View into dict-of-dict + UnionMultiInner: View into dict-of-dict-of-dict + """ + + __slots__ = () # Still uses UnionAdjacency slots names _succ, _pred + + def __getitem__(self, node): + return UnionMultiInner(self._succ[node], self._pred[node]) + + +class FilterAtlas(Mapping): # nodedict, nbrdict, keydict + """A read-only Mapping of Mappings with filtering criteria for nodes. + + It is a view into a dict-of-dict data structure, and it selects only + nodes that meet the criteria defined by ``NODE_OK``. + + See Also + ======== + FilterAdjacency + FilterMultiInner + FilterMultiAdjacency + """ + + def __init__(self, d, NODE_OK): + self._atlas = d + self.NODE_OK = NODE_OK + + def __len__(self): + # check whether NODE_OK stores the number of nodes as `length` + # or the nodes themselves as a set `nodes`. If not, count the nodes. + if hasattr(self.NODE_OK, "length"): + return self.NODE_OK.length + if hasattr(self.NODE_OK, "nodes"): + return len(self.NODE_OK.nodes & self._atlas.keys()) + return sum(1 for n in self._atlas if self.NODE_OK(n)) + + def __iter__(self): + try: # check that NODE_OK has attr 'nodes' + node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas) + except AttributeError: + node_ok_shorter = False + if node_ok_shorter: + return (n for n in self.NODE_OK.nodes if n in self._atlas) + return (n for n in self._atlas if self.NODE_OK(n)) + + def __getitem__(self, key): + if key in self._atlas and self.NODE_OK(key): + return self._atlas[key] + raise KeyError(f"Key {key} not found") + + def __str__(self): + return str({nbr: self[nbr] for nbr in self}) + + def __repr__(self): + return f"{self.__class__.__name__}({self._atlas!r}, {self.NODE_OK!r})" + + +class FilterAdjacency(Mapping): # edgedict + """A read-only Mapping of Mappings with filtering criteria for nodes and edges. + + It is a view into a dict-of-dict-of-dict data structure, and it selects nodes + and edges that satisfy specific criteria defined by ``NODE_OK`` and ``EDGE_OK``, + respectively. + + See Also + ======== + FilterAtlas + FilterMultiInner + FilterMultiAdjacency + """ + + def __init__(self, d, NODE_OK, EDGE_OK): + self._atlas = d + self.NODE_OK = NODE_OK + self.EDGE_OK = EDGE_OK + + def __len__(self): + # check whether NODE_OK stores the number of nodes as `length` + # or the nodes themselves as a set `nodes`. If not, count the nodes. + if hasattr(self.NODE_OK, "length"): + return self.NODE_OK.length + if hasattr(self.NODE_OK, "nodes"): + return len(self.NODE_OK.nodes & self._atlas.keys()) + return sum(1 for n in self._atlas if self.NODE_OK(n)) + + def __iter__(self): + try: # check that NODE_OK has attr 'nodes' + node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas) + except AttributeError: + node_ok_shorter = False + if node_ok_shorter: + return (n for n in self.NODE_OK.nodes if n in self._atlas) + return (n for n in self._atlas if self.NODE_OK(n)) + + def __getitem__(self, node): + if node in self._atlas and self.NODE_OK(node): + + def new_node_ok(nbr): + return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr) + + return FilterAtlas(self._atlas[node], new_node_ok) + raise KeyError(f"Key {node} not found") + + def __str__(self): + return str({nbr: self[nbr] for nbr in self}) + + def __repr__(self): + name = self.__class__.__name__ + return f"{name}({self._atlas!r}, {self.NODE_OK!r}, {self.EDGE_OK!r})" + + +class FilterMultiInner(FilterAdjacency): # muliedge_seconddict + """A read-only Mapping of Mappings with filtering criteria for nodes and edges. + + It is a view into a dict-of-dict-of-dict-of-dict data structure, and it selects nodes + and edges that meet specific criteria defined by ``NODE_OK`` and ``EDGE_OK``. + + See Also + ======== + FilterAtlas + FilterAdjacency + FilterMultiAdjacency + """ + + def __iter__(self): + try: # check that NODE_OK has attr 'nodes' + node_ok_shorter = 2 * len(self.NODE_OK.nodes) < len(self._atlas) + except AttributeError: + node_ok_shorter = False + if node_ok_shorter: + my_nodes = (n for n in self.NODE_OK.nodes if n in self._atlas) + else: + my_nodes = (n for n in self._atlas if self.NODE_OK(n)) + for n in my_nodes: + some_keys_ok = False + for key in self._atlas[n]: + if self.EDGE_OK(n, key): + some_keys_ok = True + break + if some_keys_ok is True: + yield n + + def __getitem__(self, nbr): + if ( + nbr in self._atlas + and self.NODE_OK(nbr) + and any(self.EDGE_OK(nbr, key) for key in self._atlas[nbr]) + ): + + def new_node_ok(key): + return self.EDGE_OK(nbr, key) + + return FilterAtlas(self._atlas[nbr], new_node_ok) + raise KeyError(f"Key {nbr} not found") + + +class FilterMultiAdjacency(FilterAdjacency): # multiedgedict + """A read-only Mapping of Mappings with filtering criteria + for nodes and edges. + + It is a view into a dict-of-dict-of-dict-of-dict data structure, + and it selects nodes and edges that satisfy specific criteria + defined by ``NODE_OK`` and ``EDGE_OK``, respectively. + + See Also + ======== + FilterAtlas + FilterAdjacency + FilterMultiInner + """ + + def __getitem__(self, node): + if node in self._atlas and self.NODE_OK(node): + + def edge_ok(nbr, key): + return self.NODE_OK(nbr) and self.EDGE_OK(node, nbr, key) + + return FilterMultiInner(self._atlas[node], self.NODE_OK, edge_ok) + raise KeyError(f"Key {node} not found") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/digraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/digraph.py new file mode 100644 index 0000000000000000000000000000000000000000..ae35128e26814315277ac4f0500abc53f36bbbb1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/digraph.py @@ -0,0 +1,1363 @@ +"""Base class for directed graphs.""" + +from copy import deepcopy +from functools import cached_property + +import networkx as nx +from networkx import convert +from networkx.classes.coreviews import AdjacencyView +from networkx.classes.graph import Graph +from networkx.classes.reportviews import ( + DiDegreeView, + InDegreeView, + InEdgeView, + OutDegreeView, + OutEdgeView, +) +from networkx.exception import NetworkXError + +__all__ = ["DiGraph"] + + +class _CachedPropertyResetterAdjAndSucc: + """Data Descriptor class that syncs and resets cached properties adj and succ + + The cached properties `adj` and `succ` are reset whenever `_adj` or `_succ` + are set to new objects. In addition, the attributes `_succ` and `_adj` + are synced so these two names point to the same object. + + Warning: most of the time, when ``G._adj`` is set, ``G._pred`` should also + be set to maintain a valid data structure. They share datadicts. + + This object sits on a class and ensures that any instance of that + class clears its cached properties "succ" and "adj" whenever the + underlying instance attributes "_succ" or "_adj" are set to a new object. + It only affects the set process of the obj._adj and obj._succ attribute. + All get/del operations act as they normally would. + + For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html + """ + + def __set__(self, obj, value): + od = obj.__dict__ + od["_adj"] = value + od["_succ"] = value + # reset cached properties + props = [ + "adj", + "succ", + "edges", + "out_edges", + "degree", + "out_degree", + "in_degree", + ] + for prop in props: + if prop in od: + del od[prop] + + +class _CachedPropertyResetterPred: + """Data Descriptor class for _pred that resets ``pred`` cached_property when needed + + This assumes that the ``cached_property`` ``G.pred`` should be reset whenever + ``G._pred`` is set to a new value. + + Warning: most of the time, when ``G._pred`` is set, ``G._adj`` should also + be set to maintain a valid data structure. They share datadicts. + + This object sits on a class and ensures that any instance of that + class clears its cached property "pred" whenever the underlying + instance attribute "_pred" is set to a new object. It only affects + the set process of the obj._pred attribute. All get/del operations + act as they normally would. + + For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html + """ + + def __set__(self, obj, value): + od = obj.__dict__ + od["_pred"] = value + # reset cached properties + props = ["pred", "in_edges", "degree", "out_degree", "in_degree"] + for prop in props: + if prop in od: + del od[prop] + + +class DiGraph(Graph): + """ + Base class for directed graphs. + + A DiGraph stores nodes and edges with optional data, or attributes. + + DiGraphs hold directed edges. Self loops are allowed but multiple + (parallel) edges are not. + + Nodes can be arbitrary (hashable) Python objects with optional + key/value attributes. By convention `None` is not used as a node. + + Edges are represented as links between nodes with optional + key/value attributes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be any format that is supported + by the to_networkx_graph() function, currently including edge list, + dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy + sparse matrix, or PyGraphviz graph. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + Graph + MultiGraph + MultiDiGraph + + Examples + -------- + Create an empty graph structure (a "null graph") with no nodes and + no edges. + + >>> G = nx.DiGraph() + + G can be grown in several ways. + + **Nodes:** + + Add one node at a time: + + >>> G.add_node(1) + + Add the nodes from any container (a list, dict, set or + even the lines from a file or the nodes from another graph). + + >>> G.add_nodes_from([2, 3]) + >>> G.add_nodes_from(range(100, 110)) + >>> H = nx.path_graph(10) + >>> G.add_nodes_from(H) + + In addition to strings and integers any hashable Python object + (except None) can represent a node, e.g. a customized node object, + or even another Graph. + + >>> G.add_node(H) + + **Edges:** + + G can also be grown by adding edges. + + Add one edge, + + >>> G.add_edge(1, 2) + + a list of edges, + + >>> G.add_edges_from([(1, 2), (1, 3)]) + + or a collection of edges, + + >>> G.add_edges_from(H.edges) + + If some edges connect nodes not yet in the graph, the nodes + are added automatically. There are no errors when adding + nodes or edges that already exist. + + **Attributes:** + + Each graph, node, and edge can hold key/value attribute pairs + in an associated attribute dictionary (the keys must be hashable). + By default these are empty, but can be added or changed using + add_edge, add_node or direct manipulation of the attribute + dictionaries named graph, node and edge respectively. + + >>> G = nx.DiGraph(day="Friday") + >>> G.graph + {'day': 'Friday'} + + Add node attributes using add_node(), add_nodes_from() or G.nodes + + >>> G.add_node(1, time="5pm") + >>> G.add_nodes_from([3], time="2pm") + >>> G.nodes[1] + {'time': '5pm'} + >>> G.nodes[1]["room"] = 714 + >>> del G.nodes[1]["room"] # remove attribute + >>> list(G.nodes(data=True)) + [(1, {'time': '5pm'}), (3, {'time': '2pm'})] + + Add edge attributes using add_edge(), add_edges_from(), subscript + notation, or G.edges. + + >>> G.add_edge(1, 2, weight=4.7) + >>> G.add_edges_from([(3, 4), (4, 5)], color="red") + >>> G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})]) + >>> G[1][2]["weight"] = 4.7 + >>> G.edges[1, 2]["weight"] = 4 + + Warning: we protect the graph data structure by making `G.edges[1, 2]` a + read-only dict-like structure. However, you can assign to attributes + in e.g. `G.edges[1, 2]`. Thus, use 2 sets of brackets to add/change + data attributes: `G.edges[1, 2]['weight'] = 4` + (For multigraphs: `MG.edges[u, v, key][name] = value`). + + **Shortcuts:** + + Many common graph features allow python syntax to speed reporting. + + >>> 1 in G # check if node in graph + True + >>> [n for n in G if n < 3] # iterate through nodes + [1, 2] + >>> len(G) # number of nodes in graph + 5 + + Often the best way to traverse all edges of a graph is via the neighbors. + The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()` + + >>> for n, nbrsdict in G.adjacency(): + ... for nbr, eattr in nbrsdict.items(): + ... if "weight" in eattr: + ... # Do something useful with the edges + ... pass + + But the edges reporting object is often more convenient: + + >>> for u, v, weight in G.edges(data="weight"): + ... if weight is not None: + ... # Do something useful with the edges + ... pass + + **Reporting:** + + Simple graph information is obtained using object-attributes and methods. + Reporting usually provides views instead of containers to reduce memory + usage. The views update as the graph is updated similarly to dict-views. + The objects `nodes`, `edges` and `adj` provide access to data attributes + via lookup (e.g. `nodes[n]`, `edges[u, v]`, `adj[u][v]`) and iteration + (e.g. `nodes.items()`, `nodes.data('color')`, + `nodes.data('color', default='blue')` and similarly for `edges`) + Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`. + + For details on these and other miscellaneous methods, see below. + + **Subclasses (Advanced):** + + The Graph class uses a dict-of-dict-of-dict data structure. + The outer dict (node_dict) holds adjacency information keyed by node. + The next dict (adjlist_dict) represents the adjacency information and holds + edge data keyed by neighbor. The inner dict (edge_attr_dict) represents + the edge data and holds edge attribute values keyed by attribute names. + + Each of these three dicts can be replaced in a subclass by a user defined + dict-like object. In general, the dict-like features should be + maintained but extra features can be added. To replace one of the + dicts create a new graph class by changing the class(!) variable + holding the factory for that dict-like structure. The variable names are + node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory, + adjlist_outer_dict_factory, edge_attr_dict_factory and graph_attr_dict_factory. + + node_dict_factory : function, (default: dict) + Factory function to be used to create the dict containing node + attributes, keyed by node id. + It should require no arguments and return a dict-like object + + node_attr_dict_factory: function, (default: dict) + Factory function to be used to create the node attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object + + adjlist_outer_dict_factory : function, (default: dict) + Factory function to be used to create the outer-most dict + in the data structure that holds adjacency info keyed by node. + It should require no arguments and return a dict-like object. + + adjlist_inner_dict_factory : function, optional (default: dict) + Factory function to be used to create the adjacency list + dict which holds edge data keyed by neighbor. + It should require no arguments and return a dict-like object + + edge_attr_dict_factory : function, optional (default: dict) + Factory function to be used to create the edge attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + graph_attr_dict_factory : function, (default: dict) + Factory function to be used to create the graph attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + Typically, if your extension doesn't impact the data structure all + methods will inherited without issue except: `to_directed/to_undirected`. + By default these methods create a DiGraph/Graph class and you probably + want them to create your extension of a DiGraph/Graph. To facilitate + this we define two class variables that you can set in your subclass. + + to_directed_class : callable, (default: DiGraph or MultiDiGraph) + Class to create a new graph structure in the `to_directed` method. + If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used. + + to_undirected_class : callable, (default: Graph or MultiGraph) + Class to create a new graph structure in the `to_undirected` method. + If `None`, a NetworkX class (Graph or MultiGraph) is used. + + **Subclassing Example** + + Create a low memory graph class that effectively disallows edge + attributes by using a single attribute dict for all edges. + This reduces the memory used, but you lose edge attributes. + + >>> class ThinGraph(nx.Graph): + ... all_edge_dict = {"weight": 1} + ... + ... def single_edge_dict(self): + ... return self.all_edge_dict + ... + ... edge_attr_dict_factory = single_edge_dict + >>> G = ThinGraph() + >>> G.add_edge(2, 1) + >>> G[2][1] + {'weight': 1} + >>> G.add_edge(2, 2) + >>> G[2][1] is G[2][2] + True + """ + + _adj = _CachedPropertyResetterAdjAndSucc() # type: ignore[assignment] + _succ = _adj # type: ignore[has-type] + _pred = _CachedPropertyResetterPred() + + # This __new__ method just does what Python itself does automatically. + # We include it here as part of the dispatchable/backend interface. + # If your goal is to understand how the graph classes work, you can ignore + # this method, even when subclassing the base classes. If you are subclassing + # in order to provide a backend that allows class instantiation, this method + # can be overridden to return your own backend graph class. + @nx._dispatchable(name="digraph__new__", graphs=None, returns_graph=True) + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, incoming_graph_data=None, **attr): + """Initialize a graph with edges, name, or graph attributes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be an edge list, or any + NetworkX graph object. If the corresponding optional Python + packages are installed the data can also be a 2D NumPy array, a + SciPy sparse array, or a PyGraphviz graph. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + convert + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G = nx.Graph(name="my graph") + >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges + >>> G = nx.Graph(e) + + Arbitrary graph attribute pairs (key=value) may be assigned + + >>> G = nx.Graph(e, day="Friday") + >>> G.graph + {'day': 'Friday'} + + """ + self.graph = self.graph_attr_dict_factory() # dictionary for graph attributes + self._node = self.node_dict_factory() # dictionary for node attr + # We store two adjacency lists: + # the predecessors of node n are stored in the dict self._pred + # the successors of node n are stored in the dict self._succ=self._adj + self._adj = self.adjlist_outer_dict_factory() # empty adjacency dict successor + self._pred = self.adjlist_outer_dict_factory() # predecessor + # Note: self._succ = self._adj # successor + + self.__networkx_cache__ = {} + # attempt to load graph with data + if incoming_graph_data is not None: + convert.to_networkx_graph(incoming_graph_data, create_using=self) + # load graph attributes (must be after convert) + attr.pop("backend", None) # Ignore explicit `backend="networkx"` + self.graph.update(attr) + + @cached_property + def adj(self): + """Graph adjacency object holding the neighbors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edge-data-dict. So `G.adj[3][2]['color'] = 'blue'` sets + the color of the edge `(3, 2)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, datadict in G.adj[n].items():`. + + The neighbor information is also provided by subscripting the graph. + So `for nbr, foovalue in G[node].data('foo', default=1):` works. + + For directed graphs, `G.adj` holds outgoing (successor) info. + """ + return AdjacencyView(self._succ) + + @cached_property + def succ(self): + """Graph adjacency object holding the successors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edge-data-dict. So `G.succ[3][2]['color'] = 'blue'` sets + the color of the edge `(3, 2)` to `"blue"`. + + Iterating over G.succ behaves like a dict. Useful idioms include + `for nbr, datadict in G.succ[n].items():`. A data-view not provided + by dicts also exists: `for nbr, foovalue in G.succ[node].data('foo'):` + and a default can be set via a `default` argument to the `data` method. + + The neighbor information is also provided by subscripting the graph. + So `for nbr, foovalue in G[node].data('foo', default=1):` works. + + For directed graphs, `G.adj` is identical to `G.succ`. + """ + return AdjacencyView(self._succ) + + @cached_property + def pred(self): + """Graph adjacency object holding the predecessors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edge-data-dict. So `G.pred[2][3]['color'] = 'blue'` sets + the color of the edge `(3, 2)` to `"blue"`. + + Iterating over G.pred behaves like a dict. Useful idioms include + `for nbr, datadict in G.pred[n].items():`. A data-view not provided + by dicts also exists: `for nbr, foovalue in G.pred[node].data('foo'):` + A default can be set via a `default` argument to the `data` method. + """ + return AdjacencyView(self._pred) + + def add_node(self, node_for_adding, **attr): + """Add a single node `node_for_adding` and update node attributes. + + Parameters + ---------- + node_for_adding : node + A node can be any hashable Python object except None. + attr : keyword arguments, optional + Set or change node attributes using key=value. + + See Also + -------- + add_nodes_from + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_node(1) + >>> G.add_node("Hello") + >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)]) + >>> G.add_node(K3) + >>> G.number_of_nodes() + 3 + + Use keywords set/change node attributes: + + >>> G.add_node(1, size=10) + >>> G.add_node(3, weight=0.4, UTM=("13S", 382871, 3972649)) + + Notes + ----- + A hashable object is one that can be used as a key in a Python + dictionary. This includes strings, numbers, tuples of strings + and numbers, etc. + + On many platforms hashable items also include mutables such as + NetworkX Graphs, though one should be careful that the hash + doesn't change on mutables. + """ + if node_for_adding not in self._succ: + if node_for_adding is None: + raise ValueError("None cannot be a node") + self._succ[node_for_adding] = self.adjlist_inner_dict_factory() + self._pred[node_for_adding] = self.adjlist_inner_dict_factory() + attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory() + attr_dict.update(attr) + else: # update attr even if node already exists + self._node[node_for_adding].update(attr) + nx._clear_cache(self) + + def add_nodes_from(self, nodes_for_adding, **attr): + """Add multiple nodes. + + Parameters + ---------- + nodes_for_adding : iterable container + A container of nodes (list, dict, set, etc.). + OR + A container of (node, attribute dict) tuples. + Node attributes are updated using the attribute dict. + attr : keyword arguments, optional (default= no attributes) + Update attributes for all nodes in nodes. + Node attributes specified in nodes as a tuple take + precedence over attributes specified via keyword arguments. + + See Also + -------- + add_node + + Notes + ----- + When adding nodes from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_nodes)`, and pass this + object to `G.add_nodes_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_nodes_from("Hello") + >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)]) + >>> G.add_nodes_from(K3) + >>> sorted(G.nodes(), key=str) + [0, 1, 2, 'H', 'e', 'l', 'o'] + + Use keywords to update specific node attributes for every node. + + >>> G.add_nodes_from([1, 2], size=10) + >>> G.add_nodes_from([3, 4], weight=0.4) + + Use (node, attrdict) tuples to update attributes for specific nodes. + + >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})]) + >>> G.nodes[1]["size"] + 11 + >>> H = nx.Graph() + >>> H.add_nodes_from(G.nodes(data=True)) + >>> H.nodes[1]["size"] + 11 + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.DiGraph([(0, 1), (1, 2), (3, 4)]) + >>> # wrong way - will raise RuntimeError + >>> # G.add_nodes_from(n + 1 for n in G.nodes) + >>> # correct way + >>> G.add_nodes_from(list(n + 1 for n in G.nodes)) + """ + for n in nodes_for_adding: + try: + newnode = n not in self._node + newdict = attr + except TypeError: + n, ndict = n + newnode = n not in self._node + newdict = attr.copy() + newdict.update(ndict) + if newnode: + if n is None: + raise ValueError("None cannot be a node") + self._succ[n] = self.adjlist_inner_dict_factory() + self._pred[n] = self.adjlist_inner_dict_factory() + self._node[n] = self.node_attr_dict_factory() + self._node[n].update(newdict) + nx._clear_cache(self) + + def remove_node(self, n): + """Remove node n. + + Removes the node n and all adjacent edges. + Attempting to remove a nonexistent node will raise an exception. + + Parameters + ---------- + n : node + A node in the graph + + Raises + ------ + NetworkXError + If n is not in the graph. + + See Also + -------- + remove_nodes_from + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> list(G.edges) + [(0, 1), (1, 2)] + >>> G.remove_node(1) + >>> list(G.edges) + [] + + """ + try: + nbrs = self._succ[n] + del self._node[n] + except KeyError as err: # NetworkXError if n not in self + raise NetworkXError(f"The node {n} is not in the digraph.") from err + for u in nbrs: + del self._pred[u][n] # remove all edges n-u in digraph + del self._succ[n] # remove node from succ + for u in self._pred[n]: + del self._succ[u][n] # remove all edges n-u in digraph + del self._pred[n] # remove node from pred + nx._clear_cache(self) + + def remove_nodes_from(self, nodes): + """Remove multiple nodes. + + Parameters + ---------- + nodes : iterable container + A container of nodes (list, dict, set, etc.). If a node + in the container is not in the graph it is silently ignored. + + See Also + -------- + remove_node + + Notes + ----- + When removing nodes from an iterator over the graph you are changing, + a `RuntimeError` will be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_nodes)`, and pass this + object to `G.remove_nodes_from`. + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> e = list(G.nodes) + >>> e + [0, 1, 2] + >>> G.remove_nodes_from(e) + >>> list(G.nodes) + [] + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.DiGraph([(0, 1), (1, 2), (3, 4)]) + >>> # this command will fail, as the graph's dict is modified during iteration + >>> # G.remove_nodes_from(n for n in G.nodes if n < 2) + >>> # this command will work, since the dictionary underlying graph is not modified + >>> G.remove_nodes_from(list(n for n in G.nodes if n < 2)) + """ + for n in nodes: + try: + succs = self._succ[n] + del self._node[n] + for u in succs: + del self._pred[u][n] # remove all edges n-u in digraph + del self._succ[n] # now remove node + for u in self._pred[n]: + del self._succ[u][n] # remove all edges n-u in digraph + del self._pred[n] # now remove node + except KeyError: + pass # silent failure on remove + nx._clear_cache(self) + + def add_edge(self, u_of_edge, v_of_edge, **attr): + """Add an edge between u and v. + + The nodes u and v will be automatically added if they are + not already in the graph. + + Edge attributes can be specified with keywords or by directly + accessing the edge's attribute dictionary. See examples below. + + Parameters + ---------- + u_of_edge, v_of_edge : nodes + Nodes can be, for example, strings or numbers. + Nodes must be hashable (and not None) Python objects. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + See Also + -------- + add_edges_from : add a collection of edges + + Notes + ----- + Adding an edge that already exists updates the edge data. + + Many NetworkX algorithms designed for weighted graphs use + an edge attribute (by default `weight`) to hold a numerical value. + + Examples + -------- + The following all add the edge e=(1, 2) to graph G: + + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> e = (1, 2) + >>> G.add_edge(1, 2) # explicit two-node form + >>> G.add_edge(*e) # single edge as tuple of two nodes + >>> G.add_edges_from([(1, 2)]) # add edges from iterable container + + Associate data to edges using keywords: + + >>> G.add_edge(1, 2, weight=3) + >>> G.add_edge(1, 3, weight=7, capacity=15, length=342.7) + + For non-string attribute keys, use subscript notation. + + >>> G.add_edge(1, 2) + >>> G[1][2].update({0: 5}) + >>> G.edges[1, 2].update({0: 5}) + """ + u, v = u_of_edge, v_of_edge + # add nodes + if u not in self._succ: + if u is None: + raise ValueError("None cannot be a node") + self._succ[u] = self.adjlist_inner_dict_factory() + self._pred[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._succ: + if v is None: + raise ValueError("None cannot be a node") + self._succ[v] = self.adjlist_inner_dict_factory() + self._pred[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + # add the edge + datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) + datadict.update(attr) + self._succ[u][v] = datadict + self._pred[v][u] = datadict + nx._clear_cache(self) + + def add_edges_from(self, ebunch_to_add, **attr): + """Add all the edges in ebunch_to_add. + + Parameters + ---------- + ebunch_to_add : container of edges + Each edge given in the container will be added to the + graph. The edges must be given as 2-tuples (u, v) or + 3-tuples (u, v, d) where d is a dictionary containing edge data. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + See Also + -------- + add_edge : add a single edge + add_weighted_edges_from : convenient way to add weighted edges + + Notes + ----- + Adding the same edge twice has no effect but any edge data + will be updated when each duplicate edge is added. + + Edge attributes specified in an ebunch take precedence over + attributes specified via keyword arguments. + + When adding edges from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_edges)`, and pass this + object to `G.add_edges_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples + >>> e = zip(range(0, 3), range(1, 4)) + >>> G.add_edges_from(e) # Add the path graph 0-1-2-3 + + Associate data to edges + + >>> G.add_edges_from([(1, 2), (2, 3)], weight=3) + >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898") + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4)]) + >>> # Grow graph by one new node, adding edges to all existing nodes. + >>> # wrong way - will raise RuntimeError + >>> # G.add_edges_from(((5, n) for n in G.nodes)) + >>> # right way - note that there will be no self-edge for node 5 + >>> G.add_edges_from(list((5, n) for n in G.nodes)) + """ + for e in ebunch_to_add: + ne = len(e) + if ne == 3: + u, v, dd = e + elif ne == 2: + u, v = e + dd = {} + else: + raise NetworkXError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.") + if u not in self._succ: + if u is None: + raise ValueError("None cannot be a node") + self._succ[u] = self.adjlist_inner_dict_factory() + self._pred[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._succ: + if v is None: + raise ValueError("None cannot be a node") + self._succ[v] = self.adjlist_inner_dict_factory() + self._pred[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) + datadict.update(attr) + datadict.update(dd) + self._succ[u][v] = datadict + self._pred[v][u] = datadict + nx._clear_cache(self) + + def remove_edge(self, u, v): + """Remove the edge between u and v. + + Parameters + ---------- + u, v : nodes + Remove the edge between nodes u and v. + + Raises + ------ + NetworkXError + If there is not an edge between u and v. + + See Also + -------- + remove_edges_from : remove a collection of edges + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, etc + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.remove_edge(0, 1) + >>> e = (1, 2) + >>> G.remove_edge(*e) # unpacks e from an edge tuple + >>> e = (2, 3, {"weight": 7}) # an edge with attribute data + >>> G.remove_edge(*e[:2]) # select first part of edge tuple + """ + try: + del self._succ[u][v] + del self._pred[v][u] + except KeyError as err: + raise NetworkXError(f"The edge {u}-{v} not in graph.") from err + nx._clear_cache(self) + + def remove_edges_from(self, ebunch): + """Remove all edges specified in ebunch. + + Parameters + ---------- + ebunch: list or container of edge tuples + Each edge given in the list or container will be removed + from the graph. The edges can be: + + - 2-tuples (u, v) edge between u and v. + - 3-tuples (u, v, k) where k is ignored. + + See Also + -------- + remove_edge : remove a single edge + + Notes + ----- + Will fail silently if an edge in ebunch is not in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> ebunch = [(1, 2), (2, 3)] + >>> G.remove_edges_from(ebunch) + """ + for e in ebunch: + u, v = e[:2] # ignore edge data + if u in self._succ and v in self._succ[u]: + del self._succ[u][v] + del self._pred[v][u] + nx._clear_cache(self) + + def has_successor(self, u, v): + """Returns True if node u has successor v. + + This is true if graph has the edge u->v. + """ + return u in self._succ and v in self._succ[u] + + def has_predecessor(self, u, v): + """Returns True if node u has predecessor v. + + This is true if graph has the edge u<-v. + """ + return u in self._pred and v in self._pred[u] + + def successors(self, n): + """Returns an iterator over successor nodes of n. + + A successor of n is a node m such that there exists a directed + edge from n to m. + + Parameters + ---------- + n : node + A node in the graph + + Raises + ------ + NetworkXError + If n is not in the graph. + + See Also + -------- + predecessors + + Notes + ----- + neighbors() and successors() are the same. + """ + try: + return iter(self._succ[n]) + except KeyError as err: + raise NetworkXError(f"The node {n} is not in the digraph.") from err + + # digraph definitions + neighbors = successors + + def predecessors(self, n): + """Returns an iterator over predecessor nodes of n. + + A predecessor of n is a node m such that there exists a directed + edge from m to n. + + Parameters + ---------- + n : node + A node in the graph + + Raises + ------ + NetworkXError + If n is not in the graph. + + See Also + -------- + successors + """ + try: + return iter(self._pred[n]) + except KeyError as err: + raise NetworkXError(f"The node {n} is not in the digraph.") from err + + @cached_property + def edges(self): + """An OutEdgeView of the DiGraph as G.edges or G.edges(). + + edges(self, nbunch=None, data=False, default=None) + + The OutEdgeView provides set-like operations on the edge-tuples + as well as edge attribute lookup. When called, it also provides + an EdgeDataView object which allows control of access to edge + attributes (but does not provide set-like operations). + Hence, `G.edges[u, v]['color']` provides the value of the color + attribute for edge `(u, v)` while + `for (u, v, c) in G.edges.data('color', default='red'):` + iterates through all the edges yielding the color attribute + with default `'red'` if no color attribute exists. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges from these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + edges : OutEdgeView + A view of edge attributes, usually it iterates over (u, v) + or (u, v, d) tuples of edges, but can also be used for + attribute lookup as `edges[u, v]['foo']`. + + See Also + -------- + in_edges, out_edges + + Notes + ----- + Nodes in nbunch that are not in the graph will be (quietly) ignored. + For directed graphs this returns the out-edges. + + Examples + -------- + >>> G = nx.DiGraph() # or MultiDiGraph, etc + >>> nx.add_path(G, [0, 1, 2]) + >>> G.add_edge(2, 3, weight=5) + >>> [e for e in G.edges] + [(0, 1), (1, 2), (2, 3)] + >>> G.edges.data() # default data is {} (empty dict) + OutEdgeDataView([(0, 1, {}), (1, 2, {}), (2, 3, {'weight': 5})]) + >>> G.edges.data("weight", default=1) + OutEdgeDataView([(0, 1, 1), (1, 2, 1), (2, 3, 5)]) + >>> G.edges([0, 2]) # only edges originating from these nodes + OutEdgeDataView([(0, 1), (2, 3)]) + >>> G.edges(0) # only edges from node 0 + OutEdgeDataView([(0, 1)]) + + """ + return OutEdgeView(self) + + # alias out_edges to edges + @cached_property + def out_edges(self): + return OutEdgeView(self) + + out_edges.__doc__ = edges.__doc__ + + @cached_property + def in_edges(self): + """A view of the in edges of the graph as G.in_edges or G.in_edges(). + + in_edges(self, nbunch=None, data=False, default=None): + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + in_edges : InEdgeView or InEdgeDataView + A view of edge attributes, usually it iterates over (u, v) + or (u, v, d) tuples of edges, but can also be used for + attribute lookup as `edges[u, v]['foo']`. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge(1, 2, color="blue") + >>> G.in_edges() + InEdgeView([(1, 2)]) + >>> G.in_edges(nbunch=2) + InEdgeDataView([(1, 2)]) + + See Also + -------- + edges + """ + return InEdgeView(self) + + @cached_property + def degree(self): + """A DegreeView for the Graph as G.degree or G.degree(). + + The node degree is the number of edges adjacent to the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator for (node, degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + DiDegreeView or int + If multiple nodes are requested (the default), returns a `DiDegreeView` + mapping nodes to their degree. + If a single node is requested, returns the degree of the node as an integer. + + See Also + -------- + in_degree, out_degree + + Examples + -------- + >>> G = nx.DiGraph() # or MultiDiGraph + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.degree(0) # node 0 with degree 1 + 1 + >>> list(G.degree([0, 1, 2])) + [(0, 1), (1, 2), (2, 2)] + + """ + return DiDegreeView(self) + + @cached_property + def in_degree(self): + """An InDegreeView for (node, in_degree) or in_degree for single node. + + The node in_degree is the number of edges pointing to the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iteration over (node, in_degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + If a single node is requested + deg : int + In-degree of the node + + OR if multiple nodes are requested + nd_iter : iterator + The iterator returns two-tuples of (node, in-degree). + + See Also + -------- + degree, out_degree + + Examples + -------- + >>> G = nx.DiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.in_degree(0) # node 0 with degree 0 + 0 + >>> list(G.in_degree([0, 1, 2])) + [(0, 0), (1, 1), (2, 1)] + + """ + return InDegreeView(self) + + @cached_property + def out_degree(self): + """An OutDegreeView for (node, out_degree) + + The node out_degree is the number of edges pointing out of the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator over (node, out_degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + If a single node is requested + deg : int + Out-degree of the node + + OR if multiple nodes are requested + nd_iter : iterator + The iterator returns two-tuples of (node, out-degree). + + See Also + -------- + degree, in_degree + + Examples + -------- + >>> G = nx.DiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.out_degree(0) # node 0 with degree 1 + 1 + >>> list(G.out_degree([0, 1, 2])) + [(0, 1), (1, 1), (2, 1)] + + """ + return OutDegreeView(self) + + def clear(self): + """Remove all nodes and edges from the graph. + + This also removes the name, and all graph, node, and edge attributes. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.clear() + >>> list(G.nodes) + [] + >>> list(G.edges) + [] + + """ + self._succ.clear() + self._pred.clear() + self._node.clear() + self.graph.clear() + nx._clear_cache(self) + + def clear_edges(self): + """Remove all edges from the graph without altering nodes. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.clear_edges() + >>> list(G.nodes) + [0, 1, 2, 3] + >>> list(G.edges) + [] + + """ + for predecessor_dict in self._pred.values(): + predecessor_dict.clear() + for successor_dict in self._succ.values(): + successor_dict.clear() + nx._clear_cache(self) + + def is_multigraph(self): + """Returns True if graph is a multigraph, False otherwise.""" + return False + + def is_directed(self): + """Returns True if graph is directed, False otherwise.""" + return True + + def to_undirected(self, reciprocal=False, as_view=False): + """Returns an undirected representation of the digraph. + + Parameters + ---------- + reciprocal : bool (optional) + If True only keep edges that appear in both directions + in the original digraph. + as_view : bool (optional, default=False) + If True return an undirected view of the original directed graph. + + Returns + ------- + G : Graph + An undirected graph with the same name and nodes and + with edge (u, v, data) if either (u, v, data) or (v, u, data) + is in the digraph. If both edges exist in digraph and + their edge data is different, only one edge is created + with an arbitrary choice of which edge data to use. + You must check and correct for this manually if desired. + + See Also + -------- + Graph, copy, add_edge, add_edges_from + + Notes + ----- + If edges in both directions (u, v) and (v, u) exist in the + graph, attributes for the new undirected edge will be a combination of + the attributes of the directed edges. The edge data is updated + in the (arbitrary) order that the edges are encountered. For + more customized control of the edge attributes use add_edge(). + + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar G=DiGraph(D) which returns a + shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed DiGraph to use dict-like objects + in the data structure, those changes do not transfer to the + Graph created by this method. + + Examples + -------- + >>> G = nx.path_graph(2) # or MultiGraph, etc + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1), (1, 0)] + >>> G2 = H.to_undirected() + >>> list(G2.edges) + [(0, 1)] + """ + graph_class = self.to_undirected_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + if reciprocal is True: + G.add_edges_from( + (u, v, deepcopy(d)) + for u, nbrs in self._adj.items() + for v, d in nbrs.items() + if v in self._pred[u] + ) + else: + G.add_edges_from( + (u, v, deepcopy(d)) + for u, nbrs in self._adj.items() + for v, d in nbrs.items() + ) + return G + + def reverse(self, copy=True): + """Returns the reverse of the graph. + + The reverse is a graph with the same nodes and edges + but with the directions of the edges reversed. + + Parameters + ---------- + copy : bool optional (default=True) + If True, return a new DiGraph holding the reversed edges. + If False, the reverse graph is created using a view of + the original graph. + """ + if copy: + H = self.__class__() + H.graph.update(deepcopy(self.graph)) + H.add_nodes_from((n, deepcopy(d)) for n, d in self.nodes.items()) + H.add_edges_from((v, u, deepcopy(d)) for u, v, d in self.edges(data=True)) + return H + return nx.reverse_view(self) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/filters.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..e989e22bb6d7e79b6eab34103edd263d82694fd4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/filters.py @@ -0,0 +1,95 @@ +"""Filter factories to hide or show sets of nodes and edges. + +These filters return the function used when creating `SubGraph`. +""" + +__all__ = [ + "no_filter", + "hide_nodes", + "hide_edges", + "hide_multiedges", + "hide_diedges", + "hide_multidiedges", + "show_nodes", + "show_edges", + "show_multiedges", + "show_diedges", + "show_multidiedges", +] + + +def no_filter(*items): + """Returns a filter function that always evaluates to True.""" + return True + + +def hide_nodes(nodes): + """Returns a filter function that hides specific nodes.""" + nodes = set(nodes) + return lambda node: node not in nodes + + +def hide_diedges(edges): + """Returns a filter function that hides specific directed edges.""" + edges = {(u, v) for u, v in edges} + return lambda u, v: (u, v) not in edges + + +def hide_edges(edges): + """Returns a filter function that hides specific undirected edges.""" + alledges = set(edges) | {(v, u) for (u, v) in edges} + return lambda u, v: (u, v) not in alledges + + +def hide_multidiedges(edges): + """Returns a filter function that hides specific multi-directed edges.""" + edges = {(u, v, k) for u, v, k in edges} + return lambda u, v, k: (u, v, k) not in edges + + +def hide_multiedges(edges): + """Returns a filter function that hides specific multi-undirected edges.""" + alledges = set(edges) | {(v, u, k) for (u, v, k) in edges} + return lambda u, v, k: (u, v, k) not in alledges + + +# write show_nodes as a class to make SubGraph pickleable +class show_nodes: + """Filter class to show specific nodes. + + Attach the set of nodes as an attribute to speed up this commonly used filter + + Note that another allowed attribute for filters is to store the number of nodes + on the filter as attribute `length` (used in `__len__`). It is a user + responsibility to ensure this attribute is accurate if present. + """ + + def __init__(self, nodes): + self.nodes = set(nodes) + + def __call__(self, node): + return node in self.nodes + + +def show_diedges(edges): + """Returns a filter function that shows specific directed edges.""" + edges = {(u, v) for u, v in edges} + return lambda u, v: (u, v) in edges + + +def show_edges(edges): + """Returns a filter function that shows specific undirected edges.""" + alledges = set(edges) | {(v, u) for (u, v) in edges} + return lambda u, v: (u, v) in alledges + + +def show_multidiedges(edges): + """Returns a filter function that shows specific multi-directed edges.""" + edges = {(u, v, k) for u, v, k in edges} + return lambda u, v, k: (u, v, k) in edges + + +def show_multiedges(edges): + """Returns a filter function that shows specific multi-undirected edges.""" + alledges = set(edges) | {(v, u, k) for (u, v, k) in edges} + return lambda u, v, k: (u, v, k) in alledges diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/function.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/function.py new file mode 100644 index 0000000000000000000000000000000000000000..31f088ede87f01b2815514f1914f67e222ff66b6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/function.py @@ -0,0 +1,1549 @@ +"""Functional interface to graph methods and assorted utilities.""" + +from collections import Counter +from itertools import chain + +import networkx as nx +from networkx.utils import not_implemented_for, pairwise + +__all__ = [ + "nodes", + "edges", + "degree", + "degree_histogram", + "neighbors", + "number_of_nodes", + "number_of_edges", + "density", + "is_directed", + "freeze", + "is_frozen", + "subgraph", + "induced_subgraph", + "edge_subgraph", + "restricted_view", + "to_directed", + "to_undirected", + "add_star", + "add_path", + "add_cycle", + "create_empty_copy", + "set_node_attributes", + "get_node_attributes", + "remove_node_attributes", + "set_edge_attributes", + "get_edge_attributes", + "remove_edge_attributes", + "all_neighbors", + "non_neighbors", + "non_edges", + "common_neighbors", + "is_weighted", + "is_negatively_weighted", + "is_empty", + "selfloop_edges", + "nodes_with_selfloops", + "number_of_selfloops", + "path_weight", + "is_path", + "describe", +] + + +def nodes(G): + """Returns a NodeView over the graph nodes. + + This function wraps the :func:`G.nodes ` property. + """ + return G.nodes() + + +def edges(G, nbunch=None): + """Returns an edge view of edges incident to nodes in nbunch. + + Return all edges if nbunch is unspecified or nbunch=None. + + For digraphs, edges=out_edges + + This function wraps the :func:`G.edges ` property. + """ + return G.edges(nbunch) + + +def degree(G, nbunch=None, weight=None): + """Returns a degree view of single node or of nbunch of nodes. + If nbunch is omitted, then return degrees of *all* nodes. + + This function wraps the :func:`G.degree ` property. + """ + return G.degree(nbunch, weight) + + +def neighbors(G, n): + """Returns an iterator over all neighbors of node n. + + This function wraps the :func:`G.neighbors ` function. + """ + return G.neighbors(n) + + +def number_of_nodes(G): + """Returns the number of nodes in the graph. + + This function wraps the :func:`G.number_of_nodes ` function. + """ + return G.number_of_nodes() + + +def number_of_edges(G): + """Returns the number of edges in the graph. + + This function wraps the :func:`G.number_of_edges ` function. + """ + return G.number_of_edges() + + +def density(G): + r"""Returns the density of a graph. + + The density for undirected graphs is + + .. math:: + + d = \frac{2m}{n(n-1)}, + + and for directed graphs is + + .. math:: + + d = \frac{m}{n(n-1)}, + + where `n` is the number of nodes and `m` is the number of edges in `G`. + + Notes + ----- + The density is 0 for a graph without edges and 1 for a complete graph. + The density of multigraphs can be higher than 1. + + Self loops are counted in the total number of edges so graphs with self + loops can have density higher than 1. + """ + n = number_of_nodes(G) + m = number_of_edges(G) + if m == 0 or n <= 1: + return 0 + d = m / (n * (n - 1)) + if not G.is_directed(): + d *= 2 + return d + + +def degree_histogram(G): + """Returns a list of the frequency of each degree value. + + Parameters + ---------- + G : Networkx graph + A graph + + Returns + ------- + hist : list + A list of frequencies of degrees. + The degree values are the index in the list. + + Notes + ----- + Note: the bins are width one, hence len(list) can be large + (Order(number_of_edges)) + """ + counts = Counter(d for n, d in G.degree()) + return [counts.get(i, 0) for i in range(max(counts) + 1 if counts else 0)] + + +def is_directed(G): + """Return True if graph is directed.""" + return G.is_directed() + + +def frozen(*args, **kwargs): + """Dummy method for raising errors when trying to modify frozen graphs""" + raise nx.NetworkXError("Frozen graph can't be modified") + + +def freeze(G): + """Modify graph to prevent further change by adding or removing + nodes or edges. + + Node and edge data can still be modified. + + Parameters + ---------- + G : graph + A NetworkX graph + + Examples + -------- + >>> G = nx.path_graph(4) + >>> G = nx.freeze(G) + >>> try: + ... G.add_edge(4, 5) + ... except nx.NetworkXError as err: + ... print(str(err)) + Frozen graph can't be modified + + Notes + ----- + To "unfreeze" a graph you must make a copy by creating a new graph object: + + >>> graph = nx.path_graph(4) + >>> frozen_graph = nx.freeze(graph) + >>> unfrozen_graph = nx.Graph(frozen_graph) + >>> nx.is_frozen(unfrozen_graph) + False + + See Also + -------- + is_frozen + """ + G.add_node = frozen + G.add_nodes_from = frozen + G.remove_node = frozen + G.remove_nodes_from = frozen + G.add_edge = frozen + G.add_edges_from = frozen + G.add_weighted_edges_from = frozen + G.remove_edge = frozen + G.remove_edges_from = frozen + G.clear = frozen + G.clear_edges = frozen + G.frozen = True + return G + + +def is_frozen(G): + """Returns True if graph is frozen. + + Parameters + ---------- + G : graph + A NetworkX graph + + See Also + -------- + freeze + """ + try: + return G.frozen + except AttributeError: + return False + + +def add_star(G_to_add_to, nodes_for_star, **attr): + """Add a star to Graph G_to_add_to. + + The first node in `nodes_for_star` is the middle of the star. + It is connected to all other nodes. + + Parameters + ---------- + G_to_add_to : graph + A NetworkX graph + nodes_for_star : iterable container + A container of nodes. + attr : keyword arguments, optional (default= no attributes) + Attributes to add to every edge in star. + + See Also + -------- + add_path, add_cycle + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_star(G, [0, 1, 2, 3]) + >>> nx.add_star(G, [10, 11, 12], weight=2) + """ + nlist = iter(nodes_for_star) + try: + v = next(nlist) + except StopIteration: + return + G_to_add_to.add_node(v) + edges = ((v, n) for n in nlist) + G_to_add_to.add_edges_from(edges, **attr) + + +def add_path(G_to_add_to, nodes_for_path, **attr): + """Add a path to the Graph G_to_add_to. + + Parameters + ---------- + G_to_add_to : graph + A NetworkX graph + nodes_for_path : iterable container + A container of nodes. A path will be constructed from + the nodes (in order) and added to the graph. + attr : keyword arguments, optional (default= no attributes) + Attributes to add to every edge in path. + + See Also + -------- + add_star, add_cycle + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> nx.add_path(G, [10, 11, 12], weight=7) + """ + nlist = iter(nodes_for_path) + try: + first_node = next(nlist) + except StopIteration: + return + G_to_add_to.add_node(first_node) + G_to_add_to.add_edges_from(pairwise(chain((first_node,), nlist)), **attr) + + +def add_cycle(G_to_add_to, nodes_for_cycle, **attr): + """Add a cycle to the Graph G_to_add_to. + + Parameters + ---------- + G_to_add_to : graph + A NetworkX graph + nodes_for_cycle: iterable container + A container of nodes. A cycle will be constructed from + the nodes (in order) and added to the graph. + attr : keyword arguments, optional (default= no attributes) + Attributes to add to every edge in cycle. + + See Also + -------- + add_path, add_star + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> nx.add_cycle(G, [0, 1, 2, 3]) + >>> nx.add_cycle(G, [10, 11, 12], weight=7) + """ + nlist = iter(nodes_for_cycle) + try: + first_node = next(nlist) + except StopIteration: + return + G_to_add_to.add_node(first_node) + G_to_add_to.add_edges_from( + pairwise(chain((first_node,), nlist), cyclic=True), **attr + ) + + +def subgraph(G, nbunch): + """Returns the subgraph induced on nodes in nbunch. + + Parameters + ---------- + G : graph + A NetworkX graph + + nbunch : list, iterable + A container of nodes that will be iterated through once (thus + it should be an iterator or be iterable). Each element of the + container should be a valid node type: any hashable type except + None. If nbunch is None, return all edges data in the graph. + Nodes in nbunch that are not in the graph will be (quietly) + ignored. + + Notes + ----- + subgraph(G) calls G.subgraph() + """ + return G.subgraph(nbunch) + + +def induced_subgraph(G, nbunch): + """Returns a SubGraph view of `G` showing only nodes in nbunch. + + The induced subgraph of a graph on a set of nodes N is the + graph with nodes N and edges from G which have both ends in N. + + Parameters + ---------- + G : NetworkX Graph + nbunch : node, container of nodes or None (for all nodes) + + Returns + ------- + subgraph : SubGraph View + A read-only view of the subgraph in `G` induced by the nodes. + Changes to the graph `G` will be reflected in the view. + + Notes + ----- + To create a mutable subgraph with its own copies of nodes + edges and attributes use `subgraph.copy()` or `Graph(subgraph)` + + For an inplace reduction of a graph to a subgraph you can remove nodes: + `G.remove_nodes_from(n in G if n not in set(nbunch))` + + If you are going to compute subgraphs of your subgraphs you could + end up with a chain of views that can be very slow once the chain + has about 15 views in it. If they are all induced subgraphs, you + can short-cut the chain by making them all subgraphs of the original + graph. The graph class method `G.subgraph` does this when `G` is + a subgraph. In contrast, this function allows you to choose to build + chains or not, as you wish. The returned subgraph is a view on `G`. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> H = nx.induced_subgraph(G, [0, 1, 3]) + >>> list(H.edges) + [(0, 1)] + >>> list(H.nodes) + [0, 1, 3] + """ + induced_nodes = nx.filters.show_nodes(G.nbunch_iter(nbunch)) + return nx.subgraph_view(G, filter_node=induced_nodes) + + +def edge_subgraph(G, edges): + """Returns a view of the subgraph induced by the specified edges. + + The induced subgraph contains each edge in `edges` and each + node incident to any of those edges. + + Parameters + ---------- + G : NetworkX Graph + edges : iterable + An iterable of edges. Edges not present in `G` are ignored. + + Returns + ------- + subgraph : SubGraph View + A read-only edge-induced subgraph of `G`. + Changes to `G` are reflected in the view. + + Notes + ----- + To create a mutable subgraph with its own copies of nodes + edges and attributes use `subgraph.copy()` or `Graph(subgraph)` + + If you create a subgraph of a subgraph recursively you can end up + with a chain of subgraphs that becomes very slow with about 15 + nested subgraph views. Luckily the edge_subgraph filter nests + nicely so you can use the original graph as G in this function + to avoid chains. We do not rule out chains programmatically so + that odd cases like an `edge_subgraph` of a `restricted_view` + can be created. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> H = G.edge_subgraph([(0, 1), (3, 4)]) + >>> list(H.nodes) + [0, 1, 3, 4] + >>> list(H.edges) + [(0, 1), (3, 4)] + """ + nxf = nx.filters + edges = set(edges) + nodes = set() + for e in edges: + nodes.update(e[:2]) + induced_nodes = nxf.show_nodes(nodes) + if G.is_multigraph(): + if G.is_directed(): + induced_edges = nxf.show_multidiedges(edges) + else: + induced_edges = nxf.show_multiedges(edges) + else: + if G.is_directed(): + induced_edges = nxf.show_diedges(edges) + else: + induced_edges = nxf.show_edges(edges) + return nx.subgraph_view(G, filter_node=induced_nodes, filter_edge=induced_edges) + + +def restricted_view(G, nodes, edges): + """Returns a view of `G` with hidden nodes and edges. + + The resulting subgraph filters out node `nodes` and edges `edges`. + Filtered out nodes also filter out any of their edges. + + Parameters + ---------- + G : NetworkX Graph + nodes : iterable + An iterable of nodes. Nodes not present in `G` are ignored. + edges : iterable + An iterable of edges. Edges not present in `G` are ignored. + + Returns + ------- + subgraph : SubGraph View + A read-only restricted view of `G` filtering out nodes and edges. + Changes to `G` are reflected in the view. + + Notes + ----- + To create a mutable subgraph with its own copies of nodes + edges and attributes use `subgraph.copy()` or `Graph(subgraph)` + + If you create a subgraph of a subgraph recursively you may end up + with a chain of subgraph views. Such chains can get quite slow + for lengths near 15. To avoid long chains, try to make your subgraph + based on the original graph. We do not rule out chains programmatically + so that odd cases like an `edge_subgraph` of a `restricted_view` + can be created. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> H = nx.restricted_view(G, [0], [(1, 2), (3, 4)]) + >>> list(H.nodes) + [1, 2, 3, 4] + >>> list(H.edges) + [(2, 3)] + """ + nxf = nx.filters + hide_nodes = nxf.hide_nodes(nodes) + if G.is_multigraph(): + if G.is_directed(): + hide_edges = nxf.hide_multidiedges(edges) + else: + hide_edges = nxf.hide_multiedges(edges) + else: + if G.is_directed(): + hide_edges = nxf.hide_diedges(edges) + else: + hide_edges = nxf.hide_edges(edges) + return nx.subgraph_view(G, filter_node=hide_nodes, filter_edge=hide_edges) + + +def to_directed(graph): + """Returns a directed view of the graph `graph`. + + Identical to graph.to_directed(as_view=True) + Note that graph.to_directed defaults to `as_view=False` + while this function always provides a view. + """ + return graph.to_directed(as_view=True) + + +def to_undirected(graph): + """Returns an undirected view of the graph `graph`. + + Identical to graph.to_undirected(as_view=True) + Note that graph.to_undirected defaults to `as_view=False` + while this function always provides a view. + """ + return graph.to_undirected(as_view=True) + + +def create_empty_copy(G, with_data=True): + """Returns a copy of the graph G with all of the edges removed. + + Parameters + ---------- + G : graph + A NetworkX graph + + with_data : bool (default=True) + Propagate Graph and Nodes data to the new graph. + + See Also + -------- + empty_graph + + """ + H = G.__class__() + H.add_nodes_from(G.nodes(data=with_data)) + if with_data: + H.graph.update(G.graph) + return H + + +@nx._dispatchable(preserve_node_attrs=True, mutates_input=True) +def set_node_attributes(G, values, name=None): + """Sets node attributes from a given value or dictionary of values. + + .. Warning:: The call order of arguments `values` and `name` + switched between v1.x & v2.x. + + Parameters + ---------- + G : NetworkX Graph + + values : scalar value, dict-like + What the node attribute should be set to. If `values` is + not a dictionary, then it is treated as a single attribute value + that is then applied to every node in `G`. This means that if + you provide a mutable object, like a list, updates to that object + will be reflected in the node attribute for every node. + The attribute name will be `name`. + + If `values` is a dict or a dict of dict, it should be keyed + by node to either an attribute value or a dict of attribute key/value + pairs used to update the node's attributes. + + name : string (optional, default=None) + Name of the node attribute to set if values is a scalar. + + Examples + -------- + After computing some property of the nodes of a graph, you may want + to assign a node attribute to store the value of that property for + each node:: + + >>> G = nx.path_graph(3) + >>> bb = nx.betweenness_centrality(G) + >>> isinstance(bb, dict) + True + >>> nx.set_node_attributes(G, bb, "betweenness") + >>> G.nodes[1]["betweenness"] + 1.0 + + If you provide a list as the second argument, updates to the list + will be reflected in the node attribute for each node:: + + >>> G = nx.path_graph(3) + >>> labels = [] + >>> nx.set_node_attributes(G, labels, "labels") + >>> labels.append("foo") + >>> G.nodes[0]["labels"] + ['foo'] + >>> G.nodes[1]["labels"] + ['foo'] + >>> G.nodes[2]["labels"] + ['foo'] + + If you provide a dictionary of dictionaries as the second argument, + the outer dictionary is assumed to be keyed by node to an inner + dictionary of node attributes for that node:: + + >>> G = nx.path_graph(3) + >>> attrs = {0: {"attr1": 20, "attr2": "nothing"}, 1: {"attr2": 3}} + >>> nx.set_node_attributes(G, attrs) + >>> G.nodes[0]["attr1"] + 20 + >>> G.nodes[0]["attr2"] + 'nothing' + >>> G.nodes[1]["attr2"] + 3 + >>> G.nodes[2] + {} + + Note that if the dictionary contains nodes that are not in `G`, the + values are silently ignored:: + + >>> G = nx.Graph() + >>> G.add_node(0) + >>> nx.set_node_attributes(G, {0: "red", 1: "blue"}, name="color") + >>> G.nodes[0]["color"] + 'red' + >>> 1 in G.nodes + False + + """ + # Set node attributes based on type of `values` + if name is not None: # `values` must not be a dict of dict + try: # `values` is a dict + for n, v in values.items(): + try: + G.nodes[n][name] = values[n] + except KeyError: + pass + except AttributeError: # `values` is a constant + for n in G: + G.nodes[n][name] = values + else: # `values` must be dict of dict + for n, d in values.items(): + try: + G.nodes[n].update(d) + except KeyError: + pass + nx._clear_cache(G) + + +@nx._dispatchable(node_attrs={"name": "default"}) +def get_node_attributes(G, name, default=None): + """Get node attributes from graph + + Parameters + ---------- + G : NetworkX Graph + + name : string + Attribute name + + default: object (default=None) + Default value of the node attribute if there is no value set for that + node in graph. If `None` then nodes without this attribute are not + included in the returned dict. + + Returns + ------- + Dictionary of attributes keyed by node. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from([1, 2, 3], color="red") + >>> color = nx.get_node_attributes(G, "color") + >>> color[1] + 'red' + >>> G.add_node(4) + >>> color = nx.get_node_attributes(G, "color", default="yellow") + >>> color[4] + 'yellow' + """ + if default is not None: + return {n: d.get(name, default) for n, d in G.nodes.items()} + return {n: d[name] for n, d in G.nodes.items() if name in d} + + +@nx._dispatchable(preserve_node_attrs=True, mutates_input=True) +def remove_node_attributes(G, *attr_names, nbunch=None): + """Remove node attributes from all nodes in the graph. + + Parameters + ---------- + G : NetworkX Graph + + *attr_names : List of Strings + The attribute names to remove from the graph. + + nbunch : List of Nodes + Remove the node attributes only from the nodes in this list. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from([1, 2, 3], color="blue") + >>> nx.get_node_attributes(G, "color") + {1: 'blue', 2: 'blue', 3: 'blue'} + >>> nx.remove_node_attributes(G, "color") + >>> nx.get_node_attributes(G, "color") + {} + """ + + if nbunch is None: + nbunch = G.nodes() + + for attr in attr_names: + for n, d in G.nodes(data=True): + if n in nbunch: + try: + del d[attr] + except KeyError: + pass + + +@nx._dispatchable(preserve_edge_attrs=True, mutates_input=True) +def set_edge_attributes(G, values, name=None): + """Sets edge attributes from a given value or dictionary of values. + + .. Warning:: The call order of arguments `values` and `name` + switched between v1.x & v2.x. + + Parameters + ---------- + G : NetworkX Graph + + values : scalar value, dict-like + What the edge attribute should be set to. If `values` is + not a dictionary, then it is treated as a single attribute value + that is then applied to every edge in `G`. This means that if + you provide a mutable object, like a list, updates to that object + will be reflected in the edge attribute for each edge. The attribute + name will be `name`. + + If `values` is a dict or a dict of dict, it should be keyed + by edge tuple to either an attribute value or a dict of attribute + key/value pairs used to update the edge's attributes. + For multigraphs, the edge tuples must be of the form ``(u, v, key)``, + where `u` and `v` are nodes and `key` is the edge key. + For non-multigraphs, the keys must be tuples of the form ``(u, v)``. + + name : string (optional, default=None) + Name of the edge attribute to set if values is a scalar. + + Examples + -------- + After computing some property of the edges of a graph, you may want + to assign a edge attribute to store the value of that property for + each edge:: + + >>> G = nx.path_graph(3) + >>> bb = nx.edge_betweenness_centrality(G, normalized=False) + >>> nx.set_edge_attributes(G, bb, "betweenness") + >>> G.edges[1, 2]["betweenness"] + 2.0 + + If you provide a list as the second argument, updates to the list + will be reflected in the edge attribute for each edge:: + + >>> labels = [] + >>> nx.set_edge_attributes(G, labels, "labels") + >>> labels.append("foo") + >>> G.edges[0, 1]["labels"] + ['foo'] + >>> G.edges[1, 2]["labels"] + ['foo'] + + If you provide a dictionary of dictionaries as the second argument, + the entire dictionary will be used to update edge attributes:: + + >>> G = nx.path_graph(3) + >>> attrs = {(0, 1): {"attr1": 20, "attr2": "nothing"}, (1, 2): {"attr2": 3}} + >>> nx.set_edge_attributes(G, attrs) + >>> G[0][1]["attr1"] + 20 + >>> G[0][1]["attr2"] + 'nothing' + >>> G[1][2]["attr2"] + 3 + + The attributes of one Graph can be used to set those of another. + + >>> H = nx.path_graph(3) + >>> nx.set_edge_attributes(H, G.edges) + + Note that if the dict contains edges that are not in `G`, they are + silently ignored:: + + >>> G = nx.Graph([(0, 1)]) + >>> nx.set_edge_attributes(G, {(1, 2): {"weight": 2.0}}) + >>> (1, 2) in G.edges() + False + + For multigraphs, the `values` dict is expected to be keyed by 3-tuples + including the edge key:: + + >>> MG = nx.MultiGraph() + >>> edges = [(0, 1), (0, 1)] + >>> MG.add_edges_from(edges) # Returns list of edge keys + [0, 1] + >>> attributes = {(0, 1, 0): {"cost": 21}, (0, 1, 1): {"cost": 7}} + >>> nx.set_edge_attributes(MG, attributes) + >>> MG[0][1][0]["cost"] + 21 + >>> MG[0][1][1]["cost"] + 7 + + If MultiGraph attributes are desired for a Graph, you must convert the 3-tuple + multiedge to a 2-tuple edge and the last multiedge's attribute value will + overwrite the previous values. Continuing from the previous case we get:: + + >>> H = nx.path_graph([0, 1, 2]) + >>> nx.set_edge_attributes(H, {(u, v): ed for u, v, ed in MG.edges.data()}) + >>> nx.get_edge_attributes(H, "cost") + {(0, 1): 7} + + """ + if name is not None: + # `values` does not contain attribute names + try: + # if `values` is a dict using `.items()` => {edge: value} + if G.is_multigraph(): + for (u, v, key), value in values.items(): + try: + G._adj[u][v][key][name] = value + except KeyError: + pass + else: + for (u, v), value in values.items(): + try: + G._adj[u][v][name] = value + except KeyError: + pass + except AttributeError: + # treat `values` as a constant + for u, v, data in G.edges(data=True): + data[name] = values + else: + # `values` consists of doct-of-dict {edge: {attr: value}} shape + if G.is_multigraph(): + for (u, v, key), d in values.items(): + try: + G._adj[u][v][key].update(d) + except KeyError: + pass + else: + for (u, v), d in values.items(): + try: + G._adj[u][v].update(d) + except KeyError: + pass + nx._clear_cache(G) + + +@nx._dispatchable(edge_attrs={"name": "default"}) +def get_edge_attributes(G, name, default=None): + """Get edge attributes from graph + + Parameters + ---------- + G : NetworkX Graph + + name : string + Attribute name + + default: object (default=None) + Default value of the edge attribute if there is no value set for that + edge in graph. If `None` then edges without this attribute are not + included in the returned dict. + + Returns + ------- + Dictionary of attributes keyed by edge. For (di)graphs, the keys are + 2-tuples of the form: (u, v). For multi(di)graphs, the keys are 3-tuples of + the form: (u, v, key). + + Examples + -------- + >>> G = nx.Graph() + >>> nx.add_path(G, [1, 2, 3], color="red") + >>> color = nx.get_edge_attributes(G, "color") + >>> color[(1, 2)] + 'red' + >>> G.add_edge(3, 4) + >>> color = nx.get_edge_attributes(G, "color", default="yellow") + >>> color[(3, 4)] + 'yellow' + """ + if G.is_multigraph(): + edges = G.edges(keys=True, data=True) + else: + edges = G.edges(data=True) + if default is not None: + return {x[:-1]: x[-1].get(name, default) for x in edges} + return {x[:-1]: x[-1][name] for x in edges if name in x[-1]} + + +@nx._dispatchable(preserve_edge_attrs=True, mutates_input=True) +def remove_edge_attributes(G, *attr_names, ebunch=None): + """Remove edge attributes from all edges in the graph. + + Parameters + ---------- + G : NetworkX Graph + + *attr_names : List of Strings + The attribute names to remove from the graph. + + Examples + -------- + >>> G = nx.path_graph(3) + >>> nx.set_edge_attributes(G, {(u, v): u + v for u, v in G.edges()}, name="weight") + >>> nx.get_edge_attributes(G, "weight") + {(0, 1): 1, (1, 2): 3} + >>> remove_edge_attributes(G, "weight") + >>> nx.get_edge_attributes(G, "weight") + {} + """ + if ebunch is None: + ebunch = G.edges(keys=True) if G.is_multigraph() else G.edges() + + for attr in attr_names: + edges = ( + G.edges(keys=True, data=True) if G.is_multigraph() else G.edges(data=True) + ) + for *e, d in edges: + if tuple(e) in ebunch: + try: + del d[attr] + except KeyError: + pass + + +def all_neighbors(graph, node): + """Returns all of the neighbors of a node in the graph. + + If the graph is directed returns predecessors as well as successors. + + Parameters + ---------- + graph : NetworkX graph + Graph to find neighbors. + node : node + The node whose neighbors will be returned. + + Returns + ------- + neighbors : iterator + Iterator of neighbors + + Raises + ------ + NetworkXError + If `node` is not in the graph. + + Examples + -------- + For undirected graphs, this function is equivalent to ``G.neighbors(node)``. + + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> list(nx.all_neighbors(G, 1)) + [0, 2] + + For directed graphs, this function returns both predecessors and successors, + which may include duplicates if a node is both a predecessor and successor + (e.g., in bidirectional edges or self-loops). + + >>> DG = nx.DiGraph([(0, 1), (1, 2), (2, 1)]) + >>> list(nx.all_neighbors(DG, 1)) + [0, 2, 2] + + Notes + ----- + This function iterates over all neighbors (both predecessors and successors). + + See Also + -------- + Graph.neighbors : Returns successors for both Graph and DiGraph + DiGraph.predecessors : Returns predecessors for directed graphs only + DiGraph.successors : Returns successors for directed graphs only + """ + if graph.is_directed(): + values = chain(graph.predecessors(node), graph.successors(node)) + else: + values = graph.neighbors(node) + return values + + +def non_neighbors(graph, node): + """Returns the non-neighbors of the node in the graph. + + Parameters + ---------- + graph : NetworkX graph + Graph to find neighbors. + + node : node + The node whose neighbors will be returned. + + Returns + ------- + non_neighbors : set + Set of nodes in the graph that are not neighbors of the node. + """ + return graph._adj.keys() - graph._adj[node].keys() - {node} + + +def non_edges(graph): + """Returns the nonexistent edges in the graph. + + Parameters + ---------- + graph : NetworkX graph. + Graph to find nonexistent edges. + + Returns + ------- + non_edges : iterator + Iterator of edges that are not in the graph. + """ + if graph.is_directed(): + for u in graph: + for v in non_neighbors(graph, u): + yield (u, v) + else: + nodes = set(graph) + while nodes: + u = nodes.pop() + for v in nodes - set(graph[u]): + yield (u, v) + + +@not_implemented_for("directed") +def common_neighbors(G, u, v): + """Returns the common neighbors of two nodes in a graph. + + Parameters + ---------- + G : graph + A NetworkX undirected graph. + + u, v : nodes + Nodes in the graph. + + Returns + ------- + cnbors : set + Set of common neighbors of u and v in the graph. + + Raises + ------ + NetworkXError + If u or v is not a node in the graph. + + Examples + -------- + >>> G = nx.complete_graph(5) + >>> sorted(nx.common_neighbors(G, 0, 1)) + [2, 3, 4] + """ + if u not in G: + raise nx.NetworkXError("u is not in the graph.") + if v not in G: + raise nx.NetworkXError("v is not in the graph.") + + return G._adj[u].keys() & G._adj[v].keys() - {u, v} + + +@nx._dispatchable(preserve_edge_attrs=True) +def is_weighted(G, edge=None, weight="weight"): + """Returns True if `G` has weighted edges. + + Parameters + ---------- + G : graph + A NetworkX graph. + + edge : tuple, optional + A 2-tuple specifying the only edge in `G` that will be tested. If + None, then every edge in `G` is tested. + + weight: string, optional + The attribute name used to query for edge weights. + + Returns + ------- + bool + A boolean signifying if `G`, or the specified edge, is weighted. + + Raises + ------ + NetworkXError + If the specified edge does not exist. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.is_weighted(G) + False + >>> nx.is_weighted(G, (2, 3)) + False + + >>> G = nx.DiGraph() + >>> G.add_edge(1, 2, weight=1) + >>> nx.is_weighted(G) + True + + """ + if edge is not None: + data = G.get_edge_data(*edge) + if data is None: + msg = f"Edge {edge!r} does not exist." + raise nx.NetworkXError(msg) + return weight in data + + if is_empty(G): + # Special handling required since: all([]) == True + return False + + return all(weight in data for u, v, data in G.edges(data=True)) + + +@nx._dispatchable(edge_attrs="weight") +def is_negatively_weighted(G, edge=None, weight="weight"): + """Returns True if `G` has negatively weighted edges. + + Parameters + ---------- + G : graph + A NetworkX graph. + + edge : tuple, optional + A 2-tuple specifying the only edge in `G` that will be tested. If + None, then every edge in `G` is tested. + + weight: string, optional + The attribute name used to query for edge weights. + + Returns + ------- + bool + A boolean signifying if `G`, or the specified edge, is negatively + weighted. + + Raises + ------ + NetworkXError + If the specified edge does not exist. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edges_from([(1, 3), (2, 4), (2, 6)]) + >>> G.add_edge(1, 2, weight=4) + >>> nx.is_negatively_weighted(G, (1, 2)) + False + >>> G[2][4]["weight"] = -2 + >>> nx.is_negatively_weighted(G) + True + >>> G = nx.DiGraph() + >>> edges = [("0", "3", 3), ("0", "1", -5), ("1", "0", -2)] + >>> G.add_weighted_edges_from(edges) + >>> nx.is_negatively_weighted(G) + True + + """ + if edge is not None: + data = G.get_edge_data(*edge) + if data is None: + msg = f"Edge {edge!r} does not exist." + raise nx.NetworkXError(msg) + return weight in data and data[weight] < 0 + + return any(weight in data and data[weight] < 0 for u, v, data in G.edges(data=True)) + + +@nx._dispatchable +def is_empty(G): + """Returns True if `G` has no edges. + + Parameters + ---------- + G : graph + A NetworkX graph. + + Returns + ------- + bool + True if `G` has no edges, and False otherwise. + + Notes + ----- + An empty graph can have nodes but not edges. The empty graph with zero + nodes is known as the null graph. This is an $O(n)$ operation where n + is the number of nodes in the graph. + + """ + return not any(G._adj.values()) + + +def nodes_with_selfloops(G): + """Returns an iterator over nodes with self loops. + + A node with a self loop has an edge with both ends adjacent + to that node. + + Returns + ------- + nodelist : iterator + A iterator over nodes with self loops. + + See Also + -------- + selfloop_edges, number_of_selfloops + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edge(1, 1) + >>> G.add_edge(1, 2) + >>> list(nx.nodes_with_selfloops(G)) + [1] + + """ + return (n for n, nbrs in G._adj.items() if n in nbrs) + + +def selfloop_edges(G, data=False, keys=False, default=None): + """Returns an iterator over selfloop edges. + + A selfloop edge has the same node at both ends. + + Parameters + ---------- + G : graph + A NetworkX graph. + data : string or bool, optional (default=False) + Return selfloop edges as two tuples (u, v) (data=False) + or three-tuples (u, v, datadict) (data=True) + or three-tuples (u, v, datavalue) (data='attrname') + keys : bool, optional (default=False) + If True, return edge keys with each edge. + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + edgeiter : iterator over edge tuples + An iterator over all selfloop edges. + + See Also + -------- + nodes_with_selfloops, number_of_selfloops + + Examples + -------- + >>> G = nx.MultiGraph() # or Graph, DiGraph, MultiDiGraph, etc + >>> ekey = G.add_edge(1, 1) + >>> ekey = G.add_edge(1, 2) + >>> list(nx.selfloop_edges(G)) + [(1, 1)] + >>> list(nx.selfloop_edges(G, data=True)) + [(1, 1, {})] + >>> list(nx.selfloop_edges(G, keys=True)) + [(1, 1, 0)] + >>> list(nx.selfloop_edges(G, keys=True, data=True)) + [(1, 1, 0, {})] + """ + if data is True: + if G.is_multigraph(): + if keys is True: + return ( + (n, n, k, d) + for n, nbrs in G._adj.items() + if n in nbrs + for k, d in nbrs[n].items() + ) + else: + return ( + (n, n, d) + for n, nbrs in G._adj.items() + if n in nbrs + for d in nbrs[n].values() + ) + else: + return ((n, n, nbrs[n]) for n, nbrs in G._adj.items() if n in nbrs) + elif data is not False: + if G.is_multigraph(): + if keys is True: + return ( + (n, n, k, d.get(data, default)) + for n, nbrs in G._adj.items() + if n in nbrs + for k, d in nbrs[n].items() + ) + else: + return ( + (n, n, d.get(data, default)) + for n, nbrs in G._adj.items() + if n in nbrs + for d in nbrs[n].values() + ) + else: + return ( + (n, n, nbrs[n].get(data, default)) + for n, nbrs in G._adj.items() + if n in nbrs + ) + else: + if G.is_multigraph(): + if keys is True: + return ( + (n, n, k) + for n, nbrs in G._adj.items() + if n in nbrs + for k in nbrs[n] + ) + else: + return ( + (n, n) + for n, nbrs in G._adj.items() + if n in nbrs + for i in range(len(nbrs[n])) # for easy edge removal (#4068) + ) + else: + return ((n, n) for n, nbrs in G._adj.items() if n in nbrs) + + +@nx._dispatchable +def number_of_selfloops(G): + """Returns the number of selfloop edges. + + A selfloop edge has the same node at both ends. + + Returns + ------- + nloops : int + The number of selfloops. + + See Also + -------- + nodes_with_selfloops, selfloop_edges + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edge(1, 1) + >>> G.add_edge(1, 2) + >>> nx.number_of_selfloops(G) + 1 + """ + return sum(1 for _ in nx.selfloop_edges(G)) + + +def is_path(G, path): + """Returns whether or not the specified path exists. + + For it to return True, every node on the path must exist and + each consecutive pair must be connected via one or more edges. + + Parameters + ---------- + G : graph + A NetworkX graph. + + path : list + A list of nodes which defines the path to traverse + + Returns + ------- + bool + True if `path` is a valid path in `G` + + """ + try: + return all(nbr in G._adj[node] for node, nbr in nx.utils.pairwise(path)) + except (KeyError, TypeError): + return False + + +def path_weight(G, path, weight): + """Returns total cost associated with specified path and weight + + Parameters + ---------- + G : graph + A NetworkX graph. + + path: list + A list of node labels which defines the path to traverse + + weight: string + A string indicating which edge attribute to use for path cost + + Returns + ------- + cost: int or float + An integer or a float representing the total cost with respect to the + specified weight of the specified path + + Raises + ------ + NetworkXNoPath + If the specified edge does not exist. + """ + multigraph = G.is_multigraph() + cost = 0 + + if not nx.is_path(G, path): + raise nx.NetworkXNoPath("path does not exist") + for node, nbr in nx.utils.pairwise(path): + if multigraph: + cost += min(v[weight] for v in G._adj[node][nbr].values()) + else: + cost += G._adj[node][nbr][weight] + return cost + + +def describe(G, describe_hook=None): + """Prints a description of the graph G. + + By default, the description includes some basic properties of the graph. + You can also provide additional functions to compute and include + more properties in the description. + + Parameters + ---------- + G : graph + A NetworkX graph. + + describe_hook: callable, optional (default=None) + A function that takes a graph as input and returns a + dictionary of additional properties to include in the description. + The keys of the dictionary are the property names, and the values + are the corresponding property values. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.describe(G) + Number of nodes : 5 + Number of edges : 4 + Directed : False + Multigraph : False + Tree : True + Bipartite : True + Average degree (min, max) : 1.60 (1, 2) + Number of connected components : 1 + + >>> def augment_description(G): + ... return {"Average Shortest Path Length": nx.average_shortest_path_length(G)} + >>> nx.describe(G, describe_hook=augment_description) + Number of nodes : 5 + Number of edges : 4 + Directed : False + Multigraph : False + Tree : True + Bipartite : True + Average degree (min, max) : 1.60 (1, 2) + Number of connected components : 1 + Average Shortest Path Length : 2.0 + + >>> G.name = "Path Graph of 5 nodes" + >>> nx.describe(G) + Name of Graph : Path Graph of 5 nodes + Number of nodes : 5 + Number of edges : 4 + Directed : False + Multigraph : False + Tree : True + Bipartite : True + Average degree (min, max) : 1.60 (1, 2) + Number of connected components : 1 + + """ + info_dict = _create_describe_info_dict(G) + + if describe_hook is not None: + additional_info = describe_hook(G) + info_dict.update(additional_info) + + max_key_len = max(len(k) for k in info_dict) + for key, val in info_dict.items(): + print(f"{key:<{max_key_len}} : {val}") + + +def _create_describe_info_dict(G): + info = {} + if G.name != "": + info["Name of Graph"] = G.name + info.update( + { + "Number of nodes": len(G), + "Number of edges": G.number_of_edges(), + "Directed": G.is_directed(), + "Multigraph": G.is_multigraph(), + "Tree": nx.is_tree(G), + "Bipartite": nx.is_bipartite(G), + } + ) + if len(G) == 0: + return info + + degree_values = dict(nx.degree(G)).values() + avg_degree = sum(degree_values) / len(G) + max_degree, min_degree = max(degree_values), min(degree_values) + info["Average degree (min, max)"] = f"{avg_degree:.2f} ({min_degree}, {max_degree})" + + if G.is_directed(): + info["Number of strongly connected components"] = ( + nx.number_strongly_connected_components(G) + ) + info["Number of weakly connected components"] = ( + nx.number_weakly_connected_components(G) + ) + else: + info["Number of connected components"] = nx.number_connected_components(G) + return info diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..0eb184f4c5bfdd2a49890a480c2be08b4c0190d4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graph.py @@ -0,0 +1,2082 @@ +"""Base class for undirected graphs. + +The Graph class allows any hashable object as a node +and can associate key/value attribute pairs with each undirected edge. + +Self-loops are allowed but multiple edges are not (see MultiGraph). + +For directed graphs see DiGraph and MultiDiGraph. +""" + +from copy import deepcopy +from functools import cached_property + +import networkx as nx +from networkx import convert +from networkx.classes.coreviews import AdjacencyView +from networkx.classes.reportviews import DegreeView, EdgeView, NodeView +from networkx.exception import NetworkXError + +__all__ = ["Graph"] + + +class _CachedPropertyResetterAdj: + """Data Descriptor class for _adj that resets ``adj`` cached_property when needed + + This assumes that the ``cached_property`` ``G.adj`` should be reset whenever + ``G._adj`` is set to a new value. + + This object sits on a class and ensures that any instance of that + class clears its cached property "adj" whenever the underlying + instance attribute "_adj" is set to a new object. It only affects + the set process of the obj._adj attribute. All get/del operations + act as they normally would. + + For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html + """ + + def __set__(self, obj, value): + od = obj.__dict__ + od["_adj"] = value + # reset cached properties + props = ["adj", "edges", "degree"] + for prop in props: + if prop in od: + del od[prop] + + +class _CachedPropertyResetterNode: + """Data Descriptor class for _node that resets ``nodes`` cached_property when needed + + This assumes that the ``cached_property`` ``G.node`` should be reset whenever + ``G._node`` is set to a new value. + + This object sits on a class and ensures that any instance of that + class clears its cached property "nodes" whenever the underlying + instance attribute "_node" is set to a new object. It only affects + the set process of the obj._adj attribute. All get/del operations + act as they normally would. + + For info on Data Descriptors see: https://docs.python.org/3/howto/descriptor.html + """ + + def __set__(self, obj, value): + od = obj.__dict__ + od["_node"] = value + # reset cached properties + if "nodes" in od: + del od["nodes"] + + +class Graph: + """ + Base class for undirected graphs. + + A Graph stores nodes and edges with optional data, or attributes. + + Graphs hold undirected edges. Self loops are allowed but multiple + (parallel) edges are not. + + Nodes can be arbitrary (hashable) Python objects with optional + key/value attributes, except that `None` is not allowed as a node. + + Edges are represented as links between nodes with optional + key/value attributes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be any format that is supported + by the to_networkx_graph() function, currently including edge list, + dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy + sparse matrix, or PyGraphviz graph. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + DiGraph + MultiGraph + MultiDiGraph + + Examples + -------- + Create an empty graph structure (a "null graph") with no nodes and + no edges. + + >>> G = nx.Graph() + + G can be grown in several ways. + + **Nodes:** + + Add one node at a time: + + >>> G.add_node(1) + + Add the nodes from any container (a list, dict, set or + even the lines from a file or the nodes from another graph). + + >>> G.add_nodes_from([2, 3]) + >>> G.add_nodes_from(range(100, 110)) + >>> H = nx.path_graph(10) + >>> G.add_nodes_from(H) + + In addition to strings and integers any hashable Python object + (except None) can represent a node, e.g. a customized node object, + or even another Graph. + + >>> G.add_node(H) + + **Edges:** + + G can also be grown by adding edges. + + Add one edge, + + >>> G.add_edge(1, 2) + + a list of edges, + + >>> G.add_edges_from([(1, 2), (1, 3)]) + + or a collection of edges, + + >>> G.add_edges_from(H.edges) + + If some edges connect nodes not yet in the graph, the nodes + are added automatically. There are no errors when adding + nodes or edges that already exist. + + **Attributes:** + + Each graph, node, and edge can hold key/value attribute pairs + in an associated attribute dictionary (the keys must be hashable). + By default these are empty, but can be added or changed using + add_edge, add_node or direct manipulation of the attribute + dictionaries named graph, node and edge respectively. + + >>> G = nx.Graph(day="Friday") + >>> G.graph + {'day': 'Friday'} + + Add node attributes using add_node(), add_nodes_from() or G.nodes + + >>> G.add_node(1, time="5pm") + >>> G.add_nodes_from([3], time="2pm") + >>> G.nodes[1] + {'time': '5pm'} + >>> G.nodes[1]["room"] = 714 # node must exist already to use G.nodes + >>> del G.nodes[1]["room"] # remove attribute + >>> list(G.nodes(data=True)) + [(1, {'time': '5pm'}), (3, {'time': '2pm'})] + + Add edge attributes using add_edge(), add_edges_from(), subscript + notation, or G.edges. + + >>> G.add_edge(1, 2, weight=4.7) + >>> G.add_edges_from([(3, 4), (4, 5)], color="red") + >>> G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})]) + >>> G[1][2]["weight"] = 4.7 + >>> G.edges[1, 2]["weight"] = 4 + + Warning: we protect the graph data structure by making `G.edges` a + read-only dict-like structure. However, you can assign to attributes + in e.g. `G.edges[1, 2]`. Thus, use 2 sets of brackets to add/change + data attributes: `G.edges[1, 2]['weight'] = 4` + (For multigraphs: `MG.edges[u, v, key][name] = value`). + + **Shortcuts:** + + Many common graph features allow python syntax to speed reporting. + + >>> 1 in G # check if node in graph + True + >>> [n for n in G if n < 3] # iterate through nodes + [1, 2] + >>> len(G) # number of nodes in graph + 5 + + Often the best way to traverse all edges of a graph is via the neighbors. + The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()` + + >>> for n, nbrsdict in G.adjacency(): + ... for nbr, eattr in nbrsdict.items(): + ... if "weight" in eattr: + ... # Do something useful with the edges + ... pass + + But the edges() method is often more convenient: + + >>> for u, v, weight in G.edges.data("weight"): + ... if weight is not None: + ... # Do something useful with the edges + ... pass + + **Reporting:** + + Simple graph information is obtained using object-attributes and methods. + Reporting typically provides views instead of containers to reduce memory + usage. The views update as the graph is updated similarly to dict-views. + The objects `nodes`, `edges` and `adj` provide access to data attributes + via lookup (e.g. `nodes[n]`, `edges[u, v]`, `adj[u][v]`) and iteration + (e.g. `nodes.items()`, `nodes.data('color')`, + `nodes.data('color', default='blue')` and similarly for `edges`) + Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`. + + For details on these and other miscellaneous methods, see below. + + **Subclasses (Advanced):** + + The Graph class uses a dict-of-dict-of-dict data structure. + The outer dict (node_dict) holds adjacency information keyed by node. + The next dict (adjlist_dict) represents the adjacency information and holds + edge data keyed by neighbor. The inner dict (edge_attr_dict) represents + the edge data and holds edge attribute values keyed by attribute names. + + Each of these three dicts can be replaced in a subclass by a user defined + dict-like object. In general, the dict-like features should be + maintained but extra features can be added. To replace one of the + dicts create a new graph class by changing the class(!) variable + holding the factory for that dict-like structure. + + node_dict_factory : function, (default: dict) + Factory function to be used to create the dict containing node + attributes, keyed by node id. + It should require no arguments and return a dict-like object + + node_attr_dict_factory: function, (default: dict) + Factory function to be used to create the node attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object + + adjlist_outer_dict_factory : function, (default: dict) + Factory function to be used to create the outer-most dict + in the data structure that holds adjacency info keyed by node. + It should require no arguments and return a dict-like object. + + adjlist_inner_dict_factory : function, (default: dict) + Factory function to be used to create the adjacency list + dict which holds edge data keyed by neighbor. + It should require no arguments and return a dict-like object + + edge_attr_dict_factory : function, (default: dict) + Factory function to be used to create the edge attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + graph_attr_dict_factory : function, (default: dict) + Factory function to be used to create the graph attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + Typically, if your extension doesn't impact the data structure all + methods will inherit without issue except: `to_directed/to_undirected`. + By default these methods create a DiGraph/Graph class and you probably + want them to create your extension of a DiGraph/Graph. To facilitate + this we define two class variables that you can set in your subclass. + + to_directed_class : callable, (default: DiGraph or MultiDiGraph) + Class to create a new graph structure in the `to_directed` method. + If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used. + + to_undirected_class : callable, (default: Graph or MultiGraph) + Class to create a new graph structure in the `to_undirected` method. + If `None`, a NetworkX class (Graph or MultiGraph) is used. + + **Subclassing Example** + + Create a low memory graph class that effectively disallows edge + attributes by using a single attribute dict for all edges. + This reduces the memory used, but you lose edge attributes. + + >>> class ThinGraph(nx.Graph): + ... all_edge_dict = {"weight": 1} + ... + ... def single_edge_dict(self): + ... return self.all_edge_dict + ... + ... edge_attr_dict_factory = single_edge_dict + >>> G = ThinGraph() + >>> G.add_edge(2, 1) + >>> G[2][1] + {'weight': 1} + >>> G.add_edge(2, 2) + >>> G[2][1] is G[2][2] + True + """ + + __networkx_backend__ = "networkx" + + _adj = _CachedPropertyResetterAdj() + _node = _CachedPropertyResetterNode() + + node_dict_factory = dict + node_attr_dict_factory = dict + adjlist_outer_dict_factory = dict + adjlist_inner_dict_factory = dict + edge_attr_dict_factory = dict + graph_attr_dict_factory = dict + + def to_directed_class(self): + """Returns the class to use for empty directed copies. + + If you subclass the base classes, use this to designate + what directed class to use for `to_directed()` copies. + """ + return nx.DiGraph + + def to_undirected_class(self): + """Returns the class to use for empty undirected copies. + + If you subclass the base classes, use this to designate + what directed class to use for `to_directed()` copies. + """ + return Graph + + # This __new__ method just does what Python itself does automatically. + # We include it here as part of the dispatchable/backend interface. + # If your goal is to understand how the graph classes work, you can ignore + # this method, even when subclassing the base classes. If you are subclassing + # in order to provide a backend that allows class instantiation, this method + # can be overridden to return your own backend graph class. + @nx._dispatchable(name="graph__new__", graphs=None, returns_graph=True) + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, incoming_graph_data=None, **attr): + """Initialize a graph with edges, name, or graph attributes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be an edge list, or any + NetworkX graph object. If the corresponding optional Python + packages are installed the data can also be a 2D NumPy array, a + SciPy sparse array, or a PyGraphviz graph. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + convert + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G = nx.Graph(name="my graph") + >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges + >>> G = nx.Graph(e) + + Arbitrary graph attribute pairs (key=value) may be assigned + + >>> G = nx.Graph(e, day="Friday") + >>> G.graph + {'day': 'Friday'} + + """ + self.graph = self.graph_attr_dict_factory() # dictionary for graph attributes + self._node = self.node_dict_factory() # empty node attribute dict + self._adj = self.adjlist_outer_dict_factory() # empty adjacency dict + self.__networkx_cache__ = {} + # attempt to load graph with data + if incoming_graph_data is not None: + convert.to_networkx_graph(incoming_graph_data, create_using=self) + # load graph attributes (must be after convert) + attr.pop("backend", None) # Ignore explicit `backend="networkx"` + self.graph.update(attr) + + @cached_property + def adj(self): + """Graph adjacency object holding the neighbors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edge-data-dict. So `G.adj[3][2]['color'] = 'blue'` sets + the color of the edge `(3, 2)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, datadict in G.adj[n].items():`. + + The neighbor information is also provided by subscripting the graph. + So `for nbr, foovalue in G[node].data('foo', default=1):` works. + + For directed graphs, `G.adj` holds outgoing (successor) info. + """ + return AdjacencyView(self._adj) + + @property + def name(self): + """String identifier of the graph. + + This graph attribute appears in the attribute dict G.graph + keyed by the string `"name"`. as well as an attribute (technically + a property) `G.name`. This is entirely user controlled. + """ + return self.graph.get("name", "") + + @name.setter + def name(self, s): + self.graph["name"] = s + nx._clear_cache(self) + + def __str__(self): + """Returns a short summary of the graph. + + Returns + ------- + info : string + Graph information including the graph name (if any), graph type, and the + number of nodes and edges. + + Examples + -------- + >>> G = nx.Graph(name="foo") + >>> str(G) + "Graph named 'foo' with 0 nodes and 0 edges" + + >>> G = nx.path_graph(3) + >>> str(G) + 'Graph with 3 nodes and 2 edges' + + """ + return "".join( + [ + type(self).__name__, + f" named {self.name!r}" if self.name else "", + f" with {self.number_of_nodes()} nodes and {self.number_of_edges()} edges", + ] + ) + + def __iter__(self): + """Iterate over the nodes. Use: 'for n in G'. + + Returns + ------- + niter : iterator + An iterator over all nodes in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> [n for n in G] + [0, 1, 2, 3] + >>> list(G) + [0, 1, 2, 3] + """ + return iter(self._node) + + def __contains__(self, n): + """Returns True if n is a node, False otherwise. Use: 'n in G'. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> 1 in G + True + """ + try: + return n in self._node + except TypeError: + return False + + def __len__(self): + """Returns the number of nodes in the graph. Use: 'len(G)'. + + Returns + ------- + nnodes : int + The number of nodes in the graph. + + See Also + -------- + number_of_nodes: identical method + order: identical method + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> len(G) + 4 + + """ + return len(self._node) + + def __getitem__(self, n): + """Returns a dict of neighbors of node n. Use: 'G[n]'. + + Parameters + ---------- + n : node + A node in the graph. + + Returns + ------- + adj_dict : dictionary + The adjacency dictionary for nodes connected to n. + + Notes + ----- + G[n] is the same as G.adj[n] and similar to G.neighbors(n) + (which is an iterator over G.adj[n]) + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G[0] + AtlasView({1: {}}) + """ + return self.adj[n] + + def add_node(self, node_for_adding, **attr): + """Add a single node `node_for_adding` and update node attributes. + + Parameters + ---------- + node_for_adding : node + A node can be any hashable Python object except None. + attr : keyword arguments, optional + Set or change node attributes using key=value. + + See Also + -------- + add_nodes_from + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_node(1) + >>> G.add_node("Hello") + >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)]) + >>> G.add_node(K3) + >>> G.number_of_nodes() + 3 + + Use keywords set/change node attributes: + + >>> G.add_node(1, size=10) + >>> G.add_node(3, weight=0.4, UTM=("13S", 382871, 3972649)) + + Notes + ----- + A hashable object is one that can be used as a key in a Python + dictionary. This includes strings, numbers, tuples of strings + and numbers, etc. + + On many platforms hashable items also include mutables such as + NetworkX Graphs, though one should be careful that the hash + doesn't change on mutables. + """ + if node_for_adding not in self._node: + if node_for_adding is None: + raise ValueError("None cannot be a node") + self._adj[node_for_adding] = self.adjlist_inner_dict_factory() + attr_dict = self._node[node_for_adding] = self.node_attr_dict_factory() + attr_dict.update(attr) + else: # update attr even if node already exists + self._node[node_for_adding].update(attr) + nx._clear_cache(self) + + def add_nodes_from(self, nodes_for_adding, **attr): + """Add multiple nodes. + + Parameters + ---------- + nodes_for_adding : iterable container + A container of nodes (list, dict, set, etc.). + OR + A container of (node, attribute dict) tuples. + Node attributes are updated using the attribute dict. + attr : keyword arguments, optional (default= no attributes) + Update attributes for all nodes in nodes. + Node attributes specified in nodes as a tuple take + precedence over attributes specified via keyword arguments. + + See Also + -------- + add_node + + Notes + ----- + When adding nodes from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_nodes)`, and pass this + object to `G.add_nodes_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_nodes_from("Hello") + >>> K3 = nx.Graph([(0, 1), (1, 2), (2, 0)]) + >>> G.add_nodes_from(K3) + >>> sorted(G.nodes(), key=str) + [0, 1, 2, 'H', 'e', 'l', 'o'] + + Use keywords to update specific node attributes for every node. + + >>> G.add_nodes_from([1, 2], size=10) + >>> G.add_nodes_from([3, 4], weight=0.4) + + Use (node, attrdict) tuples to update attributes for specific nodes. + + >>> G.add_nodes_from([(1, dict(size=11)), (2, {"color": "blue"})]) + >>> G.nodes[1]["size"] + 11 + >>> H = nx.Graph() + >>> H.add_nodes_from(G.nodes(data=True)) + >>> H.nodes[1]["size"] + 11 + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.Graph([(0, 1), (1, 2), (3, 4)]) + >>> # wrong way - will raise RuntimeError + >>> # G.add_nodes_from(n + 1 for n in G.nodes) + >>> # correct way + >>> G.add_nodes_from(list(n + 1 for n in G.nodes)) + """ + for n in nodes_for_adding: + try: + newnode = n not in self._node + newdict = attr + except TypeError: + n, ndict = n + newnode = n not in self._node + newdict = attr.copy() + newdict.update(ndict) + if newnode: + if n is None: + raise ValueError("None cannot be a node") + self._adj[n] = self.adjlist_inner_dict_factory() + self._node[n] = self.node_attr_dict_factory() + self._node[n].update(newdict) + nx._clear_cache(self) + + def remove_node(self, n): + """Remove node n. + + Removes the node n and all adjacent edges. + Attempting to remove a nonexistent node will raise an exception. + + Parameters + ---------- + n : node + A node in the graph + + Raises + ------ + NetworkXError + If n is not in the graph. + + See Also + -------- + remove_nodes_from + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> list(G.edges) + [(0, 1), (1, 2)] + >>> G.remove_node(1) + >>> list(G.edges) + [] + + """ + adj = self._adj + try: + nbrs = list(adj[n]) # list handles self-loops (allows mutation) + del self._node[n] + except KeyError as err: # NetworkXError if n not in self + raise NetworkXError(f"The node {n} is not in the graph.") from err + for u in nbrs: + del adj[u][n] # remove all edges n-u in graph + del adj[n] # now remove node + nx._clear_cache(self) + + def remove_nodes_from(self, nodes): + """Remove multiple nodes. + + Parameters + ---------- + nodes : iterable container + A container of nodes (list, dict, set, etc.). If a node + in the container is not in the graph it is silently + ignored. + + See Also + -------- + remove_node + + Notes + ----- + When removing nodes from an iterator over the graph you are changing, + a `RuntimeError` will be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_nodes)`, and pass this + object to `G.remove_nodes_from`. + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> e = list(G.nodes) + >>> e + [0, 1, 2] + >>> G.remove_nodes_from(e) + >>> list(G.nodes) + [] + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.Graph([(0, 1), (1, 2), (3, 4)]) + >>> # this command will fail, as the graph's dict is modified during iteration + >>> # G.remove_nodes_from(n for n in G.nodes if n < 2) + >>> # this command will work, since the dictionary underlying graph is not modified + >>> G.remove_nodes_from(list(n for n in G.nodes if n < 2)) + """ + adj = self._adj + for n in nodes: + try: + del self._node[n] + for u in list(adj[n]): # list handles self-loops + del adj[u][n] # (allows mutation of dict in loop) + del adj[n] + except KeyError: + pass + nx._clear_cache(self) + + @cached_property + def nodes(self): + """A NodeView of the Graph as G.nodes or G.nodes(). + + Can be used as `G.nodes` for data lookup and for set-like operations. + Can also be used as `G.nodes(data='color', default=None)` to return a + NodeDataView which reports specific node data but no set operations. + It presents a dict-like interface as well with `G.nodes.items()` + iterating over `(node, nodedata)` 2-tuples and `G.nodes[3]['foo']` + providing the value of the `foo` attribute for node `3`. In addition, + a view `G.nodes.data('foo')` provides a dict-like interface to the + `foo` attribute of each node. `G.nodes.data('foo', default=1)` + provides a default for nodes that do not have attribute `foo`. + + Parameters + ---------- + data : string or bool, optional (default=False) + The node attribute returned in 2-tuple (n, ddict[data]). + If True, return entire node attribute dict as (n, ddict). + If False, return just the nodes n. + + default : value, optional (default=None) + Value used for nodes that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + NodeView + Allows set-like operations over the nodes as well as node + attribute dict lookup and calling to get a NodeDataView. + A NodeDataView iterates over `(n, data)` and has no set operations. + A NodeView iterates over `n` and includes set operations. + + When called, if data is False, an iterator over nodes. + Otherwise an iterator of 2-tuples (node, attribute value) + where the attribute is specified in `data`. + If data is True then the attribute becomes the + entire data dictionary. + + Notes + ----- + If your node data is not needed, it is simpler and equivalent + to use the expression ``for n in G``, or ``list(G)``. + + Examples + -------- + There are two simple ways of getting a list of all nodes in the graph: + + >>> G = nx.path_graph(3) + >>> list(G.nodes) + [0, 1, 2] + >>> list(G) + [0, 1, 2] + + To get the node data along with the nodes: + + >>> G.add_node(1, time="5pm") + >>> G.nodes[0]["foo"] = "bar" + >>> list(G.nodes(data=True)) + [(0, {'foo': 'bar'}), (1, {'time': '5pm'}), (2, {})] + >>> list(G.nodes.data()) + [(0, {'foo': 'bar'}), (1, {'time': '5pm'}), (2, {})] + + >>> list(G.nodes(data="foo")) + [(0, 'bar'), (1, None), (2, None)] + >>> list(G.nodes.data("foo")) + [(0, 'bar'), (1, None), (2, None)] + + >>> list(G.nodes(data="time")) + [(0, None), (1, '5pm'), (2, None)] + >>> list(G.nodes.data("time")) + [(0, None), (1, '5pm'), (2, None)] + + >>> list(G.nodes(data="time", default="Not Available")) + [(0, 'Not Available'), (1, '5pm'), (2, 'Not Available')] + >>> list(G.nodes.data("time", default="Not Available")) + [(0, 'Not Available'), (1, '5pm'), (2, 'Not Available')] + + If some of your nodes have an attribute and the rest are assumed + to have a default attribute value you can create a dictionary + from node/attribute pairs using the `default` keyword argument + to guarantee the value is never None:: + + >>> G = nx.Graph() + >>> G.add_node(0) + >>> G.add_node(1, weight=2) + >>> G.add_node(2, weight=3) + >>> dict(G.nodes(data="weight", default=1)) + {0: 1, 1: 2, 2: 3} + + """ + return NodeView(self) + + def number_of_nodes(self): + """Returns the number of nodes in the graph. + + Returns + ------- + nnodes : int + The number of nodes in the graph. + + See Also + -------- + order: identical method + __len__: identical method + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.number_of_nodes() + 3 + """ + return len(self._node) + + def order(self): + """Returns the number of nodes in the graph. + + Returns + ------- + nnodes : int + The number of nodes in the graph. + + See Also + -------- + number_of_nodes: identical method + __len__: identical method + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.order() + 3 + """ + return len(self._node) + + def has_node(self, n): + """Returns True if the graph contains the node n. + + Identical to `n in G` + + Parameters + ---------- + n : node + + Examples + -------- + >>> G = nx.path_graph(3) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.has_node(0) + True + + It is more readable and simpler to use + + >>> 0 in G + True + + """ + try: + return n in self._node + except TypeError: + return False + + def add_edge(self, u_of_edge, v_of_edge, **attr): + """Add an edge between u and v. + + The nodes u and v will be automatically added if they are + not already in the graph. + + Edge attributes can be specified with keywords or by directly + accessing the edge's attribute dictionary. See examples below. + + Parameters + ---------- + u_of_edge, v_of_edge : nodes + Nodes can be, for example, strings or numbers. + Nodes must be hashable (and not None) Python objects. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + See Also + -------- + add_edges_from : add a collection of edges + + Notes + ----- + Adding an edge that already exists updates the edge data. + + Many NetworkX algorithms designed for weighted graphs use + an edge attribute (by default `weight`) to hold a numerical value. + + Examples + -------- + The following all add the edge e=(1, 2) to graph G: + + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> e = (1, 2) + >>> G.add_edge(1, 2) # explicit two-node form + >>> G.add_edge(*e) # single edge as tuple of two nodes + >>> G.add_edges_from([(1, 2)]) # add edges from iterable container + + Associate data to edges using keywords: + + >>> G.add_edge(1, 2, weight=3) + >>> G.add_edge(1, 3, weight=7, capacity=15, length=342.7) + + For non-string attribute keys, use subscript notation. + + >>> G.add_edge(1, 2) + >>> G[1][2].update({0: 5}) + >>> G.edges[1, 2].update({0: 5}) + """ + u, v = u_of_edge, v_of_edge + # add nodes + if u not in self._node: + if u is None: + raise ValueError("None cannot be a node") + self._adj[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._node: + if v is None: + raise ValueError("None cannot be a node") + self._adj[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + # add the edge + datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) + datadict.update(attr) + self._adj[u][v] = datadict + self._adj[v][u] = datadict + nx._clear_cache(self) + + def add_edges_from(self, ebunch_to_add, **attr): + """Add all the edges in ebunch_to_add. + + Parameters + ---------- + ebunch_to_add : container of edges + Each edge given in the container will be added to the + graph. The edges must be given as 2-tuples (u, v) or + 3-tuples (u, v, d) where d is a dictionary containing edge data. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + See Also + -------- + add_edge : add a single edge + add_weighted_edges_from : convenient way to add weighted edges + + Notes + ----- + Adding the same edge twice has no effect but any edge data + will be updated when each duplicate edge is added. + + Edge attributes specified in an ebunch take precedence over + attributes specified via keyword arguments. + + When adding edges from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_edges)`, and pass this + object to `G.add_edges_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples + >>> e = zip(range(0, 3), range(1, 4)) + >>> G.add_edges_from(e) # Add the path graph 0-1-2-3 + + Associate data to edges + + >>> G.add_edges_from([(1, 2), (2, 3)], weight=3) + >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898") + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + >>> # Grow graph by one new node, adding edges to all existing nodes. + >>> # wrong way - will raise RuntimeError + >>> # G.add_edges_from(((5, n) for n in G.nodes)) + >>> # correct way - note that there will be no self-edge for node 5 + >>> G.add_edges_from(list((5, n) for n in G.nodes)) + """ + for e in ebunch_to_add: + ne = len(e) + if ne == 3: + u, v, dd = e + elif ne == 2: + u, v = e + dd = {} # doesn't need edge_attr_dict_factory + else: + raise NetworkXError(f"Edge tuple {e} must be a 2-tuple or 3-tuple.") + if u not in self._node: + if u is None: + raise ValueError("None cannot be a node") + self._adj[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._node: + if v is None: + raise ValueError("None cannot be a node") + self._adj[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + datadict = self._adj[u].get(v, self.edge_attr_dict_factory()) + datadict.update(attr) + datadict.update(dd) + self._adj[u][v] = datadict + self._adj[v][u] = datadict + nx._clear_cache(self) + + def add_weighted_edges_from(self, ebunch_to_add, weight="weight", **attr): + """Add weighted edges in `ebunch_to_add` with specified weight attr + + Parameters + ---------- + ebunch_to_add : container of edges + Each edge given in the list or container will be added + to the graph. The edges must be given as 3-tuples (u, v, w) + where w is a number. + weight : string, optional (default= 'weight') + The attribute name for the edge weights to be added. + attr : keyword arguments, optional (default= no attributes) + Edge attributes to add/update for all edges. + + See Also + -------- + add_edge : add a single edge + add_edges_from : add multiple edges + + Notes + ----- + Adding the same edge twice for Graph/DiGraph simply updates + the edge data. For MultiGraph/MultiDiGraph, duplicate edges + are stored. + + When adding edges from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_edges)`, and pass this + object to `G.add_weighted_edges_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_weighted_edges_from([(0, 1, 3.0), (1, 2, 7.5)]) + + Evaluate an iterator over edges before passing it + + >>> G = nx.Graph([(1, 2), (2, 3), (3, 4)]) + >>> weight = 0.1 + >>> # Grow graph by one new node, adding edges to all existing nodes. + >>> # wrong way - will raise RuntimeError + >>> # G.add_weighted_edges_from(((5, n, weight) for n in G.nodes)) + >>> # correct way - note that there will be no self-edge for node 5 + >>> G.add_weighted_edges_from(list((5, n, weight) for n in G.nodes)) + """ + self.add_edges_from(((u, v, {weight: d}) for u, v, d in ebunch_to_add), **attr) + nx._clear_cache(self) + + def remove_edge(self, u, v): + """Remove the edge between u and v. + + Parameters + ---------- + u, v : nodes + Remove the edge between nodes u and v. + + Raises + ------ + NetworkXError + If there is not an edge between u and v. + + See Also + -------- + remove_edges_from : remove a collection of edges + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, etc + >>> G.remove_edge(0, 1) + >>> e = (1, 2) + >>> G.remove_edge(*e) # unpacks e from an edge tuple + >>> e = (2, 3, {"weight": 7}) # an edge with attribute data + >>> G.remove_edge(*e[:2]) # select first part of edge tuple + """ + try: + del self._adj[u][v] + if u != v: # self-loop needs only one entry removed + del self._adj[v][u] + except KeyError as err: + raise NetworkXError(f"The edge {u}-{v} is not in the graph") from err + nx._clear_cache(self) + + def remove_edges_from(self, ebunch): + """Remove all edges specified in ebunch. + + Parameters + ---------- + ebunch: list or container of edge tuples + Each edge given in the list or container will be removed + from the graph. The edges can be: + + - 2-tuples (u, v) edge between u and v. + - 3-tuples (u, v, k) where k is ignored. + + See Also + -------- + remove_edge : remove a single edge + + Notes + ----- + Will fail silently if an edge in ebunch is not in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> ebunch = [(1, 2), (2, 3)] + >>> G.remove_edges_from(ebunch) + """ + adj = self._adj + for e in ebunch: + u, v = e[:2] # ignore edge data if present + if u in adj and v in adj[u]: + del adj[u][v] + if u != v: # self loop needs only one entry removed + del adj[v][u] + nx._clear_cache(self) + + def update(self, edges=None, nodes=None): + """Update the graph using nodes/edges/graphs as input. + + Like dict.update, this method takes a graph as input, adding the + graph's nodes and edges to this graph. It can also take two inputs: + edges and nodes. Finally it can take either edges or nodes. + To specify only nodes the keyword `nodes` must be used. + + The collections of edges and nodes are treated similarly to + the add_edges_from/add_nodes_from methods. When iterated, they + should yield 2-tuples (u, v) or 3-tuples (u, v, datadict). + + Parameters + ---------- + edges : Graph object, collection of edges, or None + The first parameter can be a graph or some edges. If it has + attributes `nodes` and `edges`, then it is taken to be a + Graph-like object and those attributes are used as collections + of nodes and edges to be added to the graph. + If the first parameter does not have those attributes, it is + treated as a collection of edges and added to the graph. + If the first argument is None, no edges are added. + nodes : collection of nodes, or None + The second parameter is treated as a collection of nodes + to be added to the graph unless it is None. + If `edges is None` and `nodes is None` an exception is raised. + If the first parameter is a Graph, then `nodes` is ignored. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> G.update(nx.complete_graph(range(4, 10))) + >>> from itertools import combinations + >>> edges = ( + ... (u, v, {"power": u * v}) + ... for u, v in combinations(range(10, 20), 2) + ... if u * v < 225 + ... ) + >>> nodes = [1000] # for singleton, use a container + >>> G.update(edges, nodes) + + Notes + ----- + It you want to update the graph using an adjacency structure + it is straightforward to obtain the edges/nodes from adjacency. + The following examples provide common cases, your adjacency may + be slightly different and require tweaks of these examples:: + + >>> # dict-of-set/list/tuple + >>> adj = {1: {2, 3}, 2: {1, 3}, 3: {1, 2}} + >>> e = [(u, v) for u, nbrs in adj.items() for v in nbrs] + >>> G.update(edges=e, nodes=adj) + + >>> DG = nx.DiGraph() + >>> # dict-of-dict-of-attribute + >>> adj = {1: {2: 1.3, 3: 0.7}, 2: {1: 1.4}, 3: {1: 0.7}} + >>> e = [ + ... (u, v, {"weight": d}) + ... for u, nbrs in adj.items() + ... for v, d in nbrs.items() + ... ] + >>> DG.update(edges=e, nodes=adj) + + >>> # dict-of-dict-of-dict + >>> adj = {1: {2: {"weight": 1.3}, 3: {"color": 0.7, "weight": 1.2}}} + >>> e = [ + ... (u, v, {"weight": d}) + ... for u, nbrs in adj.items() + ... for v, d in nbrs.items() + ... ] + >>> DG.update(edges=e, nodes=adj) + + >>> # predecessor adjacency (dict-of-set) + >>> pred = {1: {2, 3}, 2: {3}, 3: {3}} + >>> e = [(v, u) for u, nbrs in pred.items() for v in nbrs] + + >>> # MultiGraph dict-of-dict-of-dict-of-attribute + >>> MDG = nx.MultiDiGraph() + >>> adj = { + ... 1: {2: {0: {"weight": 1.3}, 1: {"weight": 1.2}}}, + ... 3: {2: {0: {"weight": 0.7}}}, + ... } + >>> e = [ + ... (u, v, ekey, d) + ... for u, nbrs in adj.items() + ... for v, keydict in nbrs.items() + ... for ekey, d in keydict.items() + ... ] + >>> MDG.update(edges=e) + + See Also + -------- + add_edges_from: add multiple edges to a graph + add_nodes_from: add multiple nodes to a graph + """ + if edges is not None: + if nodes is not None: + self.add_nodes_from(nodes) + self.add_edges_from(edges) + else: + # check if edges is a Graph object + try: + graph_nodes = edges.nodes + graph_edges = edges.edges + except AttributeError: + # edge not Graph-like + self.add_edges_from(edges) + else: # edges is Graph-like + self.add_nodes_from(graph_nodes.data()) + self.add_edges_from(graph_edges.data()) + self.graph.update(edges.graph) + elif nodes is not None: + self.add_nodes_from(nodes) + else: + raise NetworkXError("update needs nodes or edges input") + + def has_edge(self, u, v): + """Returns True if the edge (u, v) is in the graph. + + This is the same as `v in G[u]` without KeyError exceptions. + + Parameters + ---------- + u, v : nodes + Nodes can be, for example, strings or numbers. + Nodes must be hashable (and not None) Python objects. + + Returns + ------- + edge_ind : bool + True if edge is in the graph, False otherwise. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.has_edge(0, 1) # using two nodes + True + >>> e = (0, 1) + >>> G.has_edge(*e) # e is a 2-tuple (u, v) + True + >>> e = (0, 1, {"weight": 7}) + >>> G.has_edge(*e[:2]) # e is a 3-tuple (u, v, data_dictionary) + True + + The following syntax are equivalent: + + >>> G.has_edge(0, 1) + True + >>> 1 in G[0] # though this gives KeyError if 0 not in G + True + + """ + try: + return v in self._adj[u] + except KeyError: + return False + + def neighbors(self, n): + """Returns an iterator over all neighbors of node n. + + This is identical to `iter(G[n])` + + Parameters + ---------- + n : node + A node in the graph + + Returns + ------- + neighbors : iterator + An iterator over all neighbors of node n + + Raises + ------ + NetworkXError + If the node n is not in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> [n for n in G.neighbors(0)] + [1] + + Notes + ----- + Alternate ways to access the neighbors are ``G.adj[n]`` or ``G[n]``: + + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edge("a", "b", weight=7) + >>> G["a"] + AtlasView({'b': {'weight': 7}}) + >>> G = nx.path_graph(4) + >>> [n for n in G[0]] + [1] + """ + try: + return iter(self._adj[n]) + except KeyError as err: + raise NetworkXError(f"The node {n} is not in the graph.") from err + + @cached_property + def edges(self): + """An EdgeView of the Graph as G.edges or G.edges(). + + edges(self, nbunch=None, data=False, default=None) + + The EdgeView provides set-like operations on the edge-tuples + as well as edge attribute lookup. When called, it also provides + an EdgeDataView object which allows control of access to edge + attributes (but does not provide set-like operations). + Hence, `G.edges[u, v]['color']` provides the value of the color + attribute for edge `(u, v)` while + `for (u, v, c) in G.edges.data('color', default='red'):` + iterates through all the edges yielding the color attribute + with default `'red'` if no color attribute exists. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges from these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + edges : EdgeView + A view of edge attributes, usually it iterates over (u, v) + or (u, v, d) tuples of edges, but can also be used for + attribute lookup as `edges[u, v]['foo']`. + + Notes + ----- + Nodes in nbunch that are not in the graph will be (quietly) ignored. + For directed graphs this returns the out-edges. + + Examples + -------- + >>> G = nx.path_graph(3) # or MultiGraph, etc + >>> G.add_edge(2, 3, weight=5) + >>> [e for e in G.edges] + [(0, 1), (1, 2), (2, 3)] + >>> G.edges.data() # default data is {} (empty dict) + EdgeDataView([(0, 1, {}), (1, 2, {}), (2, 3, {'weight': 5})]) + >>> G.edges.data("weight", default=1) + EdgeDataView([(0, 1, 1), (1, 2, 1), (2, 3, 5)]) + >>> G.edges([0, 3]) # only edges from these nodes + EdgeDataView([(0, 1), (3, 2)]) + >>> G.edges(0) # only edges from node 0 + EdgeDataView([(0, 1)]) + """ + return EdgeView(self) + + def get_edge_data(self, u, v, default=None): + """Returns the attribute dictionary associated with edge (u, v). + + This is identical to `G[u][v]` except the default is returned + instead of an exception if the edge doesn't exist. + + Parameters + ---------- + u, v : nodes + default: any Python object (default=None) + Value to return if the edge (u, v) is not found. + + Returns + ------- + edge_dict : dictionary + The edge attribute dictionary. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G[0][1] + {} + + Warning: Assigning to `G[u][v]` is not permitted. + But it is safe to assign attributes `G[u][v]['foo']` + + >>> G[0][1]["weight"] = 7 + >>> G[0][1]["weight"] + 7 + >>> G[1][0]["weight"] + 7 + + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.get_edge_data(0, 1) # default edge data is {} + {} + >>> e = (0, 1) + >>> G.get_edge_data(*e) # tuple form + {} + >>> G.get_edge_data("a", "b", default=0) # edge not in graph, return 0 + 0 + """ + try: + return self._adj[u][v] + except KeyError: + return default + + def adjacency(self): + """Returns an iterator over (node, adjacency dict) tuples for all nodes. + + For directed graphs, only outgoing neighbors/adjacencies are included. + + Returns + ------- + adj_iter : iterator + An iterator over (node, adjacency dictionary) for all nodes in + the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> [(n, nbrdict) for n, nbrdict in G.adjacency()] + [(0, {1: {}}), (1, {0: {}, 2: {}}), (2, {1: {}, 3: {}}), (3, {2: {}})] + + """ + return iter(self._adj.items()) + + @cached_property + def degree(self): + """A DegreeView for the Graph as G.degree or G.degree(). + + The node degree is the number of edges adjacent to the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator for (node, degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + DegreeView or int + If multiple nodes are requested (the default), returns a `DegreeView` + mapping nodes to their degree. + If a single node is requested, returns the degree of the node as an integer. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.degree[0] # node 0 has degree 1 + 1 + >>> list(G.degree([0, 1, 2])) + [(0, 1), (1, 2), (2, 2)] + """ + return DegreeView(self) + + def clear(self): + """Remove all nodes and edges from the graph. + + This also removes the name, and all graph, node, and edge attributes. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.clear() + >>> list(G.nodes) + [] + >>> list(G.edges) + [] + + """ + self._adj.clear() + self._node.clear() + self.graph.clear() + nx._clear_cache(self) + + def clear_edges(self): + """Remove all edges from the graph without altering nodes. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.clear_edges() + >>> list(G.nodes) + [0, 1, 2, 3] + >>> list(G.edges) + [] + """ + for nbr_dict in self._adj.values(): + nbr_dict.clear() + nx._clear_cache(self) + + def is_multigraph(self): + """Returns True if graph is a multigraph, False otherwise.""" + return False + + def is_directed(self): + """Returns True if graph is directed, False otherwise.""" + return False + + def copy(self, as_view=False): + """Returns a copy of the graph. + + The copy method by default returns an independent shallow copy + of the graph and attributes. That is, if an attribute is a + container, that container is shared by the original an the copy. + Use Python's `copy.deepcopy` for new containers. + + If `as_view` is True then a view is returned instead of a copy. + + Notes + ----- + All copies reproduce the graph structure, but data attributes + may be handled in different ways. There are four types of copies + of a graph that people might want. + + Deepcopy -- A "deepcopy" copies the graph structure as well as + all data attributes and any objects they might contain. + The entire graph object is new so that changes in the copy + do not affect the original object. (see Python's copy.deepcopy) + + Data Reference (Shallow) -- For a shallow copy the graph structure + is copied but the edge, node and graph attribute dicts are + references to those in the original graph. This saves + time and memory but could cause confusion if you change an attribute + in one graph and it changes the attribute in the other. + NetworkX does not provide this level of shallow copy. + + Independent Shallow -- This copy creates new independent attribute + dicts and then does a shallow copy of the attributes. That is, any + attributes that are containers are shared between the new graph + and the original. This is exactly what `dict.copy()` provides. + You can obtain this style copy using: + + >>> G = nx.path_graph(5) + >>> H = G.copy() + >>> H = G.copy(as_view=False) + >>> H = nx.Graph(G) + >>> H = G.__class__(G) + + Fresh Data -- For fresh data, the graph structure is copied while + new empty data attribute dicts are created. The resulting graph + is independent of the original and it has no edge, node or graph + attributes. Fresh copies are not enabled. Instead use: + + >>> H = G.__class__() + >>> H.add_nodes_from(G) + >>> H.add_edges_from(G.edges) + + View -- Inspired by dict-views, graph-views act like read-only + versions of the original graph, providing a copy of the original + structure without requiring any memory for copying the information. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Parameters + ---------- + as_view : bool, optional (default=False) + If True, the returned graph-view provides a read-only view + of the original graph without actually copying any data. + + Returns + ------- + G : Graph + A copy of the graph. + + See Also + -------- + to_directed: return a directed copy of the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> H = G.copy() + + """ + if as_view is True: + return nx.graphviews.generic_graph_view(self) + G = self.__class__() + G.graph.update(self.graph) + G.add_nodes_from((n, d.copy()) for n, d in self._node.items()) + G.add_edges_from( + (u, v, datadict.copy()) + for u, nbrs in self._adj.items() + for v, datadict in nbrs.items() + ) + return G + + def to_directed(self, as_view=False): + """Returns a directed representation of the graph. + + Returns + ------- + G : DiGraph + A directed graph with the same name, same nodes, and with + each edge (u, v, data) replaced by two directed edges + (u, v, data) and (v, u, data). + + Notes + ----- + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar D=DiGraph(G) which returns a + shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed Graph to use dict-like objects + in the data structure, those changes do not transfer to the + DiGraph created by this method. + + Examples + -------- + >>> G = nx.Graph() # or MultiGraph, etc + >>> G.add_edge(0, 1) + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1), (1, 0)] + + If already directed, return a (deep) copy + + >>> G = nx.DiGraph() # or MultiDiGraph, etc + >>> G.add_edge(0, 1) + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1)] + """ + graph_class = self.to_directed_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + G.add_edges_from( + (u, v, deepcopy(data)) + for u, nbrs in self._adj.items() + for v, data in nbrs.items() + ) + return G + + def to_undirected(self, as_view=False): + """Returns an undirected copy of the graph. + + Parameters + ---------- + as_view : bool (optional, default=False) + If True return a view of the original undirected graph. + + Returns + ------- + G : Graph/MultiGraph + A deepcopy of the graph. + + See Also + -------- + Graph, copy, add_edge, add_edges_from + + Notes + ----- + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar `G = nx.DiGraph(D)` which returns a + shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed DiGraph to use dict-like objects + in the data structure, those changes do not transfer to the + Graph created by this method. + + Examples + -------- + >>> G = nx.path_graph(2) # or MultiGraph, etc + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1), (1, 0)] + >>> G2 = H.to_undirected() + >>> list(G2.edges) + [(0, 1)] + """ + graph_class = self.to_undirected_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + G.add_edges_from( + (u, v, deepcopy(d)) + for u, nbrs in self._adj.items() + for v, d in nbrs.items() + ) + return G + + def subgraph(self, nodes): + """Returns a SubGraph view of the subgraph induced on `nodes`. + + The induced subgraph of the graph contains the nodes in `nodes` + and the edges between those nodes. + + Parameters + ---------- + nodes : list, iterable + A container of nodes which will be iterated through once. + + Returns + ------- + G : SubGraph View + A subgraph view of the graph. The graph structure cannot be + changed but node/edge attributes can and are shared with the + original graph. + + Notes + ----- + The graph, edge and node attributes are shared with the original graph. + Changes to the graph structure is ruled out by the view, but changes + to attributes are reflected in the original graph. + + To create a subgraph with its own copy of the edge/node attributes use: + G.subgraph(nodes).copy() + + For an inplace reduction of a graph to a subgraph you can remove nodes: + G.remove_nodes_from([n for n in G if n not in set(nodes)]) + + Subgraph views are sometimes NOT what you want. In most cases where + you want to do more than simply look at the induced edges, it makes + more sense to just create the subgraph as its own graph with code like: + + :: + + # Create a subgraph SG based on a (possibly multigraph) G + SG = G.__class__() + SG.add_nodes_from((n, G.nodes[n]) for n in largest_wcc) + if SG.is_multigraph(): + SG.add_edges_from( + (n, nbr, key, d) + for n, nbrs in G.adj.items() + if n in largest_wcc + for nbr, keydict in nbrs.items() + if nbr in largest_wcc + for key, d in keydict.items() + ) + else: + SG.add_edges_from( + (n, nbr, d) + for n, nbrs in G.adj.items() + if n in largest_wcc + for nbr, d in nbrs.items() + if nbr in largest_wcc + ) + SG.graph.update(G.graph) + + Subgraphs are not guaranteed to preserve the order of nodes or edges + as they appear in the original graph. For example: + + >>> G = nx.Graph() + >>> G.add_nodes_from(reversed(range(10))) + >>> list(G) + [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + >>> list(G.subgraph([1, 3, 2])) + [1, 2, 3] + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> H = G.subgraph([0, 1, 2]) + >>> list(H.edges) + [(0, 1), (1, 2)] + """ + induced_nodes = nx.filters.show_nodes(self.nbunch_iter(nodes)) + # if already a subgraph, don't make a chain + subgraph = nx.subgraph_view + if hasattr(self, "_NODE_OK"): + return subgraph( + self._graph, filter_node=induced_nodes, filter_edge=self._EDGE_OK + ) + return subgraph(self, filter_node=induced_nodes) + + def edge_subgraph(self, edges): + """Returns the subgraph induced by the specified edges. + + The induced subgraph contains each edge in `edges` and each + node incident to any one of those edges. + + Parameters + ---------- + edges : iterable + An iterable of edges in this graph. + + Returns + ------- + G : Graph + An edge-induced subgraph of this graph with the same edge + attributes. + + Notes + ----- + The graph, edge, and node attributes in the returned subgraph + view are references to the corresponding attributes in the original + graph. The view is read-only. + + To create a full graph version of the subgraph with its own copy + of the edge or node attributes, use:: + + G.edge_subgraph(edges).copy() + + Examples + -------- + >>> G = nx.path_graph(5) + >>> H = G.edge_subgraph([(0, 1), (3, 4)]) + >>> list(H.nodes) + [0, 1, 3, 4] + >>> list(H.edges) + [(0, 1), (3, 4)] + + """ + return nx.edge_subgraph(self, edges) + + def size(self, weight=None): + """Returns the number of edges or total of all edge weights. + + Parameters + ---------- + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + + Returns + ------- + size : numeric + The number of edges or + (if weight keyword is provided) the total weight sum. + + If weight is None, returns an int. Otherwise a float + (or more general numeric if the weights are more general). + + See Also + -------- + number_of_edges + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.size() + 3 + + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edge("a", "b", weight=2) + >>> G.add_edge("b", "c", weight=4) + >>> G.size() + 2 + >>> G.size(weight="weight") + 6.0 + """ + s = sum(d for v, d in self.degree(weight=weight)) + # If `weight` is None, the sum of the degrees is guaranteed to be + # even, so we can perform integer division and hence return an + # integer. Otherwise, the sum of the weighted degrees is not + # guaranteed to be an integer, so we perform "real" division. + return s // 2 if weight is None else s / 2 + + def number_of_edges(self, u=None, v=None): + """Returns the number of edges between two nodes. + + Parameters + ---------- + u, v : nodes, optional (default=all edges) + If u and v are specified, return the number of edges between + u and v. Otherwise return the total number of all edges. + + Returns + ------- + nedges : int + The number of edges in the graph. If nodes `u` and `v` are + specified return the number of edges between those nodes. If + the graph is directed, this only returns the number of edges + from `u` to `v`. + + See Also + -------- + size + + Examples + -------- + For undirected graphs, this method counts the total number of + edges in the graph: + + >>> G = nx.path_graph(4) + >>> G.number_of_edges() + 3 + + If you specify two nodes, this counts the total number of edges + joining the two nodes: + + >>> G.number_of_edges(0, 1) + 1 + + For directed graphs, this method can count the total number of + directed edges from `u` to `v`: + + >>> G = nx.DiGraph() + >>> G.add_edge(0, 1) + >>> G.add_edge(1, 0) + >>> G.number_of_edges(0, 1) + 1 + + """ + if u is None: + return int(self.size()) + if v in self._adj[u]: + return 1 + return 0 + + def nbunch_iter(self, nbunch=None): + """Returns an iterator over nodes contained in nbunch that are + also in the graph. + + The nodes in an iterable nbunch are checked for membership in the graph + and if not are silently ignored. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + Returns + ------- + niter : iterator + An iterator over nodes in nbunch that are also in the graph. + If nbunch is None, iterate over all nodes in the graph. + + Raises + ------ + NetworkXError + If nbunch is not a node or sequence of nodes. + If a node in nbunch is not hashable. + + See Also + -------- + Graph.__iter__ + + Notes + ----- + When nbunch is an iterator, the returned iterator yields values + directly from nbunch, becoming exhausted when nbunch is exhausted. + + To test whether nbunch is a single node, one can use + "if nbunch in self:", even after processing with this routine. + + If nbunch is not a node or a (possibly empty) sequence/iterator + or None, a :exc:`NetworkXError` is raised. Also, if any object in + nbunch is not hashable, a :exc:`NetworkXError` is raised. + """ + if nbunch is None: # include all nodes via iterator + bunch = iter(self._adj) + elif nbunch in self: # if nbunch is a single node + bunch = iter([nbunch]) + else: # if nbunch is a sequence of nodes + + def bunch_iter(nlist, adj): + try: + for n in nlist: + if n in adj: + yield n + except TypeError as err: + exc, message = err, err.args[0] + # capture error for non-sequence/iterator nbunch. + if "iter" in message: + exc = NetworkXError( + "nbunch is not a node or a sequence of nodes." + ) + # capture single nodes that are not in the graph. + if "object is not iterable" in message: + exc = NetworkXError(f"Node {nbunch} is not in the graph.") + # capture error for unhashable node. + if "hashable" in message: + exc = NetworkXError( + f"Node {n} in sequence nbunch is not a valid node." + ) + raise exc + + bunch = bunch_iter(nbunch, self._adj) + return bunch diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graphviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graphviews.py new file mode 100644 index 0000000000000000000000000000000000000000..0b09df649ef48fa484d27e51d86cce1e10d593a7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/graphviews.py @@ -0,0 +1,269 @@ +"""View of Graphs as SubGraph, Reverse, Directed, Undirected. + +In some algorithms it is convenient to temporarily morph +a graph to exclude some nodes or edges. It should be better +to do that via a view than to remove and then re-add. +In other algorithms it is convenient to temporarily morph +a graph to reverse directed edges, or treat a directed graph +as undirected, etc. This module provides those graph views. + +The resulting views are essentially read-only graphs that +report data from the original graph object. We provide an +attribute G._graph which points to the underlying graph object. + +Note: Since graphviews look like graphs, one can end up with +view-of-view-of-view chains. Be careful with chains because +they become very slow with about 15 nested views. +For the common simple case of node induced subgraphs created +from the graph class, we short-cut the chain by returning a +subgraph of the original graph directly rather than a subgraph +of a subgraph. We are careful not to disrupt any edge filter in +the middle subgraph. In general, determining how to short-cut +the chain is tricky and much harder with restricted_views than +with induced subgraphs. +Often it is easiest to use .copy() to avoid chains. +""" + +import networkx as nx +from networkx.classes.coreviews import ( + FilterAdjacency, + FilterAtlas, + FilterMultiAdjacency, + UnionAdjacency, + UnionMultiAdjacency, +) +from networkx.classes.filters import no_filter +from networkx.exception import NetworkXError +from networkx.utils import not_implemented_for + +__all__ = ["generic_graph_view", "subgraph_view", "reverse_view"] + + +def generic_graph_view(G, create_using=None): + """Returns a read-only view of `G`. + + The graph `G` and its attributes are not copied but viewed through the new graph object + of the same class as `G` (or of the class specified in `create_using`). + + Parameters + ---------- + G : graph + A directed/undirected graph/multigraph. + + create_using : NetworkX graph constructor, optional (default=None) + Graph type to create. If graph instance, then cleared before populated. + If `None`, then the appropriate Graph type is inferred from `G`. + + Returns + ------- + newG : graph + A view of the input graph `G` and its attributes as viewed through + the `create_using` class. + + Raises + ------ + NetworkXError + If `G` is a multigraph (or multidigraph) but `create_using` is not, or vice versa. + + Notes + ----- + The returned graph view is read-only (cannot modify the graph). + Yet the view reflects any changes in `G`. The intent is to mimic dict views. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edge(1, 2, weight=0.3) + >>> G.add_edge(2, 3, weight=0.5) + >>> G.edges(data=True) + EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})]) + + The view exposes the attributes from the original graph. + + >>> viewG = nx.graphviews.generic_graph_view(G) + >>> viewG.edges(data=True) + EdgeDataView([(1, 2, {'weight': 0.3}), (2, 3, {'weight': 0.5})]) + + Changes to `G` are reflected in `viewG`. + + >>> G.remove_edge(2, 3) + >>> G.edges(data=True) + EdgeDataView([(1, 2, {'weight': 0.3})]) + + >>> viewG.edges(data=True) + EdgeDataView([(1, 2, {'weight': 0.3})]) + + We can change the graph type with the `create_using` parameter. + + >>> type(G) + + >>> viewDG = nx.graphviews.generic_graph_view(G, create_using=nx.DiGraph) + >>> type(viewDG) + + """ + if create_using is None: + newG = G.__class__() + else: + newG = nx.empty_graph(0, create_using) + if G.is_multigraph() != newG.is_multigraph(): + raise NetworkXError("Multigraph for G must agree with create_using") + newG = nx.freeze(newG) + + # create view by assigning attributes from G + newG._graph = G + newG.graph = G.graph + + newG._node = G._node + if newG.is_directed(): + if G.is_directed(): + newG._succ = G._succ + newG._pred = G._pred + # newG._adj is synced with _succ + else: + newG._succ = G._adj + newG._pred = G._adj + # newG._adj is synced with _succ + elif G.is_directed(): + if G.is_multigraph(): + newG._adj = UnionMultiAdjacency(G._succ, G._pred) + else: + newG._adj = UnionAdjacency(G._succ, G._pred) + else: + newG._adj = G._adj + return newG + + +def subgraph_view(G, *, filter_node=no_filter, filter_edge=no_filter): + """View of `G` applying a filter on nodes and edges. + + `subgraph_view` provides a read-only view of the input graph that excludes + nodes and edges based on the outcome of two filter functions `filter_node` + and `filter_edge`. + + The `filter_node` function takes one argument --- the node --- and returns + `True` if the node should be included in the subgraph, and `False` if it + should not be included. + + The `filter_edge` function takes two (or three arguments if `G` is a + multi-graph) --- the nodes describing an edge, plus the edge-key if + parallel edges are possible --- and returns `True` if the edge should be + included in the subgraph, and `False` if it should not be included. + + Both node and edge filter functions are called on graph elements as they + are queried, meaning there is no up-front cost to creating the view. + + Parameters + ---------- + G : networkx.Graph + A directed/undirected graph/multigraph + + filter_node : callable, optional + A function taking a node as input, which returns `True` if the node + should appear in the view. + + filter_edge : callable, optional + A function taking as input the two nodes describing an edge (plus the + edge-key if `G` is a multi-graph), which returns `True` if the edge + should appear in the view. + + Returns + ------- + graph : networkx.Graph + A read-only graph view of the input graph. + + Examples + -------- + >>> G = nx.path_graph(6) + + Filter functions operate on the node, and return `True` if the node should + appear in the view: + + >>> def filter_node(n1): + ... return n1 != 5 + >>> view = nx.subgraph_view(G, filter_node=filter_node) + >>> view.nodes() + NodeView((0, 1, 2, 3, 4)) + + We can use a closure pattern to filter graph elements based on additional + data --- for example, filtering on edge data attached to the graph: + + >>> G[3][4]["cross_me"] = False + >>> def filter_edge(n1, n2): + ... return G[n1][n2].get("cross_me", True) + >>> view = nx.subgraph_view(G, filter_edge=filter_edge) + >>> view.edges() + EdgeView([(0, 1), (1, 2), (2, 3), (4, 5)]) + + >>> view = nx.subgraph_view( + ... G, + ... filter_node=filter_node, + ... filter_edge=filter_edge, + ... ) + >>> view.nodes() + NodeView((0, 1, 2, 3, 4)) + >>> view.edges() + EdgeView([(0, 1), (1, 2), (2, 3)]) + """ + newG = nx.freeze(G.__class__()) + newG._NODE_OK = filter_node + newG._EDGE_OK = filter_edge + + # create view by assigning attributes from G + newG._graph = G + newG.graph = G.graph + + newG._node = FilterAtlas(G._node, filter_node) + if G.is_multigraph(): + Adj = FilterMultiAdjacency + + def reverse_edge(u, v, k=None): + return filter_edge(v, u, k) + + else: + Adj = FilterAdjacency + + def reverse_edge(u, v, k=None): + return filter_edge(v, u) + + if G.is_directed(): + newG._succ = Adj(G._succ, filter_node, filter_edge) + newG._pred = Adj(G._pred, filter_node, reverse_edge) + # newG._adj is synced with _succ + else: + newG._adj = Adj(G._adj, filter_node, filter_edge) + return newG + + +@not_implemented_for("undirected") +def reverse_view(G): + """View of `G` with edge directions reversed + + `reverse_view` returns a read-only view of the input graph where + edge directions are reversed. + + Identical to digraph.reverse(copy=False) + + Parameters + ---------- + G : networkx.DiGraph + + Returns + ------- + graph : networkx.DiGraph + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edge(1, 2) + >>> G.add_edge(2, 3) + >>> G.edges() + OutEdgeView([(1, 2), (2, 3)]) + + >>> view = nx.reverse_view(G) + >>> view.edges() + OutEdgeView([(2, 1), (3, 2)]) + """ + newG = generic_graph_view(G) + newG._succ, newG._pred = G._pred, G._succ + # newG._adj is synced with _succ + return newG diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multidigraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multidigraph.py new file mode 100644 index 0000000000000000000000000000000000000000..27c987037e2a47dd80045042893139f0aad226f5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multidigraph.py @@ -0,0 +1,977 @@ +"""Base class for MultiDiGraph.""" + +from copy import deepcopy +from functools import cached_property + +import networkx as nx +from networkx import convert +from networkx.classes.coreviews import MultiAdjacencyView +from networkx.classes.digraph import DiGraph +from networkx.classes.multigraph import MultiGraph +from networkx.classes.reportviews import ( + DiMultiDegreeView, + InMultiDegreeView, + InMultiEdgeView, + OutMultiDegreeView, + OutMultiEdgeView, +) +from networkx.exception import NetworkXError + +__all__ = ["MultiDiGraph"] + + +class MultiDiGraph(MultiGraph, DiGraph): + """A directed graph class that can store multiedges. + + Multiedges are multiple edges between two nodes. Each edge + can hold optional data or attributes. + + A MultiDiGraph holds directed edges. Self loops are allowed. + + Nodes can be arbitrary (hashable) Python objects with optional + key/value attributes. By convention `None` is not used as a node. + + Edges are represented as links between nodes with optional + key/value attributes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be any format that is supported + by the to_networkx_graph() function, currently including edge list, + dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, SciPy + sparse matrix, or PyGraphviz graph. + + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + Graph + DiGraph + MultiGraph + + Examples + -------- + Create an empty graph structure (a "null graph") with no nodes and + no edges. + + >>> G = nx.MultiDiGraph() + + G can be grown in several ways. + + **Nodes:** + + Add one node at a time: + + >>> G.add_node(1) + + Add the nodes from any container (a list, dict, set or + even the lines from a file or the nodes from another graph). + + >>> G.add_nodes_from([2, 3]) + >>> G.add_nodes_from(range(100, 110)) + >>> H = nx.path_graph(10) + >>> G.add_nodes_from(H) + + In addition to strings and integers any hashable Python object + (except None) can represent a node, e.g. a customized node object, + or even another Graph. + + >>> G.add_node(H) + + **Edges:** + + G can also be grown by adding edges. + + Add one edge, + + >>> key = G.add_edge(1, 2) + + a list of edges, + + >>> keys = G.add_edges_from([(1, 2), (1, 3)]) + + or a collection of edges, + + >>> keys = G.add_edges_from(H.edges) + + If some edges connect nodes not yet in the graph, the nodes + are added automatically. If an edge already exists, an additional + edge is created and stored using a key to identify the edge. + By default the key is the lowest unused integer. + + >>> keys = G.add_edges_from([(4, 5, dict(route=282)), (4, 5, dict(route=37))]) + >>> G[4] + AdjacencyView({5: {0: {}, 1: {'route': 282}, 2: {'route': 37}}}) + + **Attributes:** + + Each graph, node, and edge can hold key/value attribute pairs + in an associated attribute dictionary (the keys must be hashable). + By default these are empty, but can be added or changed using + add_edge, add_node or direct manipulation of the attribute + dictionaries named graph, node and edge respectively. + + >>> G = nx.MultiDiGraph(day="Friday") + >>> G.graph + {'day': 'Friday'} + + Add node attributes using add_node(), add_nodes_from() or G.nodes + + >>> G.add_node(1, time="5pm") + >>> G.add_nodes_from([3], time="2pm") + >>> G.nodes[1] + {'time': '5pm'} + >>> G.nodes[1]["room"] = 714 + >>> del G.nodes[1]["room"] # remove attribute + >>> list(G.nodes(data=True)) + [(1, {'time': '5pm'}), (3, {'time': '2pm'})] + + Add edge attributes using add_edge(), add_edges_from(), subscript + notation, or G.edges. + + >>> key = G.add_edge(1, 2, weight=4.7) + >>> keys = G.add_edges_from([(3, 4), (4, 5)], color="red") + >>> keys = G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})]) + >>> G[1][2][0]["weight"] = 4.7 + >>> G.edges[1, 2, 0]["weight"] = 4 + + Warning: we protect the graph data structure by making `G.edges[1, + 2, 0]` a read-only dict-like structure. However, you can assign to + attributes in e.g. `G.edges[1, 2, 0]`. Thus, use 2 sets of brackets + to add/change data attributes: `G.edges[1, 2, 0]['weight'] = 4` + (for multigraphs the edge key is required: `MG.edges[u, v, + key][name] = value`). + + **Shortcuts:** + + Many common graph features allow python syntax to speed reporting. + + >>> 1 in G # check if node in graph + True + >>> [n for n in G if n < 3] # iterate through nodes + [1, 2] + >>> len(G) # number of nodes in graph + 5 + >>> G[1] # adjacency dict-like view mapping neighbor -> edge key -> edge attributes + AdjacencyView({2: {0: {'weight': 4}, 1: {'color': 'blue'}}}) + + Often the best way to traverse all edges of a graph is via the neighbors. + The neighbors are available as an adjacency-view `G.adj` object or via + the method `G.adjacency()`. + + >>> for n, nbrsdict in G.adjacency(): + ... for nbr, keydict in nbrsdict.items(): + ... for key, eattr in keydict.items(): + ... if "weight" in eattr: + ... # Do something useful with the edges + ... pass + + But the edges() method is often more convenient: + + >>> for u, v, keys, weight in G.edges(data="weight", keys=True): + ... if weight is not None: + ... # Do something useful with the edges + ... pass + + **Reporting:** + + Simple graph information is obtained using methods and object-attributes. + Reporting usually provides views instead of containers to reduce memory + usage. The views update as the graph is updated similarly to dict-views. + The objects `nodes`, `edges` and `adj` provide access to data attributes + via lookup (e.g. `nodes[n]`, `edges[u, v, k]`, `adj[u][v]`) and iteration + (e.g. `nodes.items()`, `nodes.data('color')`, + `nodes.data('color', default='blue')` and similarly for `edges`) + Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`. + + For details on these and other miscellaneous methods, see below. + + **Subclasses (Advanced):** + + The MultiDiGraph class uses a dict-of-dict-of-dict-of-dict structure. + The outer dict (node_dict) holds adjacency information keyed by node. + The next dict (adjlist_dict) represents the adjacency information + and holds edge_key dicts keyed by neighbor. The edge_key dict holds + each edge_attr dict keyed by edge key. The inner dict + (edge_attr_dict) represents the edge data and holds edge attribute + values keyed by attribute names. + + Each of these four dicts in the dict-of-dict-of-dict-of-dict + structure can be replaced by a user defined dict-like object. + In general, the dict-like features should be maintained but + extra features can be added. To replace one of the dicts create + a new graph class by changing the class(!) variable holding the + factory for that dict-like structure. The variable names are + node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory, + adjlist_outer_dict_factory, edge_key_dict_factory, edge_attr_dict_factory + and graph_attr_dict_factory. + + node_dict_factory : function, (default: dict) + Factory function to be used to create the dict containing node + attributes, keyed by node id. + It should require no arguments and return a dict-like object + + node_attr_dict_factory: function, (default: dict) + Factory function to be used to create the node attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object + + adjlist_outer_dict_factory : function, (default: dict) + Factory function to be used to create the outer-most dict + in the data structure that holds adjacency info keyed by node. + It should require no arguments and return a dict-like object. + + adjlist_inner_dict_factory : function, (default: dict) + Factory function to be used to create the adjacency list + dict which holds multiedge key dicts keyed by neighbor. + It should require no arguments and return a dict-like object. + + edge_key_dict_factory : function, (default: dict) + Factory function to be used to create the edge key dict + which holds edge data keyed by edge key. + It should require no arguments and return a dict-like object. + + edge_attr_dict_factory : function, (default: dict) + Factory function to be used to create the edge attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + graph_attr_dict_factory : function, (default: dict) + Factory function to be used to create the graph attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + Typically, if your extension doesn't impact the data structure all + methods will inherited without issue except: `to_directed/to_undirected`. + By default these methods create a DiGraph/Graph class and you probably + want them to create your extension of a DiGraph/Graph. To facilitate + this we define two class variables that you can set in your subclass. + + to_directed_class : callable, (default: DiGraph or MultiDiGraph) + Class to create a new graph structure in the `to_directed` method. + If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used. + + to_undirected_class : callable, (default: Graph or MultiGraph) + Class to create a new graph structure in the `to_undirected` method. + If `None`, a NetworkX class (Graph or MultiGraph) is used. + + **Subclassing Example** + + Create a low memory graph class that effectively disallows edge + attributes by using a single attribute dict for all edges. + This reduces the memory used, but you lose edge attributes. + + >>> class ThinGraph(nx.Graph): + ... all_edge_dict = {"weight": 1} + ... + ... def single_edge_dict(self): + ... return self.all_edge_dict + ... + ... edge_attr_dict_factory = single_edge_dict + >>> G = ThinGraph() + >>> G.add_edge(2, 1) + >>> G[2][1] + {'weight': 1} + >>> G.add_edge(2, 2) + >>> G[2][1] is G[2][2] + True + """ + + # node_dict_factory = dict # already assigned in Graph + # adjlist_outer_dict_factory = dict + # adjlist_inner_dict_factory = dict + edge_key_dict_factory = dict + # edge_attr_dict_factory = dict + + # This __new__ method just does what Python itself does automatically. + # We include it here as part of the dispatchable/backend interface. + # If your goal is to understand how the graph classes work, you can ignore + # this method, even when subclassing the base classes. If you are subclassing + # in order to provide a backend that allows class instantiation, this method + # can be overridden to return your own backend graph class. + @nx._dispatchable(name="multidigraph__new__", graphs=None, returns_graph=True) + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr): + """Initialize a graph with edges, name, or graph attributes. + + Parameters + ---------- + incoming_graph_data : input graph + Data to initialize graph. If incoming_graph_data=None (default) + an empty graph is created. The data can be an edge list, or any + NetworkX graph object. If the corresponding optional Python + packages are installed the data can also be a 2D NumPy array, a + SciPy sparse array, or a PyGraphviz graph. + + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + convert + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G = nx.Graph(name="my graph") + >>> e = [(1, 2), (2, 3), (3, 4)] # list of edges + >>> G = nx.Graph(e) + + Arbitrary graph attribute pairs (key=value) may be assigned + + >>> G = nx.Graph(e, day="Friday") + >>> G.graph + {'day': 'Friday'} + + """ + attr.pop("backend", None) # Ignore explicit `backend="networkx"` + # multigraph_input can be None/True/False. So check "is not False" + if isinstance(incoming_graph_data, dict) and multigraph_input is not False: + DiGraph.__init__(self) + try: + convert.from_dict_of_dicts( + incoming_graph_data, create_using=self, multigraph_input=True + ) + self.graph.update(attr) + except Exception as err: + if multigraph_input is True: + raise nx.NetworkXError( + f"converting multigraph_input raised:\n{type(err)}: {err}" + ) + DiGraph.__init__(self, incoming_graph_data, **attr) + else: + DiGraph.__init__(self, incoming_graph_data, **attr) + + @cached_property + def adj(self): + """Graph adjacency object holding the neighbors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets + the color of the edge `(3, 2, 0)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, datadict in G.adj[n].items():`. + + The neighbor information is also provided by subscripting the graph. + So `for nbr, foovalue in G[node].data('foo', default=1):` works. + + For directed graphs, `G.adj` holds outgoing (successor) info. + """ + return MultiAdjacencyView(self._succ) + + @cached_property + def succ(self): + """Graph adjacency object holding the successors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets + the color of the edge `(3, 2, 0)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, datadict in G.adj[n].items():`. + + The neighbor information is also provided by subscripting the graph. + So `for nbr, foovalue in G[node].data('foo', default=1):` works. + + For directed graphs, `G.succ` is identical to `G.adj`. + """ + return MultiAdjacencyView(self._succ) + + @cached_property + def pred(self): + """Graph adjacency object holding the predecessors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edgekey-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets + the color of the edge `(3, 2, 0)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, datadict in G.adj[n].items():`. + """ + return MultiAdjacencyView(self._pred) + + def add_edge(self, u_for_edge, v_for_edge, key=None, **attr): + """Add an edge between u and v. + + The nodes u and v will be automatically added if they are + not already in the graph. + + Edge attributes can be specified with keywords or by directly + accessing the edge's attribute dictionary. See examples below. + + Parameters + ---------- + u_for_edge, v_for_edge : nodes + Nodes can be, for example, strings or numbers. + Nodes must be hashable (and not None) Python objects. + key : hashable identifier, optional (default=lowest unused integer) + Used to distinguish multiedges between a pair of nodes. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + Returns + ------- + The edge key assigned to the edge. + + See Also + -------- + add_edges_from : add a collection of edges + + Notes + ----- + To replace/update edge data, use the optional key argument + to identify a unique edge. Otherwise a new edge will be created. + + NetworkX algorithms designed for weighted graphs cannot use + multigraphs directly because it is not clear how to handle + multiedge weights. Convert to Graph using edge attribute + 'weight' to enable weighted graph algorithms. + + Default keys are generated using the method `new_edge_key()`. + This method can be overridden by subclassing the base class and + providing a custom `new_edge_key()` method. + + Examples + -------- + The following all add the edge e=(1, 2) to graph G: + + >>> G = nx.MultiDiGraph() + >>> e = (1, 2) + >>> key = G.add_edge(1, 2) # explicit two-node form + >>> G.add_edge(*e) # single edge as tuple of two nodes + 1 + >>> G.add_edges_from([(1, 2)]) # add edges from iterable container + [2] + + Associate data to edges using keywords: + + >>> key = G.add_edge(1, 2, weight=3) + >>> key = G.add_edge(1, 2, key=0, weight=4) # update data for key=0 + >>> key = G.add_edge(1, 3, weight=7, capacity=15, length=342.7) + + For non-string attribute keys, use subscript notation. + + >>> ekey = G.add_edge(1, 2) + >>> G[1][2][0].update({0: 5}) + >>> G.edges[1, 2, 0].update({0: 5}) + """ + u, v = u_for_edge, v_for_edge + # add nodes + if u not in self._succ: + if u is None: + raise ValueError("None cannot be a node") + self._succ[u] = self.adjlist_inner_dict_factory() + self._pred[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._succ: + if v is None: + raise ValueError("None cannot be a node") + self._succ[v] = self.adjlist_inner_dict_factory() + self._pred[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + if key is None: + key = self.new_edge_key(u, v) + if v in self._succ[u]: + keydict = self._adj[u][v] + datadict = keydict.get(key, self.edge_attr_dict_factory()) + datadict.update(attr) + keydict[key] = datadict + else: + # selfloops work this way without special treatment + datadict = self.edge_attr_dict_factory() + datadict.update(attr) + keydict = self.edge_key_dict_factory() + keydict[key] = datadict + self._succ[u][v] = keydict + self._pred[v][u] = keydict + nx._clear_cache(self) + return key + + def remove_edge(self, u, v, key=None): + """Remove an edge between u and v. + + Parameters + ---------- + u, v : nodes + Remove an edge between nodes u and v. + key : hashable identifier, optional (default=None) + Used to distinguish multiple edges between a pair of nodes. + If None, remove a single edge between u and v. If there are + multiple edges, removes the last edge added in terms of + insertion order. + + Raises + ------ + NetworkXError + If there is not an edge between u and v, or + if there is no edge with the specified key. + + See Also + -------- + remove_edges_from : remove a collection of edges + + Examples + -------- + >>> G = nx.MultiDiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.remove_edge(0, 1) + >>> e = (1, 2) + >>> G.remove_edge(*e) # unpacks e from an edge tuple + + For multiple edges + + >>> G = nx.MultiDiGraph() + >>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned + [0, 1, 2] + + When ``key=None`` (the default), edges are removed in the opposite + order that they were added: + + >>> G.remove_edge(1, 2) + >>> G.edges(keys=True) + OutMultiEdgeView([(1, 2, 0), (1, 2, 1)]) + + For edges with keys + + >>> G = nx.MultiDiGraph() + >>> G.add_edge(1, 2, key="first") + 'first' + >>> G.add_edge(1, 2, key="second") + 'second' + >>> G.remove_edge(1, 2, key="first") + >>> G.edges(keys=True) + OutMultiEdgeView([(1, 2, 'second')]) + + """ + try: + d = self._adj[u][v] + except KeyError as err: + raise NetworkXError(f"The edge {u}-{v} is not in the graph.") from err + # remove the edge with specified data + if key is None: + d.popitem() + else: + try: + del d[key] + except KeyError as err: + msg = f"The edge {u}-{v} with key {key} is not in the graph." + raise NetworkXError(msg) from err + if len(d) == 0: + # remove the key entries if last edge + del self._succ[u][v] + del self._pred[v][u] + nx._clear_cache(self) + + @cached_property + def edges(self): + """An OutMultiEdgeView of the Graph as G.edges or G.edges(). + + edges(self, nbunch=None, data=False, keys=False, default=None) + + The OutMultiEdgeView provides set-like operations on the edge-tuples + as well as edge attribute lookup. When called, it also provides + an EdgeDataView object which allows control of access to edge + attributes (but does not provide set-like operations). + Hence, ``G.edges[u, v, k]['color']`` provides the value of the color + attribute for the edge from ``u`` to ``v`` with key ``k`` while + ``for (u, v, k, c) in G.edges(data='color', default='red', keys=True):`` + iterates through all the edges yielding the color attribute with + default `'red'` if no color attribute exists. + + Edges are returned as tuples with optional data and keys + in the order (node, neighbor, key, data). If ``keys=True`` is not + provided, the tuples will just be (node, neighbor, data), but + multiple tuples with the same node and neighbor will be + generated when multiple edges between two nodes exist. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges from these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + keys : bool, optional (default=False) + If True, return edge keys with each edge, creating (u, v, k, + d) tuples when data is also requested (the default) and (u, + v, k) tuples when data is not requested. + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + edges : OutMultiEdgeView + A view of edge attributes, usually it iterates over (u, v) + (u, v, k) or (u, v, k, d) tuples of edges, but can also be + used for attribute lookup as ``edges[u, v, k]['foo']``. + + Notes + ----- + Nodes in nbunch that are not in the graph will be (quietly) ignored. + For directed graphs this returns the out-edges. + + Examples + -------- + >>> G = nx.MultiDiGraph() + >>> nx.add_path(G, [0, 1, 2]) + >>> key = G.add_edge(2, 3, weight=5) + >>> key2 = G.add_edge(1, 2) # second edge between these nodes + >>> [e for e in G.edges()] + [(0, 1), (1, 2), (1, 2), (2, 3)] + >>> list(G.edges(data=True)) # default data is {} (empty dict) + [(0, 1, {}), (1, 2, {}), (1, 2, {}), (2, 3, {'weight': 5})] + >>> list(G.edges(data="weight", default=1)) + [(0, 1, 1), (1, 2, 1), (1, 2, 1), (2, 3, 5)] + >>> list(G.edges(keys=True)) # default keys are integers + [(0, 1, 0), (1, 2, 0), (1, 2, 1), (2, 3, 0)] + >>> list(G.edges(data=True, keys=True)) + [(0, 1, 0, {}), (1, 2, 0, {}), (1, 2, 1, {}), (2, 3, 0, {'weight': 5})] + >>> list(G.edges(data="weight", default=1, keys=True)) + [(0, 1, 0, 1), (1, 2, 0, 1), (1, 2, 1, 1), (2, 3, 0, 5)] + >>> list(G.edges([0, 2])) + [(0, 1), (2, 3)] + >>> list(G.edges(0)) + [(0, 1)] + >>> list(G.edges(1)) + [(1, 2), (1, 2)] + + See Also + -------- + in_edges, out_edges + """ + return OutMultiEdgeView(self) + + # alias out_edges to edges + @cached_property + def out_edges(self): + return OutMultiEdgeView(self) + + out_edges.__doc__ = edges.__doc__ + + @cached_property + def in_edges(self): + """A view of the in edges of the graph as G.in_edges or G.in_edges(). + + in_edges(self, nbunch=None, data=False, keys=False, default=None) + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + keys : bool, optional (default=False) + If True, return edge keys with each edge, creating 3-tuples + (u, v, k) or with data, 4-tuples (u, v, k, d). + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + in_edges : InMultiEdgeView or InMultiEdgeDataView + A view of edge attributes, usually it iterates over (u, v) + or (u, v, k) or (u, v, k, d) tuples of edges, but can also be + used for attribute lookup as `edges[u, v, k]['foo']`. + + See Also + -------- + edges + """ + return InMultiEdgeView(self) + + @cached_property + def degree(self): + """A DegreeView for the Graph as G.degree or G.degree(). + + The node degree is the number of edges adjacent to the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator for (node, degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + DiMultiDegreeView or int + If multiple nodes are requested (the default), returns a `DiMultiDegreeView` + mapping nodes to their degree. + If a single node is requested, returns the degree of the node as an integer. + + See Also + -------- + out_degree, in_degree + + Examples + -------- + >>> G = nx.MultiDiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.degree(0) # node 0 with degree 1 + 1 + >>> list(G.degree([0, 1, 2])) + [(0, 1), (1, 2), (2, 2)] + >>> G.add_edge(0, 1) # parallel edge + 1 + >>> list(G.degree([0, 1, 2])) # parallel edges are counted + [(0, 2), (1, 3), (2, 2)] + + """ + return DiMultiDegreeView(self) + + @cached_property + def in_degree(self): + """A DegreeView for (node, in_degree) or in_degree for single node. + + The node in-degree is the number of edges pointing into the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator for (node, degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + If a single node is requested + deg : int + Degree of the node + + OR if multiple nodes are requested + nd_iter : iterator + The iterator returns two-tuples of (node, in-degree). + + See Also + -------- + degree, out_degree + + Examples + -------- + >>> G = nx.MultiDiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.in_degree(0) # node 0 with degree 0 + 0 + >>> list(G.in_degree([0, 1, 2])) + [(0, 0), (1, 1), (2, 1)] + >>> G.add_edge(0, 1) # parallel edge + 1 + >>> list(G.in_degree([0, 1, 2])) # parallel edges counted + [(0, 0), (1, 2), (2, 1)] + + """ + return InMultiDegreeView(self) + + @cached_property + def out_degree(self): + """Returns an iterator for (node, out-degree) or out-degree for single node. + + out_degree(self, nbunch=None, weight=None) + + The node out-degree is the number of edges pointing out of the node. + This function returns the out-degree for a single node or an iterator + for a bunch of nodes or if nothing is passed as argument. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights. + + Returns + ------- + If a single node is requested + deg : int + Degree of the node + + OR if multiple nodes are requested + nd_iter : iterator + The iterator returns two-tuples of (node, out-degree). + + See Also + -------- + degree, in_degree + + Examples + -------- + >>> G = nx.MultiDiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.out_degree(0) # node 0 with degree 1 + 1 + >>> list(G.out_degree([0, 1, 2])) + [(0, 1), (1, 1), (2, 1)] + >>> G.add_edge(0, 1) # parallel edge + 1 + >>> list(G.out_degree([0, 1, 2])) # counts parallel edges + [(0, 2), (1, 1), (2, 1)] + + """ + return OutMultiDegreeView(self) + + def is_multigraph(self): + """Returns True if graph is a multigraph, False otherwise.""" + return True + + def is_directed(self): + """Returns True if graph is directed, False otherwise.""" + return True + + def to_undirected(self, reciprocal=False, as_view=False): + """Returns an undirected representation of the digraph. + + Parameters + ---------- + reciprocal : bool (optional) + If True only keep edges that appear in both directions + in the original digraph. + as_view : bool (optional, default=False) + If True return an undirected view of the original directed graph. + + Returns + ------- + G : MultiGraph + An undirected graph with the same name and nodes and + with edge (u, v, data) if either (u, v, data) or (v, u, data) + is in the digraph. If both edges exist in digraph and + their edge data is different, only one edge is created + with an arbitrary choice of which edge data to use. + You must check and correct for this manually if desired. + + See Also + -------- + MultiGraph, copy, add_edge, add_edges_from + + Notes + ----- + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar D=MultiDiGraph(G) which + returns a shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed MultiDiGraph to use dict-like + objects in the data structure, those changes do not transfer + to the MultiGraph created by this method. + + Examples + -------- + >>> G = nx.path_graph(2) # or MultiGraph, etc + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1), (1, 0)] + >>> G2 = H.to_undirected() + >>> list(G2.edges) + [(0, 1)] + """ + graph_class = self.to_undirected_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + if reciprocal is True: + G.add_edges_from( + (u, v, key, deepcopy(data)) + for u, nbrs in self._adj.items() + for v, keydict in nbrs.items() + for key, data in keydict.items() + if v in self._pred[u] and key in self._pred[u][v] + ) + else: + G.add_edges_from( + (u, v, key, deepcopy(data)) + for u, nbrs in self._adj.items() + for v, keydict in nbrs.items() + for key, data in keydict.items() + ) + return G + + def reverse(self, copy=True): + """Returns the reverse of the graph. + + The reverse is a graph with the same nodes and edges + but with the directions of the edges reversed. + + Parameters + ---------- + copy : bool optional (default=True) + If True, return a new DiGraph holding the reversed edges. + If False, the reverse graph is created using a view of + the original graph. + """ + if copy: + H = self.__class__() + H.graph.update(deepcopy(self.graph)) + H.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + H.add_edges_from( + (v, u, k, deepcopy(d)) + for u, v, k, d in self.edges(keys=True, data=True) + ) + return H + return nx.reverse_view(self) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multigraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multigraph.py new file mode 100644 index 0000000000000000000000000000000000000000..a942f2e04ccea8a877e40d9b03cd8484844cb147 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/multigraph.py @@ -0,0 +1,1294 @@ +"""Base class for MultiGraph.""" + +from copy import deepcopy +from functools import cached_property + +import networkx as nx +from networkx import NetworkXError, convert +from networkx.classes.coreviews import MultiAdjacencyView +from networkx.classes.graph import Graph +from networkx.classes.reportviews import MultiDegreeView, MultiEdgeView + +__all__ = ["MultiGraph"] + + +class MultiGraph(Graph): + """ + An undirected graph class that can store multiedges. + + Multiedges are multiple edges between two nodes. Each edge + can hold optional data or attributes. + + A MultiGraph holds undirected edges. Self loops are allowed. + + Nodes can be arbitrary (hashable) Python objects with optional + key/value attributes. By convention `None` is not used as a node. + + Edges are represented as links between nodes with optional + key/value attributes, in a MultiGraph each edge has a key to + distinguish between multiple edges that have the same source and + destination nodes. + + Parameters + ---------- + incoming_graph_data : input graph (optional, default: None) + Data to initialize graph. If None (default) an empty + graph is created. The data can be any format that is supported + by the to_networkx_graph() function, currently including edge list, + dict of dicts, dict of lists, NetworkX graph, 2D NumPy array, + SciPy sparse array, or PyGraphviz graph. + + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + Graph + DiGraph + MultiDiGraph + + Examples + -------- + Create an empty graph structure (a "null graph") with no nodes and + no edges. + + >>> G = nx.MultiGraph() + + G can be grown in several ways. + + **Nodes:** + + Add one node at a time: + + >>> G.add_node(1) + + Add the nodes from any container (a list, dict, set or + even the lines from a file or the nodes from another graph). + + >>> G.add_nodes_from([2, 3]) + >>> G.add_nodes_from(range(100, 110)) + >>> H = nx.path_graph(10) + >>> G.add_nodes_from(H) + + In addition to strings and integers any hashable Python object + (except None) can represent a node, e.g. a customized node object, + or even another Graph. + + >>> G.add_node(H) + + **Edges:** + + G can also be grown by adding edges. + + Add one edge, + + >>> key = G.add_edge(1, 2) + + a list of edges, + + >>> keys = G.add_edges_from([(1, 2), (1, 3)]) + + or a collection of edges, + + >>> keys = G.add_edges_from(H.edges) + + If some edges connect nodes not yet in the graph, the nodes + are added automatically. If an edge already exists, an additional + edge is created and stored using a key to identify the edge. + By default the key is the lowest unused integer. + + >>> keys = G.add_edges_from([(4, 5, {"route": 28}), (4, 5, {"route": 37})]) + >>> G[4] + AdjacencyView({3: {0: {}}, 5: {0: {}, 1: {'route': 28}, 2: {'route': 37}}}) + + **Attributes:** + + Each graph, node, and edge can hold key/value attribute pairs + in an associated attribute dictionary (the keys must be hashable). + By default these are empty, but can be added or changed using + add_edge, add_node or direct manipulation of the attribute + dictionaries named graph, node and edge respectively. + + >>> G = nx.MultiGraph(day="Friday") + >>> G.graph + {'day': 'Friday'} + + Add node attributes using add_node(), add_nodes_from() or G.nodes + + >>> G.add_node(1, time="5pm") + >>> G.add_nodes_from([3], time="2pm") + >>> G.nodes[1] + {'time': '5pm'} + >>> G.nodes[1]["room"] = 714 + >>> del G.nodes[1]["room"] # remove attribute + >>> list(G.nodes(data=True)) + [(1, {'time': '5pm'}), (3, {'time': '2pm'})] + + Add edge attributes using add_edge(), add_edges_from(), subscript + notation, or G.edges. + + >>> key = G.add_edge(1, 2, weight=4.7) + >>> keys = G.add_edges_from([(3, 4), (4, 5)], color="red") + >>> keys = G.add_edges_from([(1, 2, {"color": "blue"}), (2, 3, {"weight": 8})]) + >>> G[1][2][0]["weight"] = 4.7 + >>> G.edges[1, 2, 0]["weight"] = 4 + + Warning: we protect the graph data structure by making `G.edges[1, + 2, 0]` a read-only dict-like structure. However, you can assign to + attributes in e.g. `G.edges[1, 2, 0]`. Thus, use 2 sets of brackets + to add/change data attributes: `G.edges[1, 2, 0]['weight'] = 4`. + + **Shortcuts:** + + Many common graph features allow python syntax to speed reporting. + + >>> 1 in G # check if node in graph + True + >>> [n for n in G if n < 3] # iterate through nodes + [1, 2] + >>> len(G) # number of nodes in graph + 5 + >>> G[1] # adjacency dict-like view mapping neighbor -> edge key -> edge attributes + AdjacencyView({2: {0: {'weight': 4}, 1: {'color': 'blue'}}}) + + Often the best way to traverse all edges of a graph is via the neighbors. + The neighbors are reported as an adjacency-dict `G.adj` or `G.adjacency()`. + + >>> for n, nbrsdict in G.adjacency(): + ... for nbr, keydict in nbrsdict.items(): + ... for key, eattr in keydict.items(): + ... if "weight" in eattr: + ... # Do something useful with the edges + ... pass + + But the edges() method is often more convenient: + + >>> for u, v, keys, weight in G.edges(data="weight", keys=True): + ... if weight is not None: + ... # Do something useful with the edges + ... pass + + **Reporting:** + + Simple graph information is obtained using methods and object-attributes. + Reporting usually provides views instead of containers to reduce memory + usage. The views update as the graph is updated similarly to dict-views. + The objects `nodes`, `edges` and `adj` provide access to data attributes + via lookup (e.g. `nodes[n]`, `edges[u, v, k]`, `adj[u][v]`) and iteration + (e.g. `nodes.items()`, `nodes.data('color')`, + `nodes.data('color', default='blue')` and similarly for `edges`) + Views exist for `nodes`, `edges`, `neighbors()`/`adj` and `degree`. + + For details on these and other miscellaneous methods, see below. + + **Subclasses (Advanced):** + + The MultiGraph class uses a dict-of-dict-of-dict-of-dict data structure. + The outer dict (node_dict) holds adjacency information keyed by node. + The next dict (adjlist_dict) represents the adjacency information + and holds edge_key dicts keyed by neighbor. The edge_key dict holds + each edge_attr dict keyed by edge key. The inner dict + (edge_attr_dict) represents the edge data and holds edge attribute + values keyed by attribute names. + + Each of these four dicts in the dict-of-dict-of-dict-of-dict + structure can be replaced by a user defined dict-like object. + In general, the dict-like features should be maintained but + extra features can be added. To replace one of the dicts create + a new graph class by changing the class(!) variable holding the + factory for that dict-like structure. The variable names are + node_dict_factory, node_attr_dict_factory, adjlist_inner_dict_factory, + adjlist_outer_dict_factory, edge_key_dict_factory, edge_attr_dict_factory + and graph_attr_dict_factory. + + node_dict_factory : function, (default: dict) + Factory function to be used to create the dict containing node + attributes, keyed by node id. + It should require no arguments and return a dict-like object + + node_attr_dict_factory: function, (default: dict) + Factory function to be used to create the node attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object + + adjlist_outer_dict_factory : function, (default: dict) + Factory function to be used to create the outer-most dict + in the data structure that holds adjacency info keyed by node. + It should require no arguments and return a dict-like object. + + adjlist_inner_dict_factory : function, (default: dict) + Factory function to be used to create the adjacency list + dict which holds multiedge key dicts keyed by neighbor. + It should require no arguments and return a dict-like object. + + edge_key_dict_factory : function, (default: dict) + Factory function to be used to create the edge key dict + which holds edge data keyed by edge key. + It should require no arguments and return a dict-like object. + + edge_attr_dict_factory : function, (default: dict) + Factory function to be used to create the edge attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + graph_attr_dict_factory : function, (default: dict) + Factory function to be used to create the graph attribute + dict which holds attribute values keyed by attribute name. + It should require no arguments and return a dict-like object. + + Typically, if your extension doesn't impact the data structure all + methods will inherited without issue except: `to_directed/to_undirected`. + By default these methods create a DiGraph/Graph class and you probably + want them to create your extension of a DiGraph/Graph. To facilitate + this we define two class variables that you can set in your subclass. + + to_directed_class : callable, (default: DiGraph or MultiDiGraph) + Class to create a new graph structure in the `to_directed` method. + If `None`, a NetworkX class (DiGraph or MultiDiGraph) is used. + + to_undirected_class : callable, (default: Graph or MultiGraph) + Class to create a new graph structure in the `to_undirected` method. + If `None`, a NetworkX class (Graph or MultiGraph) is used. + + **Subclassing Example** + + Create a low memory graph class that effectively disallows edge + attributes by using a single attribute dict for all edges. + This reduces the memory used, but you lose edge attributes. + + >>> class ThinGraph(nx.Graph): + ... all_edge_dict = {"weight": 1} + ... + ... def single_edge_dict(self): + ... return self.all_edge_dict + ... + ... edge_attr_dict_factory = single_edge_dict + >>> G = ThinGraph() + >>> G.add_edge(2, 1) + >>> G[2][1] + {'weight': 1} + >>> G.add_edge(2, 2) + >>> G[2][1] is G[2][2] + True + """ + + # node_dict_factory = dict # already assigned in Graph + # adjlist_outer_dict_factory = dict + # adjlist_inner_dict_factory = dict + edge_key_dict_factory = dict + # edge_attr_dict_factory = dict + + def to_directed_class(self): + """Returns the class to use for empty directed copies. + + If you subclass the base classes, use this to designate + what directed class to use for `to_directed()` copies. + """ + return nx.MultiDiGraph + + def to_undirected_class(self): + """Returns the class to use for empty undirected copies. + + If you subclass the base classes, use this to designate + what directed class to use for `to_directed()` copies. + """ + return MultiGraph + + # This __new__ method just does what Python itself does automatically. + # We include it here as part of the dispatchable/backend interface. + # If your goal is to understand how the graph classes work, you can ignore + # this method, even when subclassing the base classes. If you are subclassing + # in order to provide a backend that allows class instantiation, this method + # can be overridden to return your own backend graph class. + @nx._dispatchable(name="multigraph__new__", graphs=None, returns_graph=True) + def __new__(cls, *args, **kwargs): + return object.__new__(cls) + + def __init__(self, incoming_graph_data=None, multigraph_input=None, **attr): + """Initialize a graph with edges, name, or graph attributes. + + Parameters + ---------- + incoming_graph_data : input graph + Data to initialize graph. If incoming_graph_data=None (default) + an empty graph is created. The data can be an edge list, or any + NetworkX graph object. If the corresponding optional Python + packages are installed the data can also be a 2D NumPy array, a + SciPy sparse array, or a PyGraphviz graph. + + multigraph_input : bool or None (default None) + Note: Only used when `incoming_graph_data` is a dict. + If True, `incoming_graph_data` is assumed to be a + dict-of-dict-of-dict-of-dict structure keyed by + node to neighbor to edge keys to edge data for multi-edges. + A NetworkXError is raised if this is not the case. + If False, :func:`to_networkx_graph` is used to try to determine + the dict's graph data structure as either a dict-of-dict-of-dict + keyed by node to neighbor to edge data, or a dict-of-iterable + keyed by node to neighbors. + If None, the treatment for True is tried, but if it fails, + the treatment for False is tried. + + attr : keyword arguments, optional (default= no attributes) + Attributes to add to graph as key=value pairs. + + See Also + -------- + convert + + Examples + -------- + >>> G = nx.MultiGraph() + >>> G = nx.MultiGraph(name="my graph") + >>> e = [(1, 2), (1, 2), (2, 3), (3, 4)] # list of edges + >>> G = nx.MultiGraph(e) + + Arbitrary graph attribute pairs (key=value) may be assigned + + >>> G = nx.MultiGraph(e, day="Friday") + >>> G.graph + {'day': 'Friday'} + + """ + attr.pop("backend", None) # Ignore explicit `backend="networkx"` + # multigraph_input can be None/True/False. So check "is not False" + if isinstance(incoming_graph_data, dict) and multigraph_input is not False: + Graph.__init__(self) + try: + convert.from_dict_of_dicts( + incoming_graph_data, create_using=self, multigraph_input=True + ) + self.graph.update(attr) + except Exception as err: + if multigraph_input is True: + raise nx.NetworkXError( + f"converting multigraph_input raised:\n{type(err)}: {err}" + ) + Graph.__init__(self, incoming_graph_data, **attr) + else: + Graph.__init__(self, incoming_graph_data, **attr) + + @cached_property + def adj(self): + """Graph adjacency object holding the neighbors of each node. + + This object is a read-only dict-like structure with node keys + and neighbor-dict values. The neighbor-dict is keyed by neighbor + to the edgekey-data-dict. So `G.adj[3][2][0]['color'] = 'blue'` sets + the color of the edge `(3, 2, 0)` to `"blue"`. + + Iterating over G.adj behaves like a dict. Useful idioms include + `for nbr, edgesdict in G.adj[n].items():`. + + The neighbor information is also provided by subscripting the graph. + + Examples + -------- + >>> e = [(1, 2), (1, 2), (1, 3), (3, 4)] # list of edges + >>> G = nx.MultiGraph(e) + >>> G.edges[1, 2, 0]["weight"] = 3 + >>> result = set() + >>> for edgekey, data in G[1][2].items(): + ... result.add(data.get("weight", 1)) + >>> result + {1, 3} + + For directed graphs, `G.adj` holds outgoing (successor) info. + """ + return MultiAdjacencyView(self._adj) + + def new_edge_key(self, u, v): + """Returns an unused key for edges between nodes `u` and `v`. + + The nodes `u` and `v` do not need to be already in the graph. + + Notes + ----- + In the standard MultiGraph class the new key is the number of existing + edges between `u` and `v` (increased if necessary to ensure unused). + The first edge will have key 0, then 1, etc. If an edge is removed + further new_edge_keys may not be in this order. + + Parameters + ---------- + u, v : nodes + + Returns + ------- + key : int + """ + try: + keydict = self._adj[u][v] + except KeyError: + return 0 + key = len(keydict) + while key in keydict: + key += 1 + return key + + def add_edge(self, u_for_edge, v_for_edge, key=None, **attr): + """Add an edge between u and v. + + The nodes u and v will be automatically added if they are + not already in the graph. + + Edge attributes can be specified with keywords or by directly + accessing the edge's attribute dictionary. See examples below. + + Parameters + ---------- + u_for_edge, v_for_edge : nodes + Nodes can be, for example, strings or numbers. + Nodes must be hashable (and not None) Python objects. + key : hashable identifier, optional (default=lowest unused integer) + Used to distinguish multiedges between a pair of nodes. + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + Returns + ------- + The edge key assigned to the edge. + + See Also + -------- + add_edges_from : add a collection of edges + + Notes + ----- + To replace/update edge data, use the optional key argument + to identify a unique edge. Otherwise a new edge will be created. + + NetworkX algorithms designed for weighted graphs cannot use + multigraphs directly because it is not clear how to handle + multiedge weights. Convert to Graph using edge attribute + 'weight' to enable weighted graph algorithms. + + Default keys are generated using the method `new_edge_key()`. + This method can be overridden by subclassing the base class and + providing a custom `new_edge_key()` method. + + Examples + -------- + The following each add an additional edge e=(1, 2) to graph G: + + >>> G = nx.MultiGraph() + >>> e = (1, 2) + >>> ekey = G.add_edge(1, 2) # explicit two-node form + >>> G.add_edge(*e) # single edge as tuple of two nodes + 1 + >>> G.add_edges_from([(1, 2)]) # add edges from iterable container + [2] + + Associate data to edges using keywords: + + >>> ekey = G.add_edge(1, 2, weight=3) + >>> ekey = G.add_edge(1, 2, key=0, weight=4) # update data for key=0 + >>> ekey = G.add_edge(1, 3, weight=7, capacity=15, length=342.7) + + For non-string attribute keys, use subscript notation. + + >>> ekey = G.add_edge(1, 2) + >>> G[1][2][0].update({0: 5}) + >>> G.edges[1, 2, 0].update({0: 5}) + """ + u, v = u_for_edge, v_for_edge + # add nodes + if u not in self._adj: + if u is None: + raise ValueError("None cannot be a node") + self._adj[u] = self.adjlist_inner_dict_factory() + self._node[u] = self.node_attr_dict_factory() + if v not in self._adj: + if v is None: + raise ValueError("None cannot be a node") + self._adj[v] = self.adjlist_inner_dict_factory() + self._node[v] = self.node_attr_dict_factory() + if key is None: + key = self.new_edge_key(u, v) + if v in self._adj[u]: + keydict = self._adj[u][v] + datadict = keydict.get(key, self.edge_attr_dict_factory()) + datadict.update(attr) + keydict[key] = datadict + else: + # selfloops work this way without special treatment + datadict = self.edge_attr_dict_factory() + datadict.update(attr) + keydict = self.edge_key_dict_factory() + keydict[key] = datadict + self._adj[u][v] = keydict + self._adj[v][u] = keydict + nx._clear_cache(self) + return key + + def add_edges_from(self, ebunch_to_add, **attr): + """Add all the edges in ebunch_to_add. + + Parameters + ---------- + ebunch_to_add : container of edges + Each edge given in the container will be added to the + graph. The edges can be: + + - 2-tuples (u, v) or + - 3-tuples (u, v, d) for an edge data dict d, or + - 3-tuples (u, v, k) for not iterable key k, or + - 4-tuples (u, v, k, d) for an edge with data and key k + + attr : keyword arguments, optional + Edge data (or labels or objects) can be assigned using + keyword arguments. + + Returns + ------- + A list of edge keys assigned to the edges in `ebunch`. + + See Also + -------- + add_edge : add a single edge + add_weighted_edges_from : convenient way to add weighted edges + + Notes + ----- + Adding the same edge twice has no effect but any edge data + will be updated when each duplicate edge is added. + + Edge attributes specified in an ebunch take precedence over + attributes specified via keyword arguments. + + Default keys are generated using the method ``new_edge_key()``. + This method can be overridden by subclassing the base class and + providing a custom ``new_edge_key()`` method. + + When adding edges from an iterator over the graph you are changing, + a `RuntimeError` can be raised with message: + `RuntimeError: dictionary changed size during iteration`. This + happens when the graph's underlying dictionary is modified during + iteration. To avoid this error, evaluate the iterator into a separate + object, e.g. by using `list(iterator_of_edges)`, and pass this + object to `G.add_edges_from`. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> G.add_edges_from([(0, 1), (1, 2)]) # using a list of edge tuples + >>> e = zip(range(0, 3), range(1, 4)) + >>> G.add_edges_from(e) # Add the path graph 0-1-2-3 + + Associate data to edges + + >>> G.add_edges_from([(1, 2), (2, 3)], weight=3) + >>> G.add_edges_from([(3, 4), (1, 4)], label="WN2898") + + Evaluate an iterator over a graph if using it to modify the same graph + + >>> G = nx.MultiGraph([(1, 2), (2, 3), (3, 4)]) + >>> # Grow graph by one new node, adding edges to all existing nodes. + >>> # wrong way - will raise RuntimeError + >>> # G.add_edges_from(((5, n) for n in G.nodes)) + >>> # right way - note that there will be no self-edge for node 5 + >>> assigned_keys = G.add_edges_from(list((5, n) for n in G.nodes)) + """ + keylist = [] + for e in ebunch_to_add: + ne = len(e) + if ne == 4: + u, v, key, dd = e + elif ne == 3: + u, v, dd = e + key = None + elif ne == 2: + u, v = e + dd = {} + key = None + else: + msg = f"Edge tuple {e} must be a 2-tuple, 3-tuple or 4-tuple." + raise NetworkXError(msg) + ddd = {} + ddd.update(attr) + try: + ddd.update(dd) + except (TypeError, ValueError): + if ne != 3: + raise + key = dd # ne == 3 with 3rd value not dict, must be a key + key = self.add_edge(u, v, key) + self[u][v][key].update(ddd) + keylist.append(key) + nx._clear_cache(self) + return keylist + + def remove_edge(self, u, v, key=None): + """Remove an edge between u and v. + + Parameters + ---------- + u, v : nodes + Remove an edge between nodes u and v. + key : hashable identifier, optional (default=None) + Used to distinguish multiple edges between a pair of nodes. + If None, remove a single edge between u and v. If there are + multiple edges, removes the last edge added in terms of + insertion order. + + Raises + ------ + NetworkXError + If there is not an edge between u and v, or + if there is no edge with the specified key. + + See Also + -------- + remove_edges_from : remove a collection of edges + + Examples + -------- + >>> G = nx.MultiGraph() + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.remove_edge(0, 1) + >>> e = (1, 2) + >>> G.remove_edge(*e) # unpacks e from an edge tuple + + For multiple edges + + >>> G = nx.MultiGraph() # or MultiDiGraph, etc + >>> G.add_edges_from([(1, 2), (1, 2), (1, 2)]) # key_list returned + [0, 1, 2] + + When ``key=None`` (the default), edges are removed in the opposite + order that they were added: + + >>> G.remove_edge(1, 2) + >>> G.edges(keys=True) + MultiEdgeView([(1, 2, 0), (1, 2, 1)]) + >>> G.remove_edge(2, 1) # edges are not directed + >>> G.edges(keys=True) + MultiEdgeView([(1, 2, 0)]) + + For edges with keys + + >>> G = nx.MultiGraph() + >>> G.add_edge(1, 2, key="first") + 'first' + >>> G.add_edge(1, 2, key="second") + 'second' + >>> G.remove_edge(1, 2, key="first") + >>> G.edges(keys=True) + MultiEdgeView([(1, 2, 'second')]) + + """ + try: + d = self._adj[u][v] + except KeyError as err: + raise NetworkXError(f"The edge {u}-{v} is not in the graph.") from err + # remove the edge with specified data + if key is None: + d.popitem() + else: + try: + del d[key] + except KeyError as err: + msg = f"The edge {u}-{v} with key {key} is not in the graph." + raise NetworkXError(msg) from err + if len(d) == 0: + # remove the key entries if last edge + del self._adj[u][v] + if u != v: # check for selfloop + del self._adj[v][u] + nx._clear_cache(self) + + def remove_edges_from(self, ebunch): + """Remove all edges specified in ebunch. + + Parameters + ---------- + ebunch: list or container of edge tuples + Each edge given in the list or container will be removed + from the graph. The edges can be: + + - 2-tuples (u, v) A single edge between u and v is removed. + - 3-tuples (u, v, key) The edge identified by key is removed. + - 4-tuples (u, v, key, data) where data is ignored. + + See Also + -------- + remove_edge : remove a single edge + + Notes + ----- + Will fail silently if an edge in ebunch is not in the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> ebunch = [(1, 2), (2, 3)] + >>> G.remove_edges_from(ebunch) + + Removing multiple copies of edges + + >>> G = nx.MultiGraph() + >>> keys = G.add_edges_from([(1, 2), (1, 2), (1, 2)]) + >>> G.remove_edges_from([(1, 2), (2, 1)]) # edges aren't directed + >>> list(G.edges()) + [(1, 2)] + >>> G.remove_edges_from([(1, 2), (1, 2)]) # silently ignore extra copy + >>> list(G.edges) # now empty graph + [] + + When the edge is a 2-tuple ``(u, v)`` but there are multiple edges between + u and v in the graph, the most recent edge (in terms of insertion + order) is removed. + + >>> G = nx.MultiGraph() + >>> for key in ("x", "y", "a"): + ... k = G.add_edge(0, 1, key=key) + >>> G.edges(keys=True) + MultiEdgeView([(0, 1, 'x'), (0, 1, 'y'), (0, 1, 'a')]) + >>> G.remove_edges_from([(0, 1)]) + >>> G.edges(keys=True) + MultiEdgeView([(0, 1, 'x'), (0, 1, 'y')]) + + """ + for e in ebunch: + try: + self.remove_edge(*e[:3]) + except NetworkXError: + pass + nx._clear_cache(self) + + def has_edge(self, u, v, key=None): + """Returns True if the graph has an edge between nodes u and v. + + This is the same as `v in G[u] or key in G[u][v]` + without KeyError exceptions. + + Parameters + ---------- + u, v : nodes + Nodes can be, for example, strings or numbers. + + key : hashable identifier, optional (default=None) + If specified return True only if the edge with + key is found. + + Returns + ------- + edge_ind : bool + True if edge is in the graph, False otherwise. + + Examples + -------- + Can be called either using two nodes u, v, an edge tuple (u, v), + or an edge tuple (u, v, key). + + >>> G = nx.MultiGraph() # or MultiDiGraph + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.has_edge(0, 1) # using two nodes + True + >>> e = (0, 1) + >>> G.has_edge(*e) # e is a 2-tuple (u, v) + True + >>> G.add_edge(0, 1, key="a") + 'a' + >>> G.has_edge(0, 1, key="a") # specify key + True + >>> G.has_edge(1, 0, key="a") # edges aren't directed + True + >>> e = (0, 1, "a") + >>> G.has_edge(*e) # e is a 3-tuple (u, v, 'a') + True + + The following syntax are equivalent: + + >>> G.has_edge(0, 1) + True + >>> 1 in G[0] # though this gives :exc:`KeyError` if 0 not in G + True + >>> 0 in G[1] # other order; also gives :exc:`KeyError` if 0 not in G + True + + """ + try: + if key is None: + return v in self._adj[u] + else: + return key in self._adj[u][v] + except KeyError: + return False + + @cached_property + def edges(self): + """Returns an iterator over the edges. + + edges(self, nbunch=None, data=False, keys=False, default=None) + + The MultiEdgeView provides set-like operations on the edge-tuples + as well as edge attribute lookup. When called, it also provides + an EdgeDataView object which allows control of access to edge + attributes (but does not provide set-like operations). + Hence, ``G.edges[u, v, k]['color']`` provides the value of the color + attribute for the edge from ``u`` to ``v`` with key ``k`` while + ``for (u, v, k, c) in G.edges(data='color', keys=True, default="red"):`` + iterates through all the edges yielding the color attribute with + default `'red'` if no color attribute exists. + + Edges are returned as tuples with optional data and keys + in the order (node, neighbor, key, data). If ``keys=True`` is not + provided, the tuples will just be (node, neighbor, data), but + multiple tuples with the same node and neighbor will be generated + when multiple edges exist between two nodes. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges from these nodes. + data : string or bool, optional (default=False) + The edge attribute returned in 3-tuple (u, v, ddict[data]). + If True, return edge attribute dict in 3-tuple (u, v, ddict). + If False, return 2-tuple (u, v). + keys : bool, optional (default=False) + If True, return edge keys with each edge, creating (u, v, k) + tuples or (u, v, k, d) tuples if data is also requested. + default : value, optional (default=None) + Value used for edges that don't have the requested attribute. + Only relevant if data is not True or False. + + Returns + ------- + edges : MultiEdgeView + A view of edge attributes, usually it iterates over (u, v) + (u, v, k) or (u, v, k, d) tuples of edges, but can also be + used for attribute lookup as ``edges[u, v, k]['foo']``. + + Notes + ----- + Nodes in nbunch that are not in the graph will be (quietly) ignored. + For directed graphs this returns the out-edges. + + Examples + -------- + >>> G = nx.MultiGraph() + >>> nx.add_path(G, [0, 1, 2]) + >>> key = G.add_edge(2, 3, weight=5) + >>> key2 = G.add_edge(2, 1, weight=2) # multi-edge + >>> [e for e in G.edges()] + [(0, 1), (1, 2), (1, 2), (2, 3)] + >>> G.edges.data() # default data is {} (empty dict) + MultiEdgeDataView([(0, 1, {}), (1, 2, {}), (1, 2, {'weight': 2}), (2, 3, {'weight': 5})]) + >>> G.edges.data("weight", default=1) + MultiEdgeDataView([(0, 1, 1), (1, 2, 1), (1, 2, 2), (2, 3, 5)]) + >>> G.edges(keys=True) # default keys are integers + MultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 1), (2, 3, 0)]) + >>> G.edges.data(keys=True) + MultiEdgeDataView([(0, 1, 0, {}), (1, 2, 0, {}), (1, 2, 1, {'weight': 2}), (2, 3, 0, {'weight': 5})]) + >>> G.edges.data("weight", default=1, keys=True) + MultiEdgeDataView([(0, 1, 0, 1), (1, 2, 0, 1), (1, 2, 1, 2), (2, 3, 0, 5)]) + >>> G.edges([0, 3]) # Note ordering of tuples from listed sources + MultiEdgeDataView([(0, 1), (3, 2)]) + >>> G.edges([0, 3, 2, 1]) # Note ordering of tuples + MultiEdgeDataView([(0, 1), (3, 2), (2, 1), (2, 1)]) + >>> G.edges(0) + MultiEdgeDataView([(0, 1)]) + """ + return MultiEdgeView(self) + + def get_edge_data(self, u, v, key=None, default=None): + """Returns the attribute dictionary associated with edge (u, v, + key). + + If a key is not provided, returns a dictionary mapping edge keys + to attribute dictionaries for each edge between u and v. + + This is identical to `G[u][v][key]` except the default is returned + instead of an exception is the edge doesn't exist. + + Parameters + ---------- + u, v : nodes + + default : any Python object (default=None) + Value to return if the specific edge (u, v, key) is not + found, OR if there are no edges between u and v and no key + is specified. + + key : hashable identifier, optional (default=None) + Return data only for the edge with specified key, as an + attribute dictionary (rather than a dictionary mapping keys + to attribute dictionaries). + + Returns + ------- + edge_dict : dictionary + The edge attribute dictionary, OR a dictionary mapping edge + keys to attribute dictionaries for each of those edges if no + specific key is provided (even if there's only one edge + between u and v). + + Examples + -------- + >>> G = nx.MultiGraph() # or MultiDiGraph + >>> key = G.add_edge(0, 1, key="a", weight=7) + >>> G[0][1]["a"] # key='a' + {'weight': 7} + >>> G.edges[0, 1, "a"] # key='a' + {'weight': 7} + + Warning: we protect the graph data structure by making + `G.edges` and `G[1][2]` read-only dict-like structures. + However, you can assign values to attributes in e.g. + `G.edges[1, 2, 'a']` or `G[1][2]['a']` using an additional + bracket as shown next. You need to specify all edge info + to assign to the edge data associated with an edge. + + >>> G[0][1]["a"]["weight"] = 10 + >>> G.edges[0, 1, "a"]["weight"] = 10 + >>> G[0][1]["a"]["weight"] + 10 + >>> G.edges[1, 0, "a"]["weight"] + 10 + + >>> G = nx.MultiGraph() # or MultiDiGraph + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.edges[0, 1, 0]["weight"] = 5 + >>> G.get_edge_data(0, 1) + {0: {'weight': 5}} + >>> e = (0, 1) + >>> G.get_edge_data(*e) # tuple form + {0: {'weight': 5}} + >>> G.get_edge_data(3, 0) # edge not in graph, returns None + >>> G.get_edge_data(3, 0, default=0) # edge not in graph, return default + 0 + >>> G.get_edge_data(1, 0, 0) # specific key gives back + {'weight': 5} + """ + try: + if key is None: + return self._adj[u][v] + else: + return self._adj[u][v][key] + except KeyError: + return default + + @cached_property + def degree(self): + """A DegreeView for the Graph as G.degree or G.degree(). + + The node degree is the number of edges adjacent to the node. + The weighted node degree is the sum of the edge weights for + edges incident to that node. + + This object provides an iterator for (node, degree) as well as + lookup for the degree for a single node. + + Parameters + ---------- + nbunch : single node, container, or all nodes (default= all nodes) + The view will only report edges incident to these nodes. + + weight : string or None, optional (default=None) + The name of an edge attribute that holds the numerical value used + as a weight. If None, then each edge has weight 1. + The degree is the sum of the edge weights adjacent to the node. + + Returns + ------- + MultiDegreeView or int + If multiple nodes are requested (the default), returns a `MultiDegreeView` + mapping nodes to their degree. + If a single node is requested, returns the degree of the node as an integer. + + Examples + -------- + >>> G = nx.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> nx.add_path(G, [0, 1, 2, 3]) + >>> G.degree(0) # node 0 with degree 1 + 1 + >>> list(G.degree([0, 1])) + [(0, 1), (1, 2)] + + """ + return MultiDegreeView(self) + + def is_multigraph(self): + """Returns True if graph is a multigraph, False otherwise.""" + return True + + def is_directed(self): + """Returns True if graph is directed, False otherwise.""" + return False + + def copy(self, as_view=False): + """Returns a copy of the graph. + + The copy method by default returns an independent shallow copy + of the graph and attributes. That is, if an attribute is a + container, that container is shared by the original an the copy. + Use Python's `copy.deepcopy` for new containers. + + If `as_view` is True then a view is returned instead of a copy. + + Notes + ----- + All copies reproduce the graph structure, but data attributes + may be handled in different ways. There are four types of copies + of a graph that people might want. + + Deepcopy -- A "deepcopy" copies the graph structure as well as + all data attributes and any objects they might contain. + The entire graph object is new so that changes in the copy + do not affect the original object. (see Python's copy.deepcopy) + + Data Reference (Shallow) -- For a shallow copy the graph structure + is copied but the edge, node and graph attribute dicts are + references to those in the original graph. This saves + time and memory but could cause confusion if you change an attribute + in one graph and it changes the attribute in the other. + NetworkX does not provide this level of shallow copy. + + Independent Shallow -- This copy creates new independent attribute + dicts and then does a shallow copy of the attributes. That is, any + attributes that are containers are shared between the new graph + and the original. This is exactly what `dict.copy()` provides. + You can obtain this style copy using: + + >>> G = nx.path_graph(5) + >>> H = G.copy() + >>> H = G.copy(as_view=False) + >>> H = nx.Graph(G) + >>> H = G.__class__(G) + + Fresh Data -- For fresh data, the graph structure is copied while + new empty data attribute dicts are created. The resulting graph + is independent of the original and it has no edge, node or graph + attributes. Fresh copies are not enabled. Instead use: + + >>> H = G.__class__() + >>> H.add_nodes_from(G) + >>> H.add_edges_from(G.edges) + + View -- Inspired by dict-views, graph-views act like read-only + versions of the original graph, providing a copy of the original + structure without requiring any memory for copying the information. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Parameters + ---------- + as_view : bool, optional (default=False) + If True, the returned graph-view provides a read-only view + of the original graph without actually copying any data. + + Returns + ------- + G : Graph + A copy of the graph. + + See Also + -------- + to_directed: return a directed copy of the graph. + + Examples + -------- + >>> G = nx.path_graph(4) # or DiGraph, MultiGraph, MultiDiGraph, etc + >>> H = G.copy() + + """ + if as_view is True: + return nx.graphviews.generic_graph_view(self) + G = self.__class__() + G.graph.update(self.graph) + G.add_nodes_from((n, d.copy()) for n, d in self._node.items()) + G.add_edges_from( + (u, v, key, datadict.copy()) + for u, nbrs in self._adj.items() + for v, keydict in nbrs.items() + for key, datadict in keydict.items() + ) + return G + + def to_directed(self, as_view=False): + """Returns a directed representation of the graph. + + Returns + ------- + G : MultiDiGraph + A directed graph with the same name, same nodes, and with + each edge (u, v, k, data) replaced by two directed edges + (u, v, k, data) and (v, u, k, data). + + Notes + ----- + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar D=MultiDiGraph(G) which + returns a shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed MultiGraph to use dict-like objects + in the data structure, those changes do not transfer to the + MultiDiGraph created by this method. + + Examples + -------- + >>> G = nx.MultiGraph() + >>> G.add_edge(0, 1) + 0 + >>> G.add_edge(0, 1) + 1 + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1)] + + If already directed, return a (deep) copy + + >>> G = nx.MultiDiGraph() + >>> G.add_edge(0, 1) + 0 + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1, 0)] + """ + graph_class = self.to_directed_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + G.add_edges_from( + (u, v, key, deepcopy(datadict)) + for u, nbrs in self.adj.items() + for v, keydict in nbrs.items() + for key, datadict in keydict.items() + ) + return G + + def to_undirected(self, as_view=False): + """Returns an undirected copy of the graph. + + Returns + ------- + G : Graph/MultiGraph + A deepcopy of the graph. + + See Also + -------- + copy, add_edge, add_edges_from + + Notes + ----- + This returns a "deepcopy" of the edge, node, and + graph attributes which attempts to completely copy + all of the data and references. + + This is in contrast to the similar `G = nx.MultiGraph(D)` + which returns a shallow copy of the data. + + See the Python copy module for more information on shallow + and deep copies, https://docs.python.org/3/library/copy.html. + + Warning: If you have subclassed MultiGraph to use dict-like + objects in the data structure, those changes do not transfer + to the MultiGraph created by this method. + + Examples + -------- + >>> G = nx.MultiGraph([(0, 1), (0, 1), (1, 2)]) + >>> H = G.to_directed() + >>> list(H.edges) + [(0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 2, 0), (2, 1, 0)] + >>> G2 = H.to_undirected() + >>> list(G2.edges) + [(0, 1, 0), (0, 1, 1), (1, 2, 0)] + """ + graph_class = self.to_undirected_class() + if as_view is True: + return nx.graphviews.generic_graph_view(self, graph_class) + # deepcopy when not a view + G = graph_class() + G.graph.update(deepcopy(self.graph)) + G.add_nodes_from((n, deepcopy(d)) for n, d in self._node.items()) + G.add_edges_from( + (u, v, key, deepcopy(datadict)) + for u, nbrs in self._adj.items() + for v, keydict in nbrs.items() + for key, datadict in keydict.items() + ) + return G + + def number_of_edges(self, u=None, v=None): + """Returns the number of edges between two nodes. + + Parameters + ---------- + u, v : nodes, optional (Default=all edges) + If u and v are specified, return the number of edges between + u and v. Otherwise return the total number of all edges. + + Returns + ------- + nedges : int + The number of edges in the graph. If nodes `u` and `v` are + specified return the number of edges between those nodes. If + the graph is directed, this only returns the number of edges + from `u` to `v`. + + See Also + -------- + size + + Examples + -------- + For undirected multigraphs, this method counts the total number + of edges in the graph:: + + >>> G = nx.MultiGraph() + >>> G.add_edges_from([(0, 1), (0, 1), (1, 2)]) + [0, 1, 0] + >>> G.number_of_edges() + 3 + + If you specify two nodes, this counts the total number of edges + joining the two nodes:: + + >>> G.number_of_edges(0, 1) + 2 + + For directed multigraphs, this method can count the total number + of directed edges from `u` to `v`:: + + >>> G = nx.MultiDiGraph() + >>> G.add_edges_from([(0, 1), (0, 1), (1, 0)]) + [0, 1, 0] + >>> G.number_of_edges(0, 1) + 2 + >>> G.number_of_edges(1, 0) + 1 + + """ + if u is None: + return self.size() + try: + edgedata = self._adj[u][v] + except KeyError: + return 0 # no such edge + return len(edgedata) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/reportviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/reportviews.py new file mode 100644 index 0000000000000000000000000000000000000000..789662de19600ec2a7922db612c525dfb75695ea --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/reportviews.py @@ -0,0 +1,1447 @@ +""" +View Classes provide node, edge and degree "views" of a graph. + +Views for nodes, edges and degree are provided for all base graph classes. +A view means a read-only object that is quick to create, automatically +updated when the graph changes, and provides basic access like `n in V`, +`for n in V`, `V[n]` and sometimes set operations. + +The views are read-only iterable containers that are updated as the +graph is updated. As with dicts, the graph should not be updated +while iterating through the view. Views can be iterated multiple times. + +Edge and Node views also allow data attribute lookup. +The resulting attribute dict is writable as `G.edges[3, 4]['color']='red'` +Degree views allow lookup of degree values for single nodes. +Weighted degree is supported with the `weight` argument. + +NodeView +======== + + `V = G.nodes` (or `V = G.nodes()`) allows `len(V)`, `n in V`, set + operations e.g. "G.nodes & H.nodes", and `dd = G.nodes[n]`, where + `dd` is the node data dict. Iteration is over the nodes by default. + +NodeDataView +============ + + To iterate over (node, data) pairs, use arguments to `G.nodes()` + to create a DataView e.g. `DV = G.nodes(data='color', default='red')`. + The DataView iterates as `for n, color in DV` and allows + `(n, 'red') in DV`. Using `DV = G.nodes(data=True)`, the DataViews + use the full datadict in writeable form also allowing contain testing as + `(n, {'color': 'red'}) in VD`. DataViews allow set operations when + data attributes are hashable. + +DegreeView +========== + + `V = G.degree` allows iteration over (node, degree) pairs as well + as lookup: `deg=V[n]`. There are many flavors of DegreeView + for In/Out/Directed/Multi. For Directed Graphs, `G.degree` + counts both in and out going edges. `G.out_degree` and + `G.in_degree` count only specific directions. + Weighted degree using edge data attributes is provide via + `V = G.degree(weight='attr_name')` where any string with the + attribute name can be used. `weight=None` is the default. + No set operations are implemented for degrees, use NodeView. + + The argument `nbunch` restricts iteration to nodes in nbunch. + The DegreeView can still lookup any node even if nbunch is specified. + +EdgeView +======== + + `V = G.edges` or `V = G.edges()` allows iteration over edges as well as + `e in V`, set operations and edge data lookup `dd = G.edges[2, 3]`. + Iteration is over 2-tuples `(u, v)` for Graph/DiGraph. For multigraphs + edges 3-tuples `(u, v, key)` are the default but 2-tuples can be obtained + via `V = G.edges(keys=False)`. + + Set operations for directed graphs treat the edges as a set of 2-tuples. + For undirected graphs, 2-tuples are not a unique representation of edges. + So long as the set being compared to contains unique representations + of its edges, the set operations will act as expected. If the other + set contains both `(0, 1)` and `(1, 0)` however, the result of set + operations may contain both representations of the same edge. + +EdgeDataView +============ + + Edge data can be reported using an EdgeDataView typically created + by calling an EdgeView: `DV = G.edges(data='weight', default=1)`. + The EdgeDataView allows iteration over edge tuples, membership checking + but no set operations. + + Iteration depends on `data` and `default` and for multigraph `keys` + If `data is False` (the default) then iterate over 2-tuples `(u, v)`. + If `data is True` iterate over 3-tuples `(u, v, datadict)`. + Otherwise iterate over `(u, v, datadict.get(data, default))`. + For Multigraphs, if `keys is True`, replace `u, v` with `u, v, key` + to create 3-tuples and 4-tuples. + + The argument `nbunch` restricts edges to those incident to nodes in nbunch. +""" + +from abc import ABC +from collections.abc import Mapping, Set + +import networkx as nx + +__all__ = [ + "NodeView", + "NodeDataView", + "EdgeView", + "OutEdgeView", + "InEdgeView", + "EdgeDataView", + "OutEdgeDataView", + "InEdgeDataView", + "MultiEdgeView", + "OutMultiEdgeView", + "InMultiEdgeView", + "MultiEdgeDataView", + "OutMultiEdgeDataView", + "InMultiEdgeDataView", + "DegreeView", + "DiDegreeView", + "InDegreeView", + "OutDegreeView", + "MultiDegreeView", + "DiMultiDegreeView", + "InMultiDegreeView", + "OutMultiDegreeView", +] + + +# NodeViews +class NodeView(Mapping, Set): + """A NodeView class to act as G.nodes for a NetworkX Graph + + Set operations act on the nodes without considering data. + Iteration is over nodes. Node data can be looked up like a dict. + Use NodeDataView to iterate over node data or to specify a data + attribute for lookup. NodeDataView is created by calling the NodeView. + + Parameters + ---------- + graph : NetworkX graph-like class + + Examples + -------- + >>> G = nx.path_graph(3) + >>> NV = G.nodes() + >>> 2 in NV + True + >>> for n in NV: + ... print(n) + 0 + 1 + 2 + >>> assert NV & {1, 2, 3} == {1, 2} + + >>> G.add_node(2, color="blue") + >>> NV[2] + {'color': 'blue'} + >>> G.add_node(8, color="red") + >>> NDV = G.nodes(data=True) + >>> (2, NV[2]) in NDV + True + >>> for n, dd in NDV: + ... print((n, dd.get("color", "aqua"))) + (0, 'aqua') + (1, 'aqua') + (2, 'blue') + (8, 'red') + >>> NDV[2] == NV[2] + True + + >>> NVdata = G.nodes(data="color", default="aqua") + >>> (2, NVdata[2]) in NVdata + True + >>> for n, dd in NVdata: + ... print((n, dd)) + (0, 'aqua') + (1, 'aqua') + (2, 'blue') + (8, 'red') + >>> NVdata[2] == NV[2] # NVdata gets 'color', NV gets datadict + False + """ + + __slots__ = ("_nodes",) + + def __getstate__(self): + return {"_nodes": self._nodes} + + def __setstate__(self, state): + self._nodes = state["_nodes"] + + def __init__(self, graph): + self._nodes = graph._node + + # Mapping methods + def __len__(self): + return len(self._nodes) + + def __iter__(self): + return iter(self._nodes) + + def __getitem__(self, n): + if isinstance(n, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.nodes)[{n.start}:{n.stop}:{n.step}]" + ) + return self._nodes[n] + + # Set methods + def __contains__(self, n): + return n in self._nodes + + @classmethod + def _from_iterable(cls, it): + return set(it) + + # DataView method + def __call__(self, data=False, default=None): + if data is False: + return self + return NodeDataView(self._nodes, data, default) + + def data(self, data=True, default=None): + """ + Return a read-only view of node data. + + Parameters + ---------- + data : bool or node data key, default=True + If ``data=True`` (the default), return a `NodeDataView` object that + maps each node to *all* of its attributes. `data` may also be an + arbitrary key, in which case the `NodeDataView` maps each node to + the value for the keyed attribute. In this case, if a node does + not have the `data` attribute, the `default` value is used. + default : object, default=None + The value used when a node does not have a specific attribute. + + Returns + ------- + NodeDataView + The layout of the returned NodeDataView depends on the value of the + `data` parameter. + + Notes + ----- + If ``data=False``, returns a `NodeView` object without data. + + See Also + -------- + NodeDataView + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_nodes_from( + ... [ + ... (0, {"color": "red", "weight": 10}), + ... (1, {"color": "blue"}), + ... (2, {"color": "yellow", "weight": 2}), + ... ] + ... ) + + Accessing node data with ``data=True`` (the default) returns a + NodeDataView mapping each node to all of its attributes: + + >>> G.nodes.data() + NodeDataView({0: {'color': 'red', 'weight': 10}, 1: {'color': 'blue'}, 2: {'color': 'yellow', 'weight': 2}}) + + If `data` represents a key in the node attribute dict, a NodeDataView mapping + the nodes to the value for that specific key is returned: + + >>> G.nodes.data("color") + NodeDataView({0: 'red', 1: 'blue', 2: 'yellow'}, data='color') + + If a specific key is not found in an attribute dict, the value specified + by `default` is returned: + + >>> G.nodes.data("weight", default=-999) + NodeDataView({0: 10, 1: -999, 2: 2}, data='weight') + + Note that there is no check that the `data` key is in any of the + node attribute dictionaries: + + >>> G.nodes.data("height") + NodeDataView({0: None, 1: None, 2: None}, data='height') + """ + if data is False: + return self + return NodeDataView(self._nodes, data, default) + + def __str__(self): + return str(list(self)) + + def __repr__(self): + return f"{self.__class__.__name__}({tuple(self)})" + + +class NodeDataView(Set): + """A DataView class for nodes of a NetworkX Graph + + The main use for this class is to iterate through node-data pairs. + The data can be the entire data-dictionary for each node, or it + can be a specific attribute (with default) for each node. + Set operations are enabled with NodeDataView, but don't work in + cases where the data is not hashable. Use with caution. + Typically, set operations on nodes use NodeView, not NodeDataView. + That is, they use `G.nodes` instead of `G.nodes(data='foo')`. + + Parameters + ========== + graph : NetworkX graph-like class + data : bool or string (default=False) + default : object (default=None) + """ + + __slots__ = ("_nodes", "_data", "_default") + + def __getstate__(self): + return {"_nodes": self._nodes, "_data": self._data, "_default": self._default} + + def __setstate__(self, state): + self._nodes = state["_nodes"] + self._data = state["_data"] + self._default = state["_default"] + + def __init__(self, nodedict, data=False, default=None): + self._nodes = nodedict + self._data = data + self._default = default + + @classmethod + def _from_iterable(cls, it): + try: + return set(it) + except TypeError as err: + if "unhashable" in str(err): + msg = " : Could be b/c data=True or your values are unhashable" + raise TypeError(str(err) + msg) from err + raise + + def __len__(self): + return len(self._nodes) + + def __iter__(self): + data = self._data + if data is False: + return iter(self._nodes) + if data is True: + return iter(self._nodes.items()) + return ( + (n, dd[data] if data in dd else self._default) + for n, dd in self._nodes.items() + ) + + def __contains__(self, n): + try: + node_in = n in self._nodes + except TypeError: + n, d = n + return n in self._nodes and self[n] == d + if node_in is True: + return node_in + try: + n, d = n + except (TypeError, ValueError): + return False + return n in self._nodes and self[n] == d + + def __getitem__(self, n): + if isinstance(n, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.nodes.data())[{n.start}:{n.stop}:{n.step}]" + ) + ddict = self._nodes[n] + data = self._data + if data is False or data is True: + return ddict + return ddict[data] if data in ddict else self._default + + def __str__(self): + return str(list(self)) + + def __repr__(self): + name = self.__class__.__name__ + if self._data is False: + return f"{name}({tuple(self)})" + if self._data is True: + return f"{name}({dict(self)})" + return f"{name}({dict(self)}, data={self._data!r})" + + +# DegreeViews +class DiDegreeView: + """A View class for degree of nodes in a NetworkX Graph + + The functionality is like dict.items() with (node, degree) pairs. + Additional functionality includes read-only lookup of node degree, + and calling with optional features nbunch (for only a subset of nodes) + and weight (use edge weights to compute degree). + + Parameters + ========== + graph : NetworkX graph-like class + nbunch : node, container of nodes, or None meaning all nodes (default=None) + weight : bool or string (default=None) + + Notes + ----- + DegreeView can still lookup any node even if nbunch is specified. + + Examples + -------- + >>> G = nx.path_graph(3) + >>> DV = G.degree() + >>> assert DV[2] == 1 + >>> assert sum(deg for n, deg in DV) == 4 + + >>> DVweight = G.degree(weight="span") + >>> G.add_edge(1, 2, span=34) + >>> DVweight[2] + 34 + >>> DVweight[0] # default edge weight is 1 + 1 + >>> sum(span for n, span in DVweight) # sum weighted degrees + 70 + + >>> DVnbunch = G.degree(nbunch=(1, 2)) + >>> assert len(list(DVnbunch)) == 2 # iteration over nbunch only + """ + + def __init__(self, G, nbunch=None, weight=None): + self._graph = G + self._succ = G._succ if hasattr(G, "_succ") else G._adj + self._pred = G._pred if hasattr(G, "_pred") else G._adj + self._nodes = self._succ if nbunch is None else list(G.nbunch_iter(nbunch)) + self._weight = weight + + def __call__(self, nbunch=None, weight=None): + if nbunch is None: + if weight == self._weight: + return self + return self.__class__(self._graph, None, weight) + try: + if nbunch in self._nodes: + if weight == self._weight: + return self[nbunch] + return self.__class__(self._graph, None, weight)[nbunch] + except TypeError: + pass + return self.__class__(self._graph, nbunch, weight) + + def __getitem__(self, n): + weight = self._weight + succs = self._succ[n] + preds = self._pred[n] + if weight is None: + return len(succs) + len(preds) + return sum(dd.get(weight, 1) for dd in succs.values()) + sum( + dd.get(weight, 1) for dd in preds.values() + ) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + succs = self._succ[n] + preds = self._pred[n] + yield (n, len(succs) + len(preds)) + else: + for n in self._nodes: + succs = self._succ[n] + preds = self._pred[n] + deg = sum(dd.get(weight, 1) for dd in succs.values()) + sum( + dd.get(weight, 1) for dd in preds.values() + ) + yield (n, deg) + + def __len__(self): + return len(self._nodes) + + def __str__(self): + return str(list(self)) + + def __repr__(self): + return f"{self.__class__.__name__}({dict(self)})" + + +class DegreeView(DiDegreeView): + """A DegreeView class to act as G.degree for a NetworkX Graph + + Typical usage focuses on iteration over `(node, degree)` pairs. + The degree is by default the number of edges incident to the node. + Optional argument `weight` enables weighted degree using the edge + attribute named in the `weight` argument. Reporting and iteration + can also be restricted to a subset of nodes using `nbunch`. + + Additional functionality include node lookup so that `G.degree[n]` + reported the (possibly weighted) degree of node `n`. Calling the + view creates a view with different arguments `nbunch` or `weight`. + + Parameters + ========== + graph : NetworkX graph-like class + nbunch : node, container of nodes, or None meaning all nodes (default=None) + weight : string or None (default=None) + + Notes + ----- + DegreeView can still lookup any node even if nbunch is specified. + + Examples + -------- + >>> G = nx.path_graph(3) + >>> DV = G.degree() + >>> assert DV[2] == 1 + >>> assert G.degree[2] == 1 + >>> assert sum(deg for n, deg in DV) == 4 + + >>> DVweight = G.degree(weight="span") + >>> G.add_edge(1, 2, span=34) + >>> DVweight[2] + 34 + >>> DVweight[0] # default edge weight is 1 + 1 + >>> sum(span for n, span in DVweight) # sum weighted degrees + 70 + + >>> DVnbunch = G.degree(nbunch=(1, 2)) + >>> assert len(list(DVnbunch)) == 2 # iteration over nbunch only + """ + + def __getitem__(self, n): + weight = self._weight + nbrs = self._succ[n] + if weight is None: + return len(nbrs) + (n in nbrs) + return sum(dd.get(weight, 1) for dd in nbrs.values()) + ( + n in nbrs and nbrs[n].get(weight, 1) + ) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + nbrs = self._succ[n] + yield (n, len(nbrs) + (n in nbrs)) + else: + for n in self._nodes: + nbrs = self._succ[n] + deg = sum(dd.get(weight, 1) for dd in nbrs.values()) + ( + n in nbrs and nbrs[n].get(weight, 1) + ) + yield (n, deg) + + +class OutDegreeView(DiDegreeView): + """A DegreeView class to report out_degree for a DiGraph; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + nbrs = self._succ[n] + if self._weight is None: + return len(nbrs) + return sum(dd.get(self._weight, 1) for dd in nbrs.values()) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + succs = self._succ[n] + yield (n, len(succs)) + else: + for n in self._nodes: + succs = self._succ[n] + deg = sum(dd.get(weight, 1) for dd in succs.values()) + yield (n, deg) + + +class InDegreeView(DiDegreeView): + """A DegreeView class to report in_degree for a DiGraph; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + nbrs = self._pred[n] + if weight is None: + return len(nbrs) + return sum(dd.get(weight, 1) for dd in nbrs.values()) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + preds = self._pred[n] + yield (n, len(preds)) + else: + for n in self._nodes: + preds = self._pred[n] + deg = sum(dd.get(weight, 1) for dd in preds.values()) + yield (n, deg) + + +class MultiDegreeView(DiDegreeView): + """A DegreeView class for undirected multigraphs; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + nbrs = self._succ[n] + if weight is None: + return sum(len(keys) for keys in nbrs.values()) + ( + n in nbrs and len(nbrs[n]) + ) + # edge weighted graph - degree is sum of nbr edge weights + deg = sum( + d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values() + ) + if n in nbrs: + deg += sum(d.get(weight, 1) for d in nbrs[n].values()) + return deg + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + nbrs = self._succ[n] + deg = sum(len(keys) for keys in nbrs.values()) + ( + n in nbrs and len(nbrs[n]) + ) + yield (n, deg) + else: + for n in self._nodes: + nbrs = self._succ[n] + deg = sum( + d.get(weight, 1) + for key_dict in nbrs.values() + for d in key_dict.values() + ) + if n in nbrs: + deg += sum(d.get(weight, 1) for d in nbrs[n].values()) + yield (n, deg) + + +class DiMultiDegreeView(DiDegreeView): + """A DegreeView class for MultiDiGraph; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + succs = self._succ[n] + preds = self._pred[n] + if weight is None: + return sum(len(keys) for keys in succs.values()) + sum( + len(keys) for keys in preds.values() + ) + # edge weighted graph - degree is sum of nbr edge weights + deg = sum( + d.get(weight, 1) for key_dict in succs.values() for d in key_dict.values() + ) + sum( + d.get(weight, 1) for key_dict in preds.values() for d in key_dict.values() + ) + return deg + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + succs = self._succ[n] + preds = self._pred[n] + deg = sum(len(keys) for keys in succs.values()) + sum( + len(keys) for keys in preds.values() + ) + yield (n, deg) + else: + for n in self._nodes: + succs = self._succ[n] + preds = self._pred[n] + deg = sum( + d.get(weight, 1) + for key_dict in succs.values() + for d in key_dict.values() + ) + sum( + d.get(weight, 1) + for key_dict in preds.values() + for d in key_dict.values() + ) + yield (n, deg) + + +class InMultiDegreeView(DiDegreeView): + """A DegreeView class for inward degree of MultiDiGraph; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + nbrs = self._pred[n] + if weight is None: + return sum(len(data) for data in nbrs.values()) + # edge weighted graph - degree is sum of nbr edge weights + return sum( + d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values() + ) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + nbrs = self._pred[n] + deg = sum(len(data) for data in nbrs.values()) + yield (n, deg) + else: + for n in self._nodes: + nbrs = self._pred[n] + deg = sum( + d.get(weight, 1) + for key_dict in nbrs.values() + for d in key_dict.values() + ) + yield (n, deg) + + +class OutMultiDegreeView(DiDegreeView): + """A DegreeView class for outward degree of MultiDiGraph; See DegreeView""" + + def __getitem__(self, n): + weight = self._weight + nbrs = self._succ[n] + if weight is None: + return sum(len(data) for data in nbrs.values()) + # edge weighted graph - degree is sum of nbr edge weights + return sum( + d.get(weight, 1) for key_dict in nbrs.values() for d in key_dict.values() + ) + + def __iter__(self): + weight = self._weight + if weight is None: + for n in self._nodes: + nbrs = self._succ[n] + deg = sum(len(data) for data in nbrs.values()) + yield (n, deg) + else: + for n in self._nodes: + nbrs = self._succ[n] + deg = sum( + d.get(weight, 1) + for key_dict in nbrs.values() + for d in key_dict.values() + ) + yield (n, deg) + + +# A base class for all edge views. Ensures all edge view and edge data view +# objects/classes are captured by `isinstance(obj, EdgeViewABC)` and +# `issubclass(cls, EdgeViewABC)` respectively +class EdgeViewABC(ABC): + pass + + +# EdgeDataViews +class OutEdgeDataView(EdgeViewABC): + """EdgeDataView for outward edges of DiGraph; See EdgeDataView""" + + __slots__ = ( + "_viewer", + "_nbunch", + "_data", + "_default", + "_adjdict", + "_nodes_nbrs", + "_report", + ) + + def __getstate__(self): + return { + "viewer": self._viewer, + "nbunch": self._nbunch, + "data": self._data, + "default": self._default, + } + + def __setstate__(self, state): + self.__init__(**state) + + def __init__(self, viewer, nbunch=None, data=False, *, default=None): + self._viewer = viewer + adjdict = self._adjdict = viewer._adjdict + if nbunch is None: + self._nodes_nbrs = adjdict.items + else: + # dict retains order of nodes but acts like a set + nbunch = dict.fromkeys(viewer._graph.nbunch_iter(nbunch)) + self._nodes_nbrs = lambda: [(n, adjdict[n]) for n in nbunch] + self._nbunch = nbunch + self._data = data + self._default = default + # Set _report based on data and default + if data is True: + self._report = lambda n, nbr, dd: (n, nbr, dd) + elif data is False: + self._report = lambda n, nbr, dd: (n, nbr) + else: # data is attribute name + self._report = ( + lambda n, nbr, dd: (n, nbr, dd[data]) + if data in dd + else (n, nbr, default) + ) + + def __len__(self): + return sum(len(nbrs) for n, nbrs in self._nodes_nbrs()) + + def __iter__(self): + return ( + self._report(n, nbr, dd) + for n, nbrs in self._nodes_nbrs() + for nbr, dd in nbrs.items() + ) + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and u not in self._nbunch: + return False # this edge doesn't start in nbunch + try: + ddict = self._adjdict[u][v] + except KeyError: + return False + return e == self._report(u, v, ddict) + + def __str__(self): + return str(list(self)) + + def __repr__(self): + return f"{self.__class__.__name__}({list(self)})" + + +class EdgeDataView(OutEdgeDataView): + """A EdgeDataView class for edges of Graph + + This view is primarily used to iterate over the edges reporting + edges as node-tuples with edge data optionally reported. The + argument `nbunch` allows restriction to edges incident to nodes + in that container/singleton. The default (nbunch=None) + reports all edges. The arguments `data` and `default` control + what edge data is reported. The default `data is False` reports + only node-tuples for each edge. If `data is True` the entire edge + data dict is returned. Otherwise `data` is assumed to hold the name + of the edge attribute to report with default `default` if that + edge attribute is not present. + + Parameters + ---------- + nbunch : container of nodes, node or None (default None) + data : False, True or string (default False) + default : default value (default None) + + Examples + -------- + >>> G = nx.path_graph(3) + >>> G.add_edge(1, 2, foo="bar") + >>> list(G.edges(data="foo", default="biz")) + [(0, 1, 'biz'), (1, 2, 'bar')] + >>> assert (0, 1, "biz") in G.edges(data="foo", default="biz") + """ + + __slots__ = () + + def __len__(self): + return sum(1 for e in self) + + def __iter__(self): + seen = {} + for n, nbrs in self._nodes_nbrs(): + for nbr, dd in nbrs.items(): + if nbr not in seen: + yield self._report(n, nbr, dd) + seen[n] = 1 + del seen + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and u not in self._nbunch and v not in self._nbunch: + return False # this edge doesn't start and it doesn't end in nbunch + try: + ddict = self._adjdict[u][v] + except KeyError: + return False + return e == self._report(u, v, ddict) + + +class InEdgeDataView(OutEdgeDataView): + """An EdgeDataView class for outward edges of DiGraph; See EdgeDataView""" + + __slots__ = () + + def __iter__(self): + return ( + self._report(nbr, n, dd) + for n, nbrs in self._nodes_nbrs() + for nbr, dd in nbrs.items() + ) + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and v not in self._nbunch: + return False # this edge doesn't end in nbunch + try: + ddict = self._adjdict[v][u] + except KeyError: + return False + return e == self._report(u, v, ddict) + + +class OutMultiEdgeDataView(OutEdgeDataView): + """An EdgeDataView for outward edges of MultiDiGraph; See EdgeDataView""" + + __slots__ = ("keys",) + + def __getstate__(self): + return { + "viewer": self._viewer, + "nbunch": self._nbunch, + "keys": self.keys, + "data": self._data, + "default": self._default, + } + + def __setstate__(self, state): + self.__init__(**state) + + def __init__(self, viewer, nbunch=None, data=False, *, default=None, keys=False): + self._viewer = viewer + adjdict = self._adjdict = viewer._adjdict + self.keys = keys + if nbunch is None: + self._nodes_nbrs = adjdict.items + else: + # dict retains order of nodes but acts like a set + nbunch = dict.fromkeys(viewer._graph.nbunch_iter(nbunch)) + self._nodes_nbrs = lambda: [(n, adjdict[n]) for n in nbunch] + self._nbunch = nbunch + self._data = data + self._default = default + # Set _report based on data and default + if data is True: + if keys is True: + self._report = lambda n, nbr, k, dd: (n, nbr, k, dd) + else: + self._report = lambda n, nbr, k, dd: (n, nbr, dd) + elif data is False: + if keys is True: + self._report = lambda n, nbr, k, dd: (n, nbr, k) + else: + self._report = lambda n, nbr, k, dd: (n, nbr) + else: # data is attribute name + if keys is True: + self._report = ( + lambda n, nbr, k, dd: (n, nbr, k, dd[data]) + if data in dd + else (n, nbr, k, default) + ) + else: + self._report = ( + lambda n, nbr, k, dd: (n, nbr, dd[data]) + if data in dd + else (n, nbr, default) + ) + + def __len__(self): + return sum(1 for e in self) + + def __iter__(self): + return ( + self._report(n, nbr, k, dd) + for n, nbrs in self._nodes_nbrs() + for nbr, kd in nbrs.items() + for k, dd in kd.items() + ) + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and u not in self._nbunch: + return False # this edge doesn't start in nbunch + try: + kdict = self._adjdict[u][v] + except KeyError: + return False + if self.keys is True: + k = e[2] + try: + dd = kdict[k] + except KeyError: + return False + return e == self._report(u, v, k, dd) + return any(e == self._report(u, v, k, dd) for k, dd in kdict.items()) + + +class MultiEdgeDataView(OutMultiEdgeDataView): + """An EdgeDataView class for edges of MultiGraph; See EdgeDataView""" + + __slots__ = () + + def __iter__(self): + seen = {} + for n, nbrs in self._nodes_nbrs(): + for nbr, kd in nbrs.items(): + if nbr not in seen: + for k, dd in kd.items(): + yield self._report(n, nbr, k, dd) + seen[n] = 1 + del seen + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and u not in self._nbunch and v not in self._nbunch: + return False # this edge doesn't start and doesn't end in nbunch + try: + kdict = self._adjdict[u][v] + except KeyError: + try: + kdict = self._adjdict[v][u] + except KeyError: + return False + if self.keys is True: + k = e[2] + try: + dd = kdict[k] + except KeyError: + return False + return e == self._report(u, v, k, dd) + return any(e == self._report(u, v, k, dd) for k, dd in kdict.items()) + + +class InMultiEdgeDataView(OutMultiEdgeDataView): + """An EdgeDataView for inward edges of MultiDiGraph; See EdgeDataView""" + + __slots__ = () + + def __iter__(self): + return ( + self._report(nbr, n, k, dd) + for n, nbrs in self._nodes_nbrs() + for nbr, kd in nbrs.items() + for k, dd in kd.items() + ) + + def __contains__(self, e): + u, v = e[:2] + if self._nbunch is not None and v not in self._nbunch: + return False # this edge doesn't end in nbunch + try: + kdict = self._adjdict[v][u] + except KeyError: + return False + if self.keys is True: + k = e[2] + dd = kdict[k] + return e == self._report(u, v, k, dd) + return any(e == self._report(u, v, k, dd) for k, dd in kdict.items()) + + +# EdgeViews have set operations and no data reported +class OutEdgeView(Set, Mapping, EdgeViewABC): + """A EdgeView class for outward edges of a DiGraph""" + + __slots__ = ("_adjdict", "_graph", "_nodes_nbrs") + + def __getstate__(self): + return {"_graph": self._graph, "_adjdict": self._adjdict} + + def __setstate__(self, state): + self._graph = state["_graph"] + self._adjdict = state["_adjdict"] + self._nodes_nbrs = self._adjdict.items + + @classmethod + def _from_iterable(cls, it): + return set(it) + + dataview = OutEdgeDataView + + def __init__(self, G): + self._graph = G + self._adjdict = G._succ if hasattr(G, "succ") else G._adj + self._nodes_nbrs = self._adjdict.items + + # Set methods + def __len__(self): + return sum(len(nbrs) for n, nbrs in self._nodes_nbrs()) + + def __iter__(self): + for n, nbrs in self._nodes_nbrs(): + for nbr in nbrs: + yield (n, nbr) + + def __contains__(self, e): + try: + u, v = e + return v in self._adjdict[u] + except KeyError: + return False + + # Mapping Methods + def __getitem__(self, e): + if isinstance(e, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.edges)[{e.start}:{e.stop}:{e.step}]" + ) + u, v = e + try: + return self._adjdict[u][v] + except KeyError as ex: # Customize msg to indicate exception origin + raise KeyError(f"The edge {e} is not in the graph.") + + # EdgeDataView methods + def __call__(self, nbunch=None, data=False, *, default=None): + if nbunch is None and data is False: + return self + return self.dataview(self, nbunch, data, default=default) + + def data(self, data=True, default=None, nbunch=None): + """ + Return a read-only view of edge data. + + Parameters + ---------- + data : bool or edge attribute key + If ``data=True``, then the data view maps each edge to a dictionary + containing all of its attributes. If `data` is a key in the edge + dictionary, then the data view maps each edge to its value for + the keyed attribute. In this case, if the edge doesn't have the + attribute, the `default` value is returned. + default : object, default=None + The value used when an edge does not have a specific attribute + nbunch : container of nodes, optional (default=None) + Allows restriction to edges only involving certain nodes. All edges + are considered by default. + + Returns + ------- + dataview + Returns an `EdgeDataView` for undirected Graphs, `OutEdgeDataView` + for DiGraphs, `MultiEdgeDataView` for MultiGraphs and + `OutMultiEdgeDataView` for MultiDiGraphs. + + Notes + ----- + If ``data=False``, returns an `EdgeView` without any edge data. + + See Also + -------- + EdgeDataView + OutEdgeDataView + MultiEdgeDataView + OutMultiEdgeDataView + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edges_from( + ... [ + ... (0, 1, {"dist": 3, "capacity": 20}), + ... (1, 2, {"dist": 4}), + ... (2, 0, {"dist": 5}), + ... ] + ... ) + + Accessing edge data with ``data=True`` (the default) returns an + edge data view object listing each edge with all of its attributes: + + >>> G.edges.data() + EdgeDataView([(0, 1, {'dist': 3, 'capacity': 20}), (0, 2, {'dist': 5}), (1, 2, {'dist': 4})]) + + If `data` represents a key in the edge attribute dict, a dataview listing + each edge with its value for that specific key is returned: + + >>> G.edges.data("dist") + EdgeDataView([(0, 1, 3), (0, 2, 5), (1, 2, 4)]) + + `nbunch` can be used to limit the edges: + + >>> G.edges.data("dist", nbunch=[0]) + EdgeDataView([(0, 1, 3), (0, 2, 5)]) + + If a specific key is not found in an edge attribute dict, the value + specified by `default` is used: + + >>> G.edges.data("capacity") + EdgeDataView([(0, 1, 20), (0, 2, None), (1, 2, None)]) + + Note that there is no check that the `data` key is present in any of + the edge attribute dictionaries: + + >>> G.edges.data("speed") + EdgeDataView([(0, 1, None), (0, 2, None), (1, 2, None)]) + """ + if nbunch is None and data is False: + return self + return self.dataview(self, nbunch, data, default=default) + + # String Methods + def __str__(self): + return str(list(self)) + + def __repr__(self): + return f"{self.__class__.__name__}({list(self)})" + + +class EdgeView(OutEdgeView): + """A EdgeView class for edges of a Graph + + This densely packed View allows iteration over edges, data lookup + like a dict and set operations on edges represented by node-tuples. + In addition, edge data can be controlled by calling this object + possibly creating an EdgeDataView. Typically edges are iterated over + and reported as `(u, v)` node tuples or `(u, v, key)` node/key tuples + for multigraphs. Those edge representations can also be using to + lookup the data dict for any edge. Set operations also are available + where those tuples are the elements of the set. + Calling this object with optional arguments `data`, `default` and `keys` + controls the form of the tuple (see EdgeDataView). Optional argument + `nbunch` allows restriction to edges only involving certain nodes. + + If `data is False` (the default) then iterate over 2-tuples `(u, v)`. + If `data is True` iterate over 3-tuples `(u, v, datadict)`. + Otherwise iterate over `(u, v, datadict.get(data, default))`. + For Multigraphs, if `keys is True`, replace `u, v` with `u, v, key` above. + + Parameters + ========== + graph : NetworkX graph-like class + nbunch : (default= all nodes in graph) only report edges with these nodes + keys : (only for MultiGraph. default=False) report edge key in tuple + data : bool or string (default=False) see above + default : object (default=None) + + Examples + ======== + >>> G = nx.path_graph(4) + >>> EV = G.edges() + >>> (2, 3) in EV + True + >>> for u, v in EV: + ... print((u, v)) + (0, 1) + (1, 2) + (2, 3) + >>> assert EV & {(1, 2), (3, 4)} == {(1, 2)} + + >>> EVdata = G.edges(data="color", default="aqua") + >>> G.add_edge(2, 3, color="blue") + >>> assert (2, 3, "blue") in EVdata + >>> for u, v, c in EVdata: + ... print(f"({u}, {v}) has color: {c}") + (0, 1) has color: aqua + (1, 2) has color: aqua + (2, 3) has color: blue + + >>> EVnbunch = G.edges(nbunch=2) + >>> assert (2, 3) in EVnbunch + >>> assert (0, 1) not in EVnbunch + >>> for u, v in EVnbunch: + ... assert u == 2 or v == 2 + + >>> MG = nx.path_graph(4, create_using=nx.MultiGraph) + >>> EVmulti = MG.edges(keys=True) + >>> (2, 3, 0) in EVmulti + True + >>> (2, 3) in EVmulti # 2-tuples work even when keys is True + True + >>> key = MG.add_edge(2, 3) + >>> for u, v, k in EVmulti: + ... print((u, v, k)) + (0, 1, 0) + (1, 2, 0) + (2, 3, 0) + (2, 3, 1) + """ + + __slots__ = () + + dataview = EdgeDataView + + def __len__(self): + num_nbrs = (len(nbrs) + (n in nbrs) for n, nbrs in self._nodes_nbrs()) + return sum(num_nbrs) // 2 + + def __iter__(self): + seen = {} + for n, nbrs in self._nodes_nbrs(): + for nbr in list(nbrs): + if nbr not in seen: + yield (n, nbr) + seen[n] = 1 + del seen + + def __contains__(self, e): + try: + u, v = e[:2] + return v in self._adjdict[u] or u in self._adjdict[v] + except (KeyError, ValueError): + return False + + +class InEdgeView(OutEdgeView): + """A EdgeView class for inward edges of a DiGraph""" + + __slots__ = () + + def __setstate__(self, state): + self._graph = state["_graph"] + self._adjdict = state["_adjdict"] + self._nodes_nbrs = self._adjdict.items + + dataview = InEdgeDataView + + def __init__(self, G): + self._graph = G + self._adjdict = G._pred if hasattr(G, "pred") else G._adj + self._nodes_nbrs = self._adjdict.items + + def __iter__(self): + for n, nbrs in self._nodes_nbrs(): + for nbr in nbrs: + yield (nbr, n) + + def __contains__(self, e): + try: + u, v = e + return u in self._adjdict[v] + except KeyError: + return False + + def __getitem__(self, e): + if isinstance(e, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.in_edges)[{e.start}:{e.stop}:{e.step}]" + ) + u, v = e + return self._adjdict[v][u] + + +class OutMultiEdgeView(OutEdgeView): + """A EdgeView class for outward edges of a MultiDiGraph""" + + __slots__ = () + + dataview = OutMultiEdgeDataView + + def __len__(self): + return sum( + len(kdict) for n, nbrs in self._nodes_nbrs() for nbr, kdict in nbrs.items() + ) + + def __iter__(self): + for n, nbrs in self._nodes_nbrs(): + for nbr, kdict in nbrs.items(): + for key in kdict: + yield (n, nbr, key) + + def __contains__(self, e): + N = len(e) + if N == 3: + u, v, k = e + elif N == 2: + u, v = e + k = 0 + else: + raise ValueError("MultiEdge must have length 2 or 3") + try: + return k in self._adjdict[u][v] + except KeyError: + return False + + def __getitem__(self, e): + if isinstance(e, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.edges)[{e.start}:{e.stop}:{e.step}]" + ) + u, v, k = e + return self._adjdict[u][v][k] + + def __call__(self, nbunch=None, data=False, *, default=None, keys=False): + if nbunch is None and data is False and keys is True: + return self + return self.dataview(self, nbunch, data, default=default, keys=keys) + + def data(self, data=True, default=None, nbunch=None, keys=False): + if nbunch is None and data is False and keys is True: + return self + return self.dataview(self, nbunch, data, default=default, keys=keys) + + +class MultiEdgeView(OutMultiEdgeView): + """A EdgeView class for edges of a MultiGraph""" + + __slots__ = () + + dataview = MultiEdgeDataView + + def __len__(self): + return sum(1 for e in self) + + def __iter__(self): + seen = {} + for n, nbrs in self._nodes_nbrs(): + for nbr, kd in nbrs.items(): + if nbr not in seen: + for k, dd in kd.items(): + yield (n, nbr, k) + seen[n] = 1 + del seen + + +class InMultiEdgeView(OutMultiEdgeView): + """A EdgeView class for inward edges of a MultiDiGraph""" + + __slots__ = () + + def __setstate__(self, state): + self._graph = state["_graph"] + self._adjdict = state["_adjdict"] + self._nodes_nbrs = self._adjdict.items + + dataview = InMultiEdgeDataView + + def __init__(self, G): + self._graph = G + self._adjdict = G._pred if hasattr(G, "pred") else G._adj + self._nodes_nbrs = self._adjdict.items + + def __iter__(self): + for n, nbrs in self._nodes_nbrs(): + for nbr, kdict in nbrs.items(): + for key in kdict: + yield (nbr, n, key) + + def __contains__(self, e): + N = len(e) + if N == 3: + u, v, k = e + elif N == 2: + u, v = e + k = 0 + else: + raise ValueError("MultiEdge must have length 2 or 3") + try: + return k in self._adjdict[v][u] + except KeyError: + return False + + def __getitem__(self, e): + if isinstance(e, slice): + raise nx.NetworkXError( + f"{type(self).__name__} does not support slicing, " + f"try list(G.in_edges)[{e.start}:{e.stop}:{e.step}]" + ) + u, v, k = e + return self._adjdict[v][u][k] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..188a538e58a938b50fe0042e54dec32162bfdb9e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/dispatch_interface.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/dispatch_interface.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..191b5f19185e852b3348aa25a1245f1768e6b164 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/dispatch_interface.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/historical_tests.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/historical_tests.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1ec2da8e045e9bc5986fe3e652647ea35f02df5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/historical_tests.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_coreviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_coreviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9a32d37f99058daad885613bdffe1bcd75479fe Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_coreviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45cf4273c899f39df26753a54ced43621f660005 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph_historical.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph_historical.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f2a1fb393d6707b454e4585bad0e03c0395deb7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_digraph_historical.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_filters.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f143c6a98ceeb29aefb941db33cc86efdf72f19 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_filters.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_function.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_function.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d681c0eefee13e0b5deda3ebab9073238f50877 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_function.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..447e526cc752a47180331e1a1569039411289fea Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph_historical.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph_historical.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0131930e6aeba2dbef372454459e0b72000ceedb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graph_historical.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graphviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graphviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c733b73085f5e1968d7fb8c1f08e0a3759fe686 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_graphviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multidigraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multidigraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0b1c9ba19f2d55536eff8caeeb0f9ab14ba3290 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multidigraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multigraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multigraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e9d6cae4faa0bfee868779e76c2faa6d616b9b1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_multigraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_reportviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_reportviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebc7d34ecb3d0ab776b33365f03ed375319534bc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_reportviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_special.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_special.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cd1430965369708b4d554cd3e7df050df3b14c0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_special.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_subgraphviews.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_subgraphviews.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9746fded2e31325d061b191cf9f5284cca1e5d6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/__pycache__/test_subgraphviews.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..dfc0b5f6bfabffcab0530e31222242ae144b506d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/dispatch_interface.py @@ -0,0 +1,192 @@ +# This file contains utilities for testing the dispatching feature + +# A full test of all dispatchable algorithms is performed by +# modifying the pytest invocation and setting an environment variable +# NETWORKX_TEST_BACKEND=nx_loopback pytest +# This is comprehensive, but only tests the `test_override_dispatch` +# function in networkx.classes.backends. + +# To test the `_dispatchable` function directly, several tests scattered throughout +# NetworkX have been augmented to test normal and dispatch mode. +# Searching for `dispatch_interface` should locate the specific tests. + +import networkx as nx +from networkx import DiGraph, Graph, MultiDiGraph, MultiGraph, PlanarEmbedding +from networkx.classes.reportviews import NodeView + + +class LoopbackGraph(Graph): + __networkx_backend__ = "nx_loopback" + + +class LoopbackDiGraph(DiGraph): + __networkx_backend__ = "nx_loopback" + + +class LoopbackMultiGraph(MultiGraph): + __networkx_backend__ = "nx_loopback" + + +class LoopbackMultiDiGraph(MultiDiGraph): + __networkx_backend__ = "nx_loopback" + + +class LoopbackPlanarEmbedding(PlanarEmbedding): + __networkx_backend__ = "nx_loopback" + + +def convert(graph): + if isinstance(graph, PlanarEmbedding): + return LoopbackPlanarEmbedding(graph) + if isinstance(graph, MultiDiGraph): + return LoopbackMultiDiGraph(graph) + if isinstance(graph, MultiGraph): + return LoopbackMultiGraph(graph) + if isinstance(graph, DiGraph): + return LoopbackDiGraph(graph) + if isinstance(graph, Graph): + return LoopbackGraph(graph) + raise TypeError(f"Unsupported type of graph: {type(graph)}") + + +class LoopbackBackendInterface: + def __getattr__(self, item): + try: + return nx.utils.backends._registered_algorithms[item].orig_func + except KeyError: + raise AttributeError(item) from None + + @staticmethod + def graph__new__(cls, incoming_graph_data=None, **attr): + # LoopbackGraph.__init__ will be called next since the returned + # object is an instance of an nx.Graph. For more details, see: + # https://docs.python.org/3/reference/datamodel.html#object.__new__ + return object.__new__(LoopbackGraph) + + @staticmethod + def convert_from_nx( + graph, + *, + edge_attrs=None, + node_attrs=None, + preserve_edge_attrs=None, + preserve_node_attrs=None, + preserve_graph_attrs=None, + name=None, + graph_name=None, + ): + if name in { + # Raise if input graph changes. See test_dag.py::test_topological_sort6 + "lexicographical_topological_sort", + "topological_generations", + "topological_sort", + # Would be nice to some day avoid these cutoffs of full testing + }: + return graph + if isinstance(graph, NodeView): + # Convert to a Graph with only nodes (no edges) + new_graph = Graph() + new_graph.add_nodes_from(graph.items()) + graph = new_graph + G = LoopbackGraph() + elif not isinstance(graph, Graph): + raise TypeError( + f"Bad type for graph argument {graph_name} in {name}: {type(graph)}" + ) + elif graph.__class__ in {Graph, LoopbackGraph}: + G = LoopbackGraph() + elif graph.__class__ in {DiGraph, LoopbackDiGraph}: + G = LoopbackDiGraph() + elif graph.__class__ in {MultiGraph, LoopbackMultiGraph}: + G = LoopbackMultiGraph() + elif graph.__class__ in {MultiDiGraph, LoopbackMultiDiGraph}: + G = LoopbackMultiDiGraph() + elif graph.__class__ in {PlanarEmbedding, LoopbackPlanarEmbedding}: + G = LoopbackDiGraph() # or LoopbackPlanarEmbedding + else: + # Would be nice to handle these better some day + # nx.algorithms.approximation.kcomponents._AntiGraph + # nx.classes.tests.test_multidigraph.MultiDiGraphSubClass + # nx.classes.tests.test_multigraph.MultiGraphSubClass + G = graph.__class__() + + if preserve_graph_attrs: + G.graph.update(graph.graph) + + # add nodes + G.add_nodes_from(graph) + if preserve_node_attrs: + for n, dd in G._node.items(): + dd.update(graph.nodes[n]) + elif node_attrs: + for n, dd in G._node.items(): + dd.update( + (attr, graph._node[n].get(attr, default)) + for attr, default in node_attrs.items() + if default is not None or attr in graph._node[n] + ) + + # tools to build datadict and keydict + if preserve_edge_attrs: + + def G_new_datadict(old_dd): + return G.edge_attr_dict_factory(old_dd) + elif edge_attrs: + + def G_new_datadict(old_dd): + return G.edge_attr_dict_factory( + (attr, old_dd.get(attr, default)) + for attr, default in edge_attrs.items() + if default is not None or attr in old_dd + ) + else: + + def G_new_datadict(old_dd): + return G.edge_attr_dict_factory() + + if G.is_multigraph(): + + def G_new_inner(keydict): + kd = G.adjlist_inner_dict_factory( + (k, G_new_datadict(dd)) for k, dd in keydict.items() + ) + return kd + else: + G_new_inner = G_new_datadict + + # add edges keeping the same order in _adj and _pred + G_adj = G._adj + if G.is_directed(): + for n, nbrs in graph._adj.items(): + G_adj[n].update((nbr, G_new_inner(dd)) for nbr, dd in nbrs.items()) + # ensure same datadict for pred and adj; and pred order of graph._pred + G_pred = G._pred + for n, nbrs in graph._pred.items(): + G_pred[n].update((nbr, G_adj[nbr][n]) for nbr in nbrs) + else: # undirected + for n, nbrs in graph._adj.items(): + # ensure same datadict for both ways; and adj order of graph._adj + G_adj[n].update( + (nbr, G_adj[nbr][n] if n in G_adj[nbr] else G_new_inner(dd)) + for nbr, dd in nbrs.items() + ) + + return G + + @staticmethod + def convert_to_nx(obj, *, name=None): + return obj + + @staticmethod + def on_start_tests(items): + # Verify that items can be xfailed + for item in items: + assert hasattr(item, "add_marker") + + def can_run(self, name, args, kwargs): + # It is unnecessary to define this function if algorithms are fully supported. + # We include it for illustration purposes. + return hasattr(self, name) + + +backend_interface = LoopbackBackendInterface() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..0b70a2e985b64477ea3d26b89ccb21c8cb4dc6c5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/historical_tests.py @@ -0,0 +1,476 @@ +"""Original NetworkX graph tests""" + +import pytest + +import networkx as nx +from networkx import convert_node_labels_to_integers as cnlti +from networkx.utils import edges_equal, nodes_equal + + +class HistoricalTests: + @classmethod + def setup_class(cls): + cls.null = nx.null_graph() + cls.P1 = cnlti(nx.path_graph(1), first_label=1) + cls.P3 = cnlti(nx.path_graph(3), first_label=1) + cls.P10 = cnlti(nx.path_graph(10), first_label=1) + cls.K1 = cnlti(nx.complete_graph(1), first_label=1) + cls.K3 = cnlti(nx.complete_graph(3), first_label=1) + cls.K4 = cnlti(nx.complete_graph(4), first_label=1) + cls.K5 = cnlti(nx.complete_graph(5), first_label=1) + cls.K10 = cnlti(nx.complete_graph(10), first_label=1) + cls.G = nx.Graph + + def test_name(self): + G = self.G(name="test") + assert G.name == "test" + H = self.G() + assert H.name == "" + + # Nodes + + def test_add_remove_node(self): + G = self.G() + G.add_node("A") + assert G.has_node("A") + G.remove_node("A") + assert not G.has_node("A") + + def test_nonhashable_node(self): + # Test if a non-hashable object is in the Graph. A python dict will + # raise a TypeError, but for a Graph class a simple False should be + # returned (see Graph __contains__). If it cannot be a node then it is + # not a node. + G = self.G() + assert not G.has_node(["A"]) + assert not G.has_node({"A": 1}) + + def test_add_nodes_from(self): + G = self.G() + G.add_nodes_from(list("ABCDEFGHIJKL")) + assert G.has_node("L") + G.remove_nodes_from(["H", "I", "J", "K", "L"]) + G.add_nodes_from([1, 2, 3, 4]) + assert sorted(G.nodes(), key=str) == [ + 1, + 2, + 3, + 4, + "A", + "B", + "C", + "D", + "E", + "F", + "G", + ] + # test __iter__ + assert sorted(G, key=str) == [1, 2, 3, 4, "A", "B", "C", "D", "E", "F", "G"] + + def test_contains(self): + G = self.G() + G.add_node("A") + assert "A" in G + assert [] not in G # never raise a Key or TypeError in this test + assert {1: 1} not in G + + def test_add_remove(self): + # Test add_node and remove_node acting for various nbunch + G = self.G() + G.add_node("m") + assert G.has_node("m") + G.add_node("m") # no complaints + pytest.raises(nx.NetworkXError, G.remove_node, "j") + G.remove_node("m") + assert list(G) == [] + + def test_nbunch_is_list(self): + G = self.G() + G.add_nodes_from(list("ABCD")) + G.add_nodes_from(self.P3) # add nbunch of nodes (nbunch=Graph) + assert sorted(G.nodes(), key=str) == [1, 2, 3, "A", "B", "C", "D"] + G.remove_nodes_from(self.P3) # remove nbunch of nodes (nbunch=Graph) + assert sorted(G.nodes(), key=str) == ["A", "B", "C", "D"] + + def test_nbunch_is_set(self): + G = self.G() + nbunch = set("ABCDEFGHIJKL") + G.add_nodes_from(nbunch) + assert G.has_node("L") + + def test_nbunch_dict(self): + # nbunch is a dict with nodes as keys + G = self.G() + nbunch = set("ABCDEFGHIJKL") + G.add_nodes_from(nbunch) + nbunch = {"I": "foo", "J": 2, "K": True, "L": "spam"} + G.remove_nodes_from(nbunch) + assert sorted(G.nodes(), key=str), ["A", "B", "C", "D", "E", "F", "G", "H"] + + def test_nbunch_iterator(self): + G = self.G() + G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"]) + n_iter = self.P3.nodes() + G.add_nodes_from(n_iter) + assert sorted(G.nodes(), key=str) == [ + 1, + 2, + 3, + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + ] + n_iter = self.P3.nodes() # rebuild same iterator + G.remove_nodes_from(n_iter) # remove nbunch of nodes (nbunch=iterator) + assert sorted(G.nodes(), key=str) == ["A", "B", "C", "D", "E", "F", "G", "H"] + + def test_nbunch_graph(self): + G = self.G() + G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"]) + nbunch = self.K3 + G.add_nodes_from(nbunch) + assert sorted(G.nodes(), key=str), [ + 1, + 2, + 3, + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + ] + + # Edges + + def test_add_edge(self): + G = self.G() + pytest.raises(TypeError, G.add_edge, "A") + + G.add_edge("A", "B") # testing add_edge() + G.add_edge("A", "B") # should fail silently + assert G.has_edge("A", "B") + assert not G.has_edge("A", "C") + assert G.has_edge(*("A", "B")) + if G.is_directed(): + assert not G.has_edge("B", "A") + else: + # G is undirected, so B->A is an edge + assert G.has_edge("B", "A") + + G.add_edge("A", "C") # test directedness + G.add_edge("C", "A") + G.remove_edge("C", "A") + if G.is_directed(): + assert G.has_edge("A", "C") + else: + assert not G.has_edge("A", "C") + assert not G.has_edge("C", "A") + + def test_self_loop(self): + G = self.G() + G.add_edge("A", "A") # test self loops + assert G.has_edge("A", "A") + G.remove_edge("A", "A") + G.add_edge("X", "X") + assert G.has_node("X") + G.remove_node("X") + G.add_edge("A", "Z") # should add the node silently + assert G.has_node("Z") + + def test_add_edges_from(self): + G = self.G() + G.add_edges_from([("B", "C")]) # test add_edges_from() + assert G.has_edge("B", "C") + if G.is_directed(): + assert not G.has_edge("C", "B") + else: + assert G.has_edge("C", "B") # undirected + + G.add_edges_from([("D", "F"), ("B", "D")]) + assert G.has_edge("D", "F") + assert G.has_edge("B", "D") + + if G.is_directed(): + assert not G.has_edge("D", "B") + else: + assert G.has_edge("D", "B") # undirected + + def test_add_edges_from2(self): + G = self.G() + # after failing silently, should add 2nd edge + G.add_edges_from([tuple("IJ"), list("KK"), tuple("JK")]) + assert G.has_edge(*("I", "J")) + assert G.has_edge(*("K", "K")) + assert G.has_edge(*("J", "K")) + if G.is_directed(): + assert not G.has_edge(*("K", "J")) + else: + assert G.has_edge(*("K", "J")) + + def test_add_edges_from3(self): + G = self.G() + G.add_edges_from(zip(list("ACD"), list("CDE"))) + assert G.has_edge("D", "E") + assert not G.has_edge("E", "C") + + def test_remove_edge(self): + G = self.G() + G.add_nodes_from([1, 2, 3, "A", "B", "C", "D", "E", "F", "G", "H"]) + + G.add_edges_from(zip(list("MNOP"), list("NOPM"))) + assert G.has_edge("O", "P") + assert G.has_edge("P", "M") + G.remove_node("P") # tests remove_node()'s handling of edges. + assert not G.has_edge("P", "M") + pytest.raises(TypeError, G.remove_edge, "M") + + G.add_edge("N", "M") + assert G.has_edge("M", "N") + G.remove_edge("M", "N") + assert not G.has_edge("M", "N") + + # self loop fails silently + G.remove_edges_from([list("HI"), list("DF"), tuple("KK"), tuple("JK")]) + assert not G.has_edge("H", "I") + assert not G.has_edge("J", "K") + G.remove_edges_from([list("IJ"), list("KK"), list("JK")]) + assert not G.has_edge("I", "J") + G.remove_nodes_from(set("ZEFHIMNO")) + G.add_edge("J", "K") + + def test_edges_nbunch(self): + # Test G.edges(nbunch) with various forms of nbunch + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + # node not in nbunch should be quietly ignored + pytest.raises(nx.NetworkXError, G.edges, 6) + assert list(G.edges("Z")) == [] # iterable non-node + # nbunch can be an empty list + assert list(G.edges([])) == [] + if G.is_directed(): + elist = [("A", "B"), ("A", "C"), ("B", "D")] + else: + elist = [("A", "B"), ("A", "C"), ("B", "C"), ("B", "D")] + # nbunch can be a list + assert edges_equal(list(G.edges(["A", "B"])), elist, directed=G.is_directed()) + # nbunch can be a set + assert edges_equal(G.edges({"A", "B"}), elist, directed=G.is_directed()) + # nbunch can be a graph + G1 = self.G() + G1.add_nodes_from("AB") + assert edges_equal(G.edges(G1), elist, directed=G.is_directed()) + # nbunch can be a dict with nodes as keys + ndict = {"A": "thing1", "B": "thing2"} + assert edges_equal(G.edges(ndict), elist, directed=G.is_directed()) + # nbunch can be a single node + assert edges_equal(list(G.edges("A")), [("A", "B"), ("A", "C")]) + assert nodes_equal(sorted(G), ["A", "B", "C", "D"]) + + # nbunch can be nothing (whole graph) + assert edges_equal( + list(G.edges()), + [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")], + ) + + def test_degree(self): + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + assert G.degree("A") == 2 + + # degree of single node in iterable container must return dict + assert list(G.degree(["A"])) == [("A", 2)] + assert sorted(d for n, d in G.degree(["A", "B"])) == [2, 3] + assert sorted(d for n, d in G.degree()) == [2, 2, 3, 3] + + def test_degree2(self): + H = self.G() + H.add_edges_from([(1, 24), (1, 2)]) + assert sorted(d for n, d in H.degree([1, 24])) == [1, 2] + + def test_degree_graph(self): + P3 = nx.path_graph(3) + P5 = nx.path_graph(5) + # silently ignore nodes not in P3 + assert dict(d for n, d in P3.degree(["A", "B"])) == {} + # nbunch can be a graph + assert sorted(d for n, d in P5.degree(P3)) == [1, 2, 2] + # nbunch can be a graph that's way too big + assert sorted(d for n, d in P3.degree(P5)) == [1, 1, 2] + assert list(P5.degree([])) == [] + assert dict(P5.degree([])) == {} + + def test_null(self): + null = nx.null_graph() + assert list(null.degree()) == [] + assert dict(null.degree()) == {} + + def test_order_size(self): + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + assert G.order() == 4 + assert G.size() == 5 + assert G.number_of_edges() == 5 + assert G.number_of_edges("A", "B") == 1 + assert G.number_of_edges("A", "D") == 0 + + def test_copy(self): + G = self.G() + H = G.copy() # copy + assert H.adj == G.adj + assert H.name == G.name + assert H is not G + + def test_subgraph(self): + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + SG = G.subgraph(["A", "B", "D"]) + assert nodes_equal(list(SG), ["A", "B", "D"]) + assert edges_equal(list(SG.edges()), [("A", "B"), ("B", "D")]) + + def test_to_directed(self): + G = self.G() + if not G.is_directed(): + G.add_edges_from( + [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")] + ) + + DG = G.to_directed() + assert DG is not G # directed copy or copy + + assert DG.is_directed() + assert DG.name == G.name + assert DG.adj == G.adj + assert sorted(DG.out_edges(list("AB"))) == [ + ("A", "B"), + ("A", "C"), + ("B", "A"), + ("B", "C"), + ("B", "D"), + ] + DG.remove_edge("A", "B") + assert DG.has_edge("B", "A") # this removes B-A but not A-B + assert not DG.has_edge("A", "B") + + def test_to_undirected(self): + G = self.G() + if G.is_directed(): + G.add_edges_from( + [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")] + ) + UG = G.to_undirected() # to_undirected + assert UG is not G + assert not UG.is_directed() + assert G.is_directed() + assert UG.name == G.name + assert UG.adj != G.adj + assert sorted(UG.edges(list("AB"))) == [ + ("A", "B"), + ("A", "C"), + ("B", "C"), + ("B", "D"), + ] + assert sorted(UG.edges(["A", "B"])) == [ + ("A", "B"), + ("A", "C"), + ("B", "C"), + ("B", "D"), + ] + UG.remove_edge("A", "B") + assert not UG.has_edge("B", "A") + assert not UG.has_edge("A", "B") + + def test_neighbors(self): + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + G.add_nodes_from("GJK") + assert sorted(G["A"]) == ["B", "C"] + assert sorted(G.neighbors("A")) == ["B", "C"] + assert sorted(G.neighbors("A")) == ["B", "C"] + assert sorted(G.neighbors("G")) == [] + pytest.raises(nx.NetworkXError, G.neighbors, "j") + + def test_iterators(self): + G = self.G() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")]) + G.add_nodes_from("GJK") + assert sorted(G.nodes()) == ["A", "B", "C", "D", "G", "J", "K"] + assert edges_equal( + G.edges(), + [("A", "B"), ("A", "C"), ("B", "D"), ("C", "B"), ("C", "D")], + ) + + assert sorted(v for k, v in G.degree()) == [0, 0, 0, 2, 2, 3, 3] + assert sorted(G.degree(), key=str) == [ + ("A", 2), + ("B", 3), + ("C", 3), + ("D", 2), + ("G", 0), + ("J", 0), + ("K", 0), + ] + assert sorted(G.neighbors("A")) == ["B", "C"] + pytest.raises(nx.NetworkXError, G.neighbors, "X") + G.clear() + assert nx.number_of_nodes(G) == 0 + assert nx.number_of_edges(G) == 0 + + def test_null_subgraph(self): + # Subgraph of a null graph is a null graph + nullgraph = nx.null_graph() + G = nx.null_graph() + H = G.subgraph([]) + assert nx.is_isomorphic(H, nullgraph) + + def test_empty_subgraph(self): + # Subgraph of an empty graph is an empty graph. test 1 + nullgraph = nx.null_graph() + E5 = nx.empty_graph(5) + E10 = nx.empty_graph(10) + H = E10.subgraph([]) + assert nx.is_isomorphic(H, nullgraph) + H = E10.subgraph([1, 2, 3, 4, 5]) + assert nx.is_isomorphic(H, E5) + + def test_complete_subgraph(self): + # Subgraph of a complete graph is a complete graph + K1 = nx.complete_graph(1) + K3 = nx.complete_graph(3) + K5 = nx.complete_graph(5) + H = K5.subgraph([1, 2, 3]) + assert nx.is_isomorphic(H, K3) + + def test_subgraph_nbunch(self): + nullgraph = nx.null_graph() + K1 = nx.complete_graph(1) + K3 = nx.complete_graph(3) + K5 = nx.complete_graph(5) + # Test G.subgraph(nbunch), where nbunch is a single node + H = K5.subgraph(1) + assert nx.is_isomorphic(H, K1) + # Test G.subgraph(nbunch), where nbunch is a set + H = K5.subgraph({1}) + assert nx.is_isomorphic(H, K1) + # Test G.subgraph(nbunch), where nbunch is an iterator + H = K5.subgraph(iter(K3)) + assert nx.is_isomorphic(H, K3) + # Test G.subgraph(nbunch), where nbunch is another graph + H = K5.subgraph(K3) + assert nx.is_isomorphic(H, K3) + H = K5.subgraph([9]) + assert nx.is_isomorphic(H, nullgraph) + + def test_node_tuple_issue(self): + H = self.G() + # Test error handling of tuple as a node + pytest.raises(nx.NetworkXError, H.remove_node, (1, 2)) + H.remove_nodes_from([(1, 2)]) # no error + pytest.raises(nx.NetworkXError, H.neighbors, (1, 2)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py new file mode 100644 index 0000000000000000000000000000000000000000..24de7f2f1115b864682b261daa256eff0deef696 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_coreviews.py @@ -0,0 +1,362 @@ +import pickle + +import pytest + +import networkx as nx + + +class TestAtlasView: + # node->data + def setup_method(self): + self.d = {0: {"color": "blue", "weight": 1.2}, 1: {}, 2: {"color": 1}} + self.av = nx.classes.coreviews.AtlasView(self.d) + + def test_pickle(self): + view = self.av + pview = pickle.loads(pickle.dumps(view, -1)) + assert view == pview + assert view.__slots__ == pview.__slots__ + pview = pickle.loads(pickle.dumps(view)) + assert view == pview + assert view.__slots__ == pview.__slots__ + + def test_len(self): + assert len(self.av) == len(self.d) + + def test_iter(self): + assert list(self.av) == list(self.d) + + def test_getitem(self): + assert self.av[1] is self.d[1] + assert self.av[2]["color"] == 1 + pytest.raises(KeyError, self.av.__getitem__, 3) + + def test_copy(self): + avcopy = self.av.copy() + assert avcopy[0] == self.av[0] + assert avcopy == self.av + assert avcopy[0] is not self.av[0] + assert avcopy is not self.av + avcopy[5] = {} + assert avcopy != self.av + + avcopy[0]["ht"] = 4 + assert avcopy[0] != self.av[0] + self.av[0]["ht"] = 4 + assert avcopy[0] == self.av[0] + del self.av[0]["ht"] + + assert not hasattr(self.av, "__setitem__") + + def test_items(self): + assert sorted(self.av.items()) == sorted(self.d.items()) + + def test_str(self): + out = str(self.d) + assert str(self.av) == out + + def test_repr(self): + out = "AtlasView(" + str(self.d) + ")" + assert repr(self.av) == out + + +class TestAdjacencyView: + # node->nbr->data + def setup_method(self): + dd = {"color": "blue", "weight": 1.2} + self.nd = {0: dd, 1: {}, 2: {"color": 1}} + self.adj = {3: self.nd, 0: {3: dd}, 1: {}, 2: {3: {"color": 1}}} + self.adjview = nx.classes.coreviews.AdjacencyView(self.adj) + + def test_pickle(self): + view = self.adjview + pview = pickle.loads(pickle.dumps(view, -1)) + assert view == pview + assert view.__slots__ == pview.__slots__ + + def test_len(self): + assert len(self.adjview) == len(self.adj) + + def test_iter(self): + assert list(self.adjview) == list(self.adj) + + def test_getitem(self): + assert self.adjview[1] is not self.adj[1] + assert self.adjview[3][0] is self.adjview[0][3] + assert self.adjview[2][3]["color"] == 1 + pytest.raises(KeyError, self.adjview.__getitem__, 4) + + def test_copy(self): + avcopy = self.adjview.copy() + assert avcopy[0] == self.adjview[0] + assert avcopy[0] is not self.adjview[0] + + avcopy[2][3]["ht"] = 4 + assert avcopy[2] != self.adjview[2] + self.adjview[2][3]["ht"] = 4 + assert avcopy[2] == self.adjview[2] + del self.adjview[2][3]["ht"] + + assert not hasattr(self.adjview, "__setitem__") + + def test_items(self): + view_items = sorted((n, dict(d)) for n, d in self.adjview.items()) + assert view_items == sorted(self.adj.items()) + + def test_str(self): + out = str(dict(self.adj)) + assert str(self.adjview) == out + + def test_repr(self): + out = self.adjview.__class__.__name__ + "(" + str(self.adj) + ")" + assert repr(self.adjview) == out + + +class TestMultiAdjacencyView(TestAdjacencyView): + # node->nbr->key->data + def setup_method(self): + dd = {"color": "blue", "weight": 1.2} + self.kd = {0: dd, 1: {}, 2: {"color": 1}} + self.nd = {3: self.kd, 0: {3: dd}, 1: {0: {}}, 2: {3: {"color": 1}}} + self.adj = {3: self.nd, 0: {3: {3: dd}}, 1: {}, 2: {3: {8: {}}}} + self.adjview = nx.classes.coreviews.MultiAdjacencyView(self.adj) + + def test_getitem(self): + assert self.adjview[1] is not self.adj[1] + assert self.adjview[3][0][3] is self.adjview[0][3][3] + assert self.adjview[3][2][3]["color"] == 1 + pytest.raises(KeyError, self.adjview.__getitem__, 4) + + def test_copy(self): + avcopy = self.adjview.copy() + assert avcopy[0] == self.adjview[0] + assert avcopy[0] is not self.adjview[0] + + avcopy[2][3][8]["ht"] = 4 + assert avcopy[2] != self.adjview[2] + self.adjview[2][3][8]["ht"] = 4 + assert avcopy[2] == self.adjview[2] + del self.adjview[2][3][8]["ht"] + + assert not hasattr(self.adjview, "__setitem__") + + +class TestUnionAtlas: + # node->data + def setup_method(self): + self.s = {0: {"color": "blue", "weight": 1.2}, 1: {}, 2: {"color": 1}} + self.p = {3: {"color": "blue", "weight": 1.2}, 4: {}, 2: {"watch": 2}} + self.av = nx.classes.coreviews.UnionAtlas(self.s, self.p) + + def test_pickle(self): + view = self.av + pview = pickle.loads(pickle.dumps(view, -1)) + assert view == pview + assert view.__slots__ == pview.__slots__ + + def test_len(self): + assert len(self.av) == len(self.s.keys() | self.p.keys()) == 5 + + def test_iter(self): + assert set(self.av) == set(self.s) | set(self.p) + + def test_getitem(self): + assert self.av[0] is self.s[0] + assert self.av[4] is self.p[4] + assert self.av[2]["color"] == 1 + pytest.raises(KeyError, self.av[2].__getitem__, "watch") + pytest.raises(KeyError, self.av.__getitem__, 8) + + def test_copy(self): + avcopy = self.av.copy() + assert avcopy[0] == self.av[0] + assert avcopy[0] is not self.av[0] + assert avcopy is not self.av + avcopy[5] = {} + assert avcopy != self.av + + avcopy[0]["ht"] = 4 + assert avcopy[0] != self.av[0] + self.av[0]["ht"] = 4 + assert avcopy[0] == self.av[0] + del self.av[0]["ht"] + + assert not hasattr(self.av, "__setitem__") + + def test_items(self): + expected = dict(self.p.items()) + expected.update(self.s) + assert sorted(self.av.items()) == sorted(expected.items()) + + def test_str(self): + out = str(dict(self.av)) + assert str(self.av) == out + + def test_repr(self): + out = f"{self.av.__class__.__name__}({self.s}, {self.p})" + assert repr(self.av) == out + + +class TestUnionAdjacency: + # node->nbr->data + def setup_method(self): + dd = {"color": "blue", "weight": 1.2} + self.nd = {0: dd, 1: {}, 2: {"color": 1}} + self.s = {3: self.nd, 0: {}, 1: {}, 2: {3: {"color": 1}}} + self.p = {3: {}, 0: {3: dd}, 1: {0: {}}, 2: {1: {"color": 1}}} + self.adjview = nx.classes.coreviews.UnionAdjacency(self.s, self.p) + + def test_pickle(self): + view = self.adjview + pview = pickle.loads(pickle.dumps(view, -1)) + assert view == pview + assert view.__slots__ == pview.__slots__ + + def test_len(self): + assert len(self.adjview) == len(self.s) + + def test_iter(self): + assert sorted(self.adjview) == sorted(self.s) + + def test_getitem(self): + assert self.adjview[1] is not self.s[1] + assert self.adjview[3][0] is self.adjview[0][3] + assert self.adjview[2][3]["color"] == 1 + pytest.raises(KeyError, self.adjview.__getitem__, 4) + + def test_copy(self): + avcopy = self.adjview.copy() + assert avcopy[0] == self.adjview[0] + assert avcopy[0] is not self.adjview[0] + + avcopy[2][3]["ht"] = 4 + assert avcopy[2] != self.adjview[2] + self.adjview[2][3]["ht"] = 4 + assert avcopy[2] == self.adjview[2] + del self.adjview[2][3]["ht"] + + assert not hasattr(self.adjview, "__setitem__") + + def test_str(self): + out = str(dict(self.adjview)) + assert str(self.adjview) == out + + def test_repr(self): + clsname = self.adjview.__class__.__name__ + out = f"{clsname}({self.s}, {self.p})" + assert repr(self.adjview) == out + + +class TestUnionMultiInner(TestUnionAdjacency): + # nbr->key->data + def setup_method(self): + dd = {"color": "blue", "weight": 1.2} + self.kd = {7: {}, "ekey": {}, 9: {"color": 1}} + self.s = {3: self.kd, 0: {7: dd}, 1: {}, 2: {"key": {"color": 1}}} + self.p = {3: {}, 0: {3: dd}, 1: {}, 2: {1: {"span": 2}}} + self.adjview = nx.classes.coreviews.UnionMultiInner(self.s, self.p) + + def test_len(self): + assert len(self.adjview) == len(self.s.keys() | self.p.keys()) == 4 + + def test_getitem(self): + assert self.adjview[1] is not self.s[1] + assert self.adjview[0][7] is self.adjview[0][3] + assert self.adjview[2]["key"]["color"] == 1 + assert self.adjview[2][1]["span"] == 2 + pytest.raises(KeyError, self.adjview.__getitem__, 4) + pytest.raises(KeyError, self.adjview[1].__getitem__, "key") + + def test_copy(self): + avcopy = self.adjview.copy() + assert avcopy[0] == self.adjview[0] + assert avcopy[0] is not self.adjview[0] + + avcopy[2][1]["width"] = 8 + assert avcopy[2] != self.adjview[2] + self.adjview[2][1]["width"] = 8 + assert avcopy[2] == self.adjview[2] + del self.adjview[2][1]["width"] + + assert not hasattr(self.adjview, "__setitem__") + assert hasattr(avcopy, "__setitem__") + + +class TestUnionMultiAdjacency(TestUnionAdjacency): + # node->nbr->key->data + def setup_method(self): + dd = {"color": "blue", "weight": 1.2} + self.kd = {7: {}, 8: {}, 9: {"color": 1}} + self.nd = {3: self.kd, 0: {9: dd}, 1: {8: {}}, 2: {9: {"color": 1}}} + self.s = {3: self.nd, 0: {3: {7: dd}}, 1: {}, 2: {3: {8: {}}}} + self.p = {3: {}, 0: {3: {9: dd}}, 1: {}, 2: {1: {8: {}}}} + self.adjview = nx.classes.coreviews.UnionMultiAdjacency(self.s, self.p) + + def test_getitem(self): + assert self.adjview[1] is not self.s[1] + assert self.adjview[3][0][9] is self.adjview[0][3][9] + assert self.adjview[3][2][9]["color"] == 1 + pytest.raises(KeyError, self.adjview.__getitem__, 4) + + def test_copy(self): + avcopy = self.adjview.copy() + assert avcopy[0] == self.adjview[0] + assert avcopy[0] is not self.adjview[0] + + avcopy[2][3][8]["ht"] = 4 + assert avcopy[2] != self.adjview[2] + self.adjview[2][3][8]["ht"] = 4 + assert avcopy[2] == self.adjview[2] + del self.adjview[2][3][8]["ht"] + + assert not hasattr(self.adjview, "__setitem__") + assert hasattr(avcopy, "__setitem__") + + +class TestFilteredGraphs: + def setup_method(self): + self.Graphs = [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] + + def test_hide_show_nodes(self): + SubGraph = nx.subgraph_view + for Graph in self.Graphs: + G = nx.path_graph(4, Graph) + SG = G.subgraph([2, 3]) + RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1])) + assert SG.nodes == RG.nodes + assert SG.edges == RG.edges + SGC = SG.copy() + RGC = RG.copy() + assert SGC.nodes == RGC.nodes + assert SGC.edges == RGC.edges + + def test_str_repr(self): + SubGraph = nx.subgraph_view + for Graph in self.Graphs: + G = nx.path_graph(4, Graph) + SG = G.subgraph([2, 3]) + RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1])) + str(SG.adj) + str(RG.adj) + repr(SG.adj) + repr(RG.adj) + str(SG.adj[2]) + str(RG.adj[2]) + repr(SG.adj[2]) + repr(RG.adj[2]) + + def test_copy(self): + SubGraph = nx.subgraph_view + for Graph in self.Graphs: + G = nx.path_graph(4, Graph) + SG = G.subgraph([2, 3]) + RG = SubGraph(G, filter_node=nx.filters.hide_nodes([0, 1])) + RsG = SubGraph(G, filter_node=nx.filters.show_nodes([2, 3])) + assert G.adj.copy() == G.adj + assert G.adj[2].copy() == G.adj[2] + assert SG.adj.copy() == SG.adj + assert SG.adj[2].copy() == SG.adj[2] + assert RG.adj.copy() == RG.adj + assert RG.adj[2].copy() == RG.adj[2] + assert RsG.adj.copy() == RsG.adj + assert RsG.adj[2].copy() == RsG.adj[2] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py new file mode 100644 index 0000000000000000000000000000000000000000..b9972f9a5f1ab101b9f6f2f9a1584ddafccd2ff3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph.py @@ -0,0 +1,331 @@ +import pytest + +import networkx as nx +from networkx.utils import nodes_equal + +from .test_graph import BaseAttrGraphTester, BaseGraphTester +from .test_graph import TestEdgeSubgraph as _TestGraphEdgeSubgraph +from .test_graph import TestGraph as _TestGraph + + +class BaseDiGraphTester(BaseGraphTester): + def test_has_successor(self): + G = self.K3 + assert G.has_successor(0, 1) + assert not G.has_successor(0, -1) + + def test_successors(self): + G = self.K3 + assert sorted(G.successors(0)) == [1, 2] + with pytest.raises(nx.NetworkXError): + G.successors(-1) + + def test_has_predecessor(self): + G = self.K3 + assert G.has_predecessor(0, 1) + assert not G.has_predecessor(0, -1) + + def test_predecessors(self): + G = self.K3 + assert sorted(G.predecessors(0)) == [1, 2] + with pytest.raises(nx.NetworkXError): + G.predecessors(-1) + + def test_edges(self): + G = self.K3 + assert sorted(G.edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.edges(0)) == [(0, 1), (0, 2)] + assert sorted(G.edges([0, 1])) == [(0, 1), (0, 2), (1, 0), (1, 2)] + with pytest.raises(nx.NetworkXError): + G.edges(-1) + + def test_out_edges(self): + G = self.K3 + assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)] + with pytest.raises(nx.NetworkXError): + G.out_edges(-1) + + def test_out_edges_dir(self): + G = self.P3 + assert sorted(G.out_edges()) == [(0, 1), (1, 2)] + assert sorted(G.out_edges(0)) == [(0, 1)] + assert sorted(G.out_edges(2)) == [] + + def test_out_edges_data(self): + G = nx.DiGraph([(0, 1, {"data": 0}), (1, 0, {})]) + assert sorted(G.out_edges(data=True)) == [(0, 1, {"data": 0}), (1, 0, {})] + assert sorted(G.out_edges(0, data=True)) == [(0, 1, {"data": 0})] + assert sorted(G.out_edges(data="data")) == [(0, 1, 0), (1, 0, None)] + assert sorted(G.out_edges(0, data="data")) == [(0, 1, 0)] + + def test_in_edges_dir(self): + G = self.P3 + assert sorted(G.in_edges()) == [(0, 1), (1, 2)] + assert sorted(G.in_edges(0)) == [] + assert sorted(G.in_edges(2)) == [(1, 2)] + + def test_in_edges_data(self): + G = nx.DiGraph([(0, 1, {"data": 0}), (1, 0, {})]) + assert sorted(G.in_edges(data=True)) == [(0, 1, {"data": 0}), (1, 0, {})] + assert sorted(G.in_edges(1, data=True)) == [(0, 1, {"data": 0})] + assert sorted(G.in_edges(data="data")) == [(0, 1, 0), (1, 0, None)] + assert sorted(G.in_edges(1, data="data")) == [(0, 1, 0)] + + def test_degree(self): + G = self.K3 + assert sorted(G.degree()) == [(0, 4), (1, 4), (2, 4)] + assert dict(G.degree()) == {0: 4, 1: 4, 2: 4} + assert G.degree(0) == 4 + assert list(G.degree(iter([0]))) == [(0, 4)] # run through iterator + + def test_in_degree(self): + G = self.K3 + assert sorted(G.in_degree()) == [(0, 2), (1, 2), (2, 2)] + assert dict(G.in_degree()) == {0: 2, 1: 2, 2: 2} + assert G.in_degree(0) == 2 + assert list(G.in_degree(iter([0]))) == [(0, 2)] # run through iterator + + def test_out_degree(self): + G = self.K3 + assert sorted(G.out_degree()) == [(0, 2), (1, 2), (2, 2)] + assert dict(G.out_degree()) == {0: 2, 1: 2, 2: 2} + assert G.out_degree(0) == 2 + assert list(G.out_degree(iter([0]))) == [(0, 2)] + + def test_size(self): + G = self.K3 + assert G.size() == 6 + assert G.number_of_edges() == 6 + + def test_to_undirected_reciprocal(self): + G = self.Graph() + G.add_edge(1, 2) + assert G.to_undirected().has_edge(1, 2) + assert not G.to_undirected(reciprocal=True).has_edge(1, 2) + G.add_edge(2, 1) + assert G.to_undirected(reciprocal=True).has_edge(1, 2) + + def test_reverse_copy(self): + G = nx.DiGraph([(0, 1), (1, 2)]) + R = G.reverse() + assert sorted(R.edges()) == [(1, 0), (2, 1)] + R.remove_edge(1, 0) + assert sorted(R.edges()) == [(2, 1)] + assert sorted(G.edges()) == [(0, 1), (1, 2)] + + def test_reverse_nocopy(self): + G = nx.DiGraph([(0, 1), (1, 2)]) + R = G.reverse(copy=False) + assert sorted(R.edges()) == [(1, 0), (2, 1)] + with pytest.raises(nx.NetworkXError): + R.remove_edge(1, 0) + + def test_reverse_hashable(self): + class Foo: + pass + + x = Foo() + y = Foo() + G = nx.DiGraph() + G.add_edge(x, y) + assert nodes_equal(G.nodes(), G.reverse().nodes()) + assert [(y, x)] == list(G.reverse().edges()) + + def test_di_cache_reset(self): + G = self.K3.copy() + old_succ = G.succ + assert id(G.succ) == id(old_succ) + old_adj = G.adj + assert id(G.adj) == id(old_adj) + + G._succ = {} + assert id(G.succ) != id(old_succ) + assert id(G.adj) != id(old_adj) + + old_pred = G.pred + assert id(G.pred) == id(old_pred) + G._pred = {} + assert id(G.pred) != id(old_pred) + + def test_di_attributes_cached(self): + G = self.K3.copy() + assert id(G.in_edges) == id(G.in_edges) + assert id(G.out_edges) == id(G.out_edges) + assert id(G.in_degree) == id(G.in_degree) + assert id(G.out_degree) == id(G.out_degree) + assert id(G.succ) == id(G.succ) + assert id(G.pred) == id(G.pred) + + +class BaseAttrDiGraphTester(BaseDiGraphTester, BaseAttrGraphTester): + def test_edges_data(self): + G = self.K3 + all_edges = [ + (0, 1, {}), + (0, 2, {}), + (1, 0, {}), + (1, 2, {}), + (2, 0, {}), + (2, 1, {}), + ] + assert sorted(G.edges(data=True)) == all_edges + assert sorted(G.edges(0, data=True)) == all_edges[:2] + assert sorted(G.edges([0, 1], data=True)) == all_edges[:4] + with pytest.raises(nx.NetworkXError): + G.edges(-1, True) + + def test_in_degree_weighted(self): + G = self.K3.copy() + G.add_edge(0, 1, weight=0.3, other=1.2) + assert sorted(G.in_degree(weight="weight")) == [(0, 2), (1, 1.3), (2, 2)] + assert dict(G.in_degree(weight="weight")) == {0: 2, 1: 1.3, 2: 2} + assert G.in_degree(1, weight="weight") == 1.3 + assert sorted(G.in_degree(weight="other")) == [(0, 2), (1, 2.2), (2, 2)] + assert dict(G.in_degree(weight="other")) == {0: 2, 1: 2.2, 2: 2} + assert G.in_degree(1, weight="other") == 2.2 + assert list(G.in_degree(iter([1]), weight="other")) == [(1, 2.2)] + + def test_out_degree_weighted(self): + G = self.K3.copy() + G.add_edge(0, 1, weight=0.3, other=1.2) + assert sorted(G.out_degree(weight="weight")) == [(0, 1.3), (1, 2), (2, 2)] + assert dict(G.out_degree(weight="weight")) == {0: 1.3, 1: 2, 2: 2} + assert G.out_degree(0, weight="weight") == 1.3 + assert sorted(G.out_degree(weight="other")) == [(0, 2.2), (1, 2), (2, 2)] + assert dict(G.out_degree(weight="other")) == {0: 2.2, 1: 2, 2: 2} + assert G.out_degree(0, weight="other") == 2.2 + assert list(G.out_degree(iter([0]), weight="other")) == [(0, 2.2)] + + +class TestDiGraph(BaseAttrDiGraphTester, _TestGraph): + """Tests specific to dict-of-dict-of-dict digraph data structure""" + + def setup_method(self): + self.Graph = nx.DiGraph + # build dict-of-dict-of-dict K3 + ed1, ed2, ed3, ed4, ed5, ed6 = ({}, {}, {}, {}, {}, {}) + self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed3, 2: ed4}, 2: {0: ed5, 1: ed6}} + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._succ = self.k3adj # K3._adj is synced with K3._succ + self.K3._pred = {0: {1: ed3, 2: ed5}, 1: {0: ed1, 2: ed6}, 2: {0: ed2, 1: ed4}} + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + ed1, ed2 = ({}, {}) + self.P3 = self.Graph() + self.P3._succ = {0: {1: ed1}, 1: {2: ed2}, 2: {}} + self.P3._pred = {0: {}, 1: {0: ed1}, 2: {1: ed2}} + # P3._adj is synced with P3._succ + self.P3._node = {} + self.P3._node[0] = {} + self.P3._node[1] = {} + self.P3._node[2] = {} + + def test_data_input(self): + G = self.Graph({1: [2], 2: [1]}, name="test") + assert G.name == "test" + assert sorted(G.adj.items()) == [(1, {2: {}}), (2, {1: {}})] + assert sorted(G.succ.items()) == [(1, {2: {}}), (2, {1: {}})] + assert sorted(G.pred.items()) == [(1, {2: {}}), (2, {1: {}})] + + def test_add_edge(self): + G = self.Graph() + G.add_edge(0, 1) + assert G.adj == {0: {1: {}}, 1: {}} + assert G.succ == {0: {1: {}}, 1: {}} + assert G.pred == {0: {}, 1: {0: {}}} + G = self.Graph() + G.add_edge(*(0, 1)) + assert G.adj == {0: {1: {}}, 1: {}} + assert G.succ == {0: {1: {}}, 1: {}} + assert G.pred == {0: {}, 1: {0: {}}} + with pytest.raises(ValueError, match="None cannot be a node"): + G.add_edge(None, 3) + + def test_add_edges_from(self): + G = self.Graph() + G.add_edges_from([(0, 1), (0, 2, {"data": 3})], data=2) + assert G.adj == {0: {1: {"data": 2}, 2: {"data": 3}}, 1: {}, 2: {}} + assert G.succ == {0: {1: {"data": 2}, 2: {"data": 3}}, 1: {}, 2: {}} + assert G.pred == {0: {}, 1: {0: {"data": 2}}, 2: {0: {"data": 3}}} + + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0,)]) # too few in tuple + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0, 1, 2, 3)]) # too many in tuple + with pytest.raises(TypeError): + G.add_edges_from([0]) # not a tuple + with pytest.raises(ValueError, match="None cannot be a node"): + G.add_edges_from([(None, 3), (3, 2)]) + + def test_remove_edge(self): + G = self.K3.copy() + G.remove_edge(0, 1) + assert G.succ == {0: {2: {}}, 1: {0: {}, 2: {}}, 2: {0: {}, 1: {}}} + assert G.pred == {0: {1: {}, 2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}} + with pytest.raises(nx.NetworkXError): + G.remove_edge(-1, 0) + + def test_remove_edges_from(self): + G = self.K3.copy() + G.remove_edges_from([(0, 1)]) + assert G.succ == {0: {2: {}}, 1: {0: {}, 2: {}}, 2: {0: {}, 1: {}}} + assert G.pred == {0: {1: {}, 2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}} + G.remove_edges_from([(0, 0)]) # silent fail + + def test_clear(self): + G = self.K3 + G.graph["name"] = "K3" + G.clear() + assert list(G.nodes) == [] + assert G.succ == {} + assert G.pred == {} + assert G.graph == {} + + def test_clear_edges(self): + G = self.K3 + G.graph["name"] = "K3" + nodes = list(G.nodes) + G.clear_edges() + assert list(G.nodes) == nodes + expected = {0: {}, 1: {}, 2: {}} + assert G.succ == expected + assert G.pred == expected + assert list(G.edges) == [] + assert G.graph["name"] == "K3" + + +class TestEdgeSubgraph(_TestGraphEdgeSubgraph): + """Unit tests for the :meth:`DiGraph.edge_subgraph` method.""" + + def setup_method(self): + # Create a doubly-linked path graph on five nodes. + G = nx.DiGraph(nx.path_graph(5)) + # Add some node, edge, and graph attributes. + for i in range(5): + G.nodes[i]["name"] = f"node{i}" + G.edges[0, 1]["name"] = "edge01" + G.edges[3, 4]["name"] = "edge34" + G.graph["name"] = "graph" + # Get the subgraph induced by the first and last edges. + self.G = G + self.H = G.edge_subgraph([(0, 1), (3, 4)]) + + def test_pred_succ(self): + """Test that nodes are added to predecessors and successors. + + For more information, see GitHub issue #2370. + + """ + G = nx.DiGraph() + G.add_edge(0, 1) + H = G.edge_subgraph([(0, 1)]) + assert list(H.predecessors(0)) == [] + assert list(H.successors(0)) == [1] + assert list(H.predecessors(1)) == [0] + assert list(H.successors(1)) == [] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py new file mode 100644 index 0000000000000000000000000000000000000000..ff9f9e9cc4e698a70a6123452fde20069a09f0a4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_digraph_historical.py @@ -0,0 +1,110 @@ +"""Original NetworkX graph tests""" + +import pytest + +import networkx as nx + +from .historical_tests import HistoricalTests + + +class TestDiGraphHistorical(HistoricalTests): + @classmethod + def setup_class(cls): + HistoricalTests.setup_class() + cls.G = nx.DiGraph + + def test_in_degree(self): + G = self.G() + G.add_nodes_from("GJK") + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")]) + + assert sorted(d for n, d in G.in_degree()) == [0, 0, 0, 0, 1, 2, 2] + assert dict(G.in_degree()) == { + "A": 0, + "C": 2, + "B": 1, + "D": 2, + "G": 0, + "K": 0, + "J": 0, + } + + def test_out_degree(self): + G = self.G() + G.add_nodes_from("GJK") + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")]) + assert sorted(v for k, v in G.in_degree()) == [0, 0, 0, 0, 1, 2, 2] + assert dict(G.out_degree()) == { + "A": 2, + "C": 1, + "B": 2, + "D": 0, + "G": 0, + "K": 0, + "J": 0, + } + + def test_degree_digraph(self): + H = nx.DiGraph() + H.add_edges_from([(1, 24), (1, 2)]) + assert sorted(d for n, d in H.in_degree([1, 24])) == [0, 1] + assert sorted(d for n, d in H.out_degree([1, 24])) == [0, 2] + assert sorted(d for n, d in H.degree([1, 24])) == [1, 2] + + def test_neighbors(self): + G = self.G() + G.add_nodes_from("GJK") + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")]) + + assert sorted(G.neighbors("C")) == ["D"] + assert sorted(G["C"]) == ["D"] + assert sorted(G.neighbors("A")) == ["B", "C"] + pytest.raises(nx.NetworkXError, G.neighbors, "j") + pytest.raises(nx.NetworkXError, G.neighbors, "j") + + def test_successors(self): + G = self.G() + G.add_nodes_from("GJK") + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")]) + assert sorted(G.successors("A")) == ["B", "C"] + assert sorted(G.successors("A")) == ["B", "C"] + assert sorted(G.successors("G")) == [] + assert sorted(G.successors("D")) == [] + assert sorted(G.successors("G")) == [] + pytest.raises(nx.NetworkXError, G.successors, "j") + pytest.raises(nx.NetworkXError, G.successors, "j") + + def test_predecessors(self): + G = self.G() + G.add_nodes_from("GJK") + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("B", "C"), ("C", "D")]) + assert sorted(G.predecessors("C")) == ["A", "B"] + assert sorted(G.predecessors("C")) == ["A", "B"] + assert sorted(G.predecessors("G")) == [] + assert sorted(G.predecessors("A")) == [] + assert sorted(G.predecessors("G")) == [] + assert sorted(G.predecessors("A")) == [] + assert sorted(G.successors("D")) == [] + + pytest.raises(nx.NetworkXError, G.predecessors, "j") + pytest.raises(nx.NetworkXError, G.predecessors, "j") + + def test_reverse(self): + G = nx.complete_graph(10) + H = G.to_directed() + HR = H.reverse() + assert nx.is_isomorphic(H, HR) + assert sorted(H.edges()) == sorted(HR.edges()) + + def test_reverse2(self): + H = nx.DiGraph() + foo = [H.add_edge(u, u + 1) for u in range(5)] + HR = H.reverse() + for u in range(5): + assert HR.has_edge(u + 1, u) + + def test_reverse3(self): + H = nx.DiGraph() + H.add_nodes_from([1, 2, 3, 4]) + HR = H.reverse() + assert sorted(HR.nodes()) == [1, 2, 3, 4] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py new file mode 100644 index 0000000000000000000000000000000000000000..2da59117cad0d72d5830b53c8d19c6e0ca988d54 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_filters.py @@ -0,0 +1,177 @@ +import pytest + +import networkx as nx + + +class TestFilterFactory: + def test_no_filter(self): + nf = nx.filters.no_filter + assert nf() + assert nf(1) + assert nf(2, 1) + + def test_hide_nodes(self): + f = nx.classes.filters.hide_nodes([1, 2, 3]) + assert not f(1) + assert not f(2) + assert not f(3) + assert f(4) + assert f(0) + assert f("a") + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f) + + def test_show_nodes(self): + f = nx.classes.filters.show_nodes([1, 2, 3]) + assert f(1) + assert f(2) + assert f(3) + assert not f(4) + assert not f(0) + assert not f("a") + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f) + + def test_hide_edges(self): + factory = nx.classes.filters.hide_edges + f = factory([(1, 2), (3, 4)]) + assert not f(1, 2) + assert not f(3, 4) + assert not f(4, 3) + assert f(2, 3) + assert f(0, -1) + assert f("a", "b") + pytest.raises(TypeError, f, 1, 2, 3) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2, 3)]) + + def test_show_edges(self): + factory = nx.classes.filters.show_edges + f = factory([(1, 2), (3, 4)]) + assert f(1, 2) + assert f(3, 4) + assert f(4, 3) + assert not f(2, 3) + assert not f(0, -1) + assert not f("a", "b") + pytest.raises(TypeError, f, 1, 2, 3) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2, 3)]) + + def test_hide_diedges(self): + factory = nx.classes.filters.hide_diedges + f = factory([(1, 2), (3, 4)]) + assert not f(1, 2) + assert not f(3, 4) + assert f(4, 3) + assert f(2, 3) + assert f(0, -1) + assert f("a", "b") + pytest.raises(TypeError, f, 1, 2, 3) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2, 3)]) + + def test_show_diedges(self): + factory = nx.classes.filters.show_diedges + f = factory([(1, 2), (3, 4)]) + assert f(1, 2) + assert f(3, 4) + assert not f(4, 3) + assert not f(2, 3) + assert not f(0, -1) + assert not f("a", "b") + pytest.raises(TypeError, f, 1, 2, 3) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2, 3)]) + + def test_hide_multiedges(self): + factory = nx.classes.filters.hide_multiedges + f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)]) + assert not f(1, 2, 0) + assert not f(1, 2, 1) + assert f(1, 2, 2) + assert f(3, 4, 0) + assert not f(3, 4, 1) + assert not f(4, 3, 1) + assert f(4, 3, 0) + assert f(2, 3, 0) + assert f(0, -1, 0) + assert f("a", "b", 0) + pytest.raises(TypeError, f, 1, 2, 3, 4) + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2)]) + pytest.raises(ValueError, factory, [(1, 2, 3, 4)]) + + def test_show_multiedges(self): + factory = nx.classes.filters.show_multiedges + f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)]) + assert f(1, 2, 0) + assert f(1, 2, 1) + assert not f(1, 2, 2) + assert not f(3, 4, 0) + assert f(3, 4, 1) + assert f(4, 3, 1) + assert not f(4, 3, 0) + assert not f(2, 3, 0) + assert not f(0, -1, 0) + assert not f("a", "b", 0) + pytest.raises(TypeError, f, 1, 2, 3, 4) + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2)]) + pytest.raises(ValueError, factory, [(1, 2, 3, 4)]) + + def test_hide_multidiedges(self): + factory = nx.classes.filters.hide_multidiedges + f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)]) + assert not f(1, 2, 0) + assert not f(1, 2, 1) + assert f(1, 2, 2) + assert f(3, 4, 0) + assert not f(3, 4, 1) + assert f(4, 3, 1) + assert f(4, 3, 0) + assert f(2, 3, 0) + assert f(0, -1, 0) + assert f("a", "b", 0) + pytest.raises(TypeError, f, 1, 2, 3, 4) + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2)]) + pytest.raises(ValueError, factory, [(1, 2, 3, 4)]) + + def test_show_multidiedges(self): + factory = nx.classes.filters.show_multidiedges + f = factory([(1, 2, 0), (3, 4, 1), (1, 2, 1)]) + assert f(1, 2, 0) + assert f(1, 2, 1) + assert not f(1, 2, 2) + assert not f(3, 4, 0) + assert f(3, 4, 1) + assert not f(4, 3, 1) + assert not f(4, 3, 0) + assert not f(2, 3, 0) + assert not f(0, -1, 0) + assert not f("a", "b", 0) + pytest.raises(TypeError, f, 1, 2, 3, 4) + pytest.raises(TypeError, f, 1, 2) + pytest.raises(TypeError, f, 1) + pytest.raises(TypeError, f) + pytest.raises(TypeError, factory, [1, 2, 3]) + pytest.raises(ValueError, factory, [(1, 2)]) + pytest.raises(ValueError, factory, [(1, 2, 3, 4)]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_function.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_function.py new file mode 100644 index 0000000000000000000000000000000000000000..8d875c84c0de403ad0f4294913ad04f4c5c8220c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_function.py @@ -0,0 +1,1045 @@ +import random + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + + +def test_degree_histogram_empty(): + G = nx.Graph() + assert nx.degree_histogram(G) == [] + + +class TestFunction: + def setup_method(self): + self.G = nx.Graph({0: [1, 2, 3], 1: [1, 2, 0], 4: []}, name="Test") + self.Gdegree = {0: 3, 1: 2, 2: 2, 3: 1, 4: 0} + self.Gnodes = list(range(5)) + self.Gedges = [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)] + self.DG = nx.DiGraph({0: [1, 2, 3], 1: [1, 2, 0], 4: []}) + self.DGin_degree = {0: 1, 1: 2, 2: 2, 3: 1, 4: 0} + self.DGout_degree = {0: 3, 1: 3, 2: 0, 3: 0, 4: 0} + self.DGnodes = list(range(5)) + self.DGedges = [(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)] + + def test_describe_info_dict(self): + info_dict = nx.classes.function._create_describe_info_dict(self.G) + assert info_dict["Name of Graph"] == "Test" + assert not info_dict["Directed"] + assert not info_dict["Multigraph"] + assert info_dict["Number of nodes"] == 5 + assert info_dict["Number of edges"] == 5 + assert info_dict["Average degree (min, max)"] == "2.00 (0, 4)" + assert info_dict["Number of connected components"] == 2 + + def test_nodes(self): + assert nodes_equal(self.G.nodes(), list(nx.nodes(self.G))) + assert nodes_equal(self.DG.nodes(), list(nx.nodes(self.DG))) + + def test_edges(self): + assert edges_equal(self.G.edges(), list(nx.edges(self.G))) + assert sorted(self.DG.edges()) == sorted(nx.edges(self.DG)) + assert edges_equal( + self.G.edges(nbunch=[0, 1, 3]), list(nx.edges(self.G, nbunch=[0, 1, 3])) + ) + assert sorted(self.DG.edges(nbunch=[0, 1, 3])) == sorted( + nx.edges(self.DG, nbunch=[0, 1, 3]) + ) + + def test_degree(self): + assert edges_equal(self.G.degree(), list(nx.degree(self.G))) + assert sorted(self.DG.degree()) == sorted(nx.degree(self.DG)) + assert edges_equal( + self.G.degree(nbunch=[0, 1]), list(nx.degree(self.G, nbunch=[0, 1])) + ) + assert sorted(self.DG.degree(nbunch=[0, 1])) == sorted( + nx.degree(self.DG, nbunch=[0, 1]) + ) + assert edges_equal( + self.G.degree(weight="weight"), list(nx.degree(self.G, weight="weight")) + ) + assert sorted(self.DG.degree(weight="weight")) == sorted( + nx.degree(self.DG, weight="weight") + ) + + def test_neighbors(self): + assert list(self.G.neighbors(1)) == list(nx.neighbors(self.G, 1)) + assert list(self.DG.neighbors(1)) == list(nx.neighbors(self.DG, 1)) + + def test_number_of_nodes(self): + assert self.G.number_of_nodes() == nx.number_of_nodes(self.G) + assert self.DG.number_of_nodes() == nx.number_of_nodes(self.DG) + + def test_number_of_edges(self): + assert self.G.number_of_edges() == nx.number_of_edges(self.G) + assert self.DG.number_of_edges() == nx.number_of_edges(self.DG) + + def test_is_directed(self): + assert self.G.is_directed() == nx.is_directed(self.G) + assert self.DG.is_directed() == nx.is_directed(self.DG) + + def test_add_star(self): + G = self.G.copy() + nlist = [12, 13, 14, 15] + nx.add_star(G, nlist) + assert edges_equal(G.edges(nlist), [(12, 13), (12, 14), (12, 15)]) + + G = self.G.copy() + nx.add_star(G, nlist, weight=2.0) + assert edges_equal( + G.edges(nlist, data=True), + [ + (12, 13, {"weight": 2.0}), + (12, 14, {"weight": 2.0}), + (12, 15, {"weight": 2.0}), + ], + ) + + G = self.G.copy() + nlist = [12] + nx.add_star(G, nlist) + assert nodes_equal(G, list(self.G) + nlist) + + G = self.G.copy() + nlist = [] + nx.add_star(G, nlist) + assert nodes_equal(G.nodes, self.Gnodes) + assert edges_equal(G.edges, self.G.edges) + + def test_add_path(self): + G = self.G.copy() + nlist = [12, 13, 14, 15] + nx.add_path(G, nlist) + assert edges_equal(G.edges(nlist), [(12, 13), (13, 14), (14, 15)]) + G = self.G.copy() + nx.add_path(G, nlist, weight=2.0) + assert edges_equal( + G.edges(nlist, data=True), + [ + (12, 13, {"weight": 2.0}), + (13, 14, {"weight": 2.0}), + (14, 15, {"weight": 2.0}), + ], + ) + + G = self.G.copy() + nlist = ["node"] + nx.add_path(G, nlist) + assert edges_equal(G.edges(nlist), []) + assert nodes_equal(G, list(self.G) + ["node"]) + + G = self.G.copy() + nlist = iter(["node"]) + nx.add_path(G, nlist) + assert edges_equal(G.edges(["node"]), []) + assert nodes_equal(G, list(self.G) + ["node"]) + + G = self.G.copy() + nlist = [12] + nx.add_path(G, nlist) + assert edges_equal(G.edges(nlist), []) + assert nodes_equal(G, list(self.G) + [12]) + + G = self.G.copy() + nlist = iter([12]) + nx.add_path(G, nlist) + assert edges_equal(G.edges([12]), []) + assert nodes_equal(G, list(self.G) + [12]) + + G = self.G.copy() + nlist = [] + nx.add_path(G, nlist) + assert edges_equal(G.edges, self.G.edges) + assert nodes_equal(G, list(self.G)) + + G = self.G.copy() + nlist = iter([]) + nx.add_path(G, nlist) + assert edges_equal(G.edges, self.G.edges) + assert nodes_equal(G, list(self.G)) + + def test_add_cycle(self): + G = self.G.copy() + nlist = [12, 13, 14, 15] + oklists = [ + [(12, 13), (12, 15), (13, 14), (14, 15)], + [(12, 13), (13, 14), (14, 15), (15, 12)], + ] + nx.add_cycle(G, nlist) + assert sorted(G.edges(nlist)) in oklists + G = self.G.copy() + oklists = [ + [ + (12, 13, {"weight": 1.0}), + (12, 15, {"weight": 1.0}), + (13, 14, {"weight": 1.0}), + (14, 15, {"weight": 1.0}), + ], + [ + (12, 13, {"weight": 1.0}), + (13, 14, {"weight": 1.0}), + (14, 15, {"weight": 1.0}), + (15, 12, {"weight": 1.0}), + ], + ] + nx.add_cycle(G, nlist, weight=1.0) + assert sorted(G.edges(nlist, data=True)) in oklists + + G = self.G.copy() + nlist = [12] + nx.add_cycle(G, nlist) + assert nodes_equal(G, list(self.G) + nlist) + + G = self.G.copy() + nlist = [] + nx.add_cycle(G, nlist) + assert nodes_equal(G.nodes, self.Gnodes) + assert edges_equal(G.edges, self.G.edges) + + def test_subgraph(self): + assert ( + self.G.subgraph([0, 1, 2, 4]).adj == nx.subgraph(self.G, [0, 1, 2, 4]).adj + ) + assert ( + self.DG.subgraph([0, 1, 2, 4]).adj == nx.subgraph(self.DG, [0, 1, 2, 4]).adj + ) + assert ( + self.G.subgraph([0, 1, 2, 4]).adj + == nx.induced_subgraph(self.G, [0, 1, 2, 4]).adj + ) + assert ( + self.DG.subgraph([0, 1, 2, 4]).adj + == nx.induced_subgraph(self.DG, [0, 1, 2, 4]).adj + ) + # subgraph-subgraph chain is allowed in function interface + H = nx.induced_subgraph(self.G.subgraph([0, 1, 2, 4]), [0, 1, 4]) + assert H._graph is not self.G + assert H.adj == self.G.subgraph([0, 1, 4]).adj + + def test_edge_subgraph(self): + assert ( + self.G.edge_subgraph([(1, 2), (0, 3)]).adj + == nx.edge_subgraph(self.G, [(1, 2), (0, 3)]).adj + ) + assert ( + self.DG.edge_subgraph([(1, 2), (0, 3)]).adj + == nx.edge_subgraph(self.DG, [(1, 2), (0, 3)]).adj + ) + + def test_create_empty_copy(self): + G = nx.create_empty_copy(self.G, with_data=False) + assert nodes_equal(G, list(self.G)) + assert G.graph == {} + assert G._node == {}.fromkeys(self.G.nodes(), {}) + assert G._adj == {}.fromkeys(self.G.nodes(), {}) + G = nx.create_empty_copy(self.G) + assert nodes_equal(G, list(self.G)) + assert G.graph == self.G.graph + assert G._node == self.G._node + assert G._adj == {}.fromkeys(self.G.nodes(), {}) + + def test_degree_histogram(self): + assert nx.degree_histogram(self.G) == [1, 1, 1, 1, 1] + + def test_density(self): + assert nx.density(self.G) == 0.5 + assert nx.density(self.DG) == 0.3 + G = nx.Graph() + G.add_node(1) + assert nx.density(G) == 0.0 + + def test_density_selfloop(self): + G = nx.Graph() + G.add_edge(1, 1) + assert nx.density(G) == 0.0 + G.add_edge(1, 2) + assert nx.density(G) == 2.0 + + def test_freeze(self): + G = nx.freeze(self.G) + assert G.frozen + pytest.raises(nx.NetworkXError, G.add_node, 1) + pytest.raises(nx.NetworkXError, G.add_nodes_from, [1]) + pytest.raises(nx.NetworkXError, G.remove_node, 1) + pytest.raises(nx.NetworkXError, G.remove_nodes_from, [1]) + pytest.raises(nx.NetworkXError, G.add_edge, 1, 2) + pytest.raises(nx.NetworkXError, G.add_edges_from, [(1, 2)]) + pytest.raises(nx.NetworkXError, G.remove_edge, 1, 2) + pytest.raises(nx.NetworkXError, G.remove_edges_from, [(1, 2)]) + pytest.raises(nx.NetworkXError, G.clear_edges) + pytest.raises(nx.NetworkXError, G.clear) + + def test_is_frozen(self): + assert not nx.is_frozen(self.G) + G = nx.freeze(self.G) + assert G.frozen == nx.is_frozen(self.G) + assert G.frozen + + def test_node_attributes_are_still_mutable_on_frozen_graph(self): + G = nx.freeze(nx.path_graph(3)) + node = G.nodes[0] + node["node_attribute"] = True + assert node["node_attribute"] is True + + def test_edge_attributes_are_still_mutable_on_frozen_graph(self): + G = nx.freeze(nx.path_graph(3)) + edge = G.edges[(0, 1)] + edge["edge_attribute"] = True + assert edge["edge_attribute"] is True + + def test_neighbors_complete_graph(self): + graph = nx.complete_graph(100) + pop = random.sample(list(graph), 1) + nbors = list(nx.neighbors(graph, pop[0])) + # should be all the other vertices in the graph + assert len(nbors) == len(graph) - 1 + + graph = nx.path_graph(100) + node = random.sample(list(graph), 1)[0] + nbors = list(nx.neighbors(graph, node)) + # should be all the other vertices in the graph + if node != 0 and node != 99: + assert len(nbors) == 2 + else: + assert len(nbors) == 1 + + # create a star graph with 99 outer nodes + graph = nx.star_graph(99) + nbors = list(nx.neighbors(graph, 0)) + assert len(nbors) == 99 + + def test_non_neighbors(self): + graph = nx.complete_graph(100) + pop = random.sample(list(graph), 1) + nbors = nx.non_neighbors(graph, pop[0]) + # should be all the other vertices in the graph + assert len(nbors) == 0 + + graph = nx.path_graph(100) + node = random.sample(list(graph), 1)[0] + nbors = nx.non_neighbors(graph, node) + # should be all the other vertices in the graph + if node != 0 and node != 99: + assert len(nbors) == 97 + else: + assert len(nbors) == 98 + + # create a star graph with 99 outer nodes + graph = nx.star_graph(99) + nbors = nx.non_neighbors(graph, 0) + assert len(nbors) == 0 + + # disconnected graph + graph = nx.Graph() + graph.add_nodes_from(range(10)) + nbors = nx.non_neighbors(graph, 0) + assert len(nbors) == 9 + + def test_non_edges(self): + # All possible edges exist + graph = nx.complete_graph(5) + nedges = list(nx.non_edges(graph)) + assert len(nedges) == 0 + + graph = nx.path_graph(4) + expected = [(0, 2), (0, 3), (1, 3)] + nedges = list(nx.non_edges(graph)) + for u, v in expected: + assert (u, v) in nedges or (v, u) in nedges + + graph = nx.star_graph(4) + expected = [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + nedges = list(nx.non_edges(graph)) + for u, v in expected: + assert (u, v) in nedges or (v, u) in nedges + + # Directed graphs + graph = nx.DiGraph() + graph.add_edges_from([(0, 2), (2, 0), (2, 1)]) + expected = [(0, 1), (1, 0), (1, 2)] + nedges = list(nx.non_edges(graph)) + for e in expected: + assert e in nedges + + def test_is_weighted(self): + G = nx.Graph() + assert not nx.is_weighted(G) + + G = nx.path_graph(4) + assert not nx.is_weighted(G) + assert not nx.is_weighted(G, (2, 3)) + + G.add_node(4) + G.add_edge(3, 4, weight=4) + assert not nx.is_weighted(G) + assert nx.is_weighted(G, (3, 4)) + + G = nx.DiGraph() + G.add_weighted_edges_from( + [ + ("0", "3", 3), + ("0", "1", -5), + ("1", "0", -5), + ("0", "2", 2), + ("1", "2", 4), + ("2", "3", 1), + ] + ) + assert nx.is_weighted(G) + assert nx.is_weighted(G, ("1", "0")) + + G = G.to_undirected() + assert nx.is_weighted(G) + assert nx.is_weighted(G, ("1", "0")) + + pytest.raises(nx.NetworkXError, nx.is_weighted, G, (1, 2)) + + def test_is_negatively_weighted(self): + G = nx.Graph() + assert not nx.is_negatively_weighted(G) + + G.add_node(1) + G.add_nodes_from([2, 3, 4, 5]) + assert not nx.is_negatively_weighted(G) + + G.add_edge(1, 2, weight=4) + assert not nx.is_negatively_weighted(G, (1, 2)) + + G.add_edges_from([(1, 3), (2, 4), (2, 6)]) + G[1][3]["color"] = "blue" + assert not nx.is_negatively_weighted(G) + assert not nx.is_negatively_weighted(G, (1, 3)) + + G[2][4]["weight"] = -2 + assert nx.is_negatively_weighted(G, (2, 4)) + assert nx.is_negatively_weighted(G) + + G = nx.DiGraph() + G.add_weighted_edges_from( + [ + ("0", "3", 3), + ("0", "1", -5), + ("1", "0", -2), + ("0", "2", 2), + ("1", "2", -3), + ("2", "3", 1), + ] + ) + assert nx.is_negatively_weighted(G) + assert not nx.is_negatively_weighted(G, ("0", "3")) + assert nx.is_negatively_weighted(G, ("1", "0")) + + pytest.raises(nx.NetworkXError, nx.is_negatively_weighted, G, (1, 4)) + + +class TestCommonNeighbors: + @classmethod + def setup_class(cls): + cls.func = staticmethod(nx.common_neighbors) + + def test_func(G, u, v, expected): + result = sorted(cls.func(G, u, v)) + assert result == expected + + cls.test = staticmethod(test_func) + + def test_K5(self): + G = nx.complete_graph(5) + self.test(G, 0, 1, [2, 3, 4]) + + def test_P3(self): + G = nx.path_graph(3) + self.test(G, 0, 2, [1]) + + def test_S4(self): + G = nx.star_graph(4) + self.test(G, 1, 2, [0]) + + def test_digraph(self): + with pytest.raises(nx.NetworkXNotImplemented): + G = nx.DiGraph() + G.add_edges_from([(0, 1), (1, 2)]) + self.func(G, 0, 2) + + def test_nonexistent_nodes(self): + G = nx.complete_graph(5) + pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 5, 4) + pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 4, 5) + pytest.raises(nx.NetworkXError, nx.common_neighbors, G, 5, 6) + + def test_custom1(self): + """Case of no common neighbors.""" + G = nx.Graph() + G.add_nodes_from([0, 1]) + self.test(G, 0, 1, []) + + def test_custom2(self): + """Case of equal nodes.""" + G = nx.complete_graph(4) + self.test(G, 0, 0, [1, 2, 3]) + + +@pytest.mark.parametrize( + "graph_type", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph) +) +def test_set_node_attributes(graph_type): + # Test single value + G = nx.path_graph(3, create_using=graph_type) + vals = 100 + attr = "hello" + nx.set_node_attributes(G, vals, attr) + assert G.nodes[0][attr] == vals + assert G.nodes[1][attr] == vals + assert G.nodes[2][attr] == vals + + # Test dictionary + G = nx.path_graph(3, create_using=graph_type) + vals = dict(zip(sorted(G.nodes()), range(len(G)))) + attr = "hi" + nx.set_node_attributes(G, vals, attr) + assert G.nodes[0][attr] == 0 + assert G.nodes[1][attr] == 1 + assert G.nodes[2][attr] == 2 + + # Test dictionary of dictionaries + G = nx.path_graph(3, create_using=graph_type) + d = {"hi": 0, "hello": 200} + vals = dict.fromkeys(G.nodes(), d) + vals.pop(0) + nx.set_node_attributes(G, vals) + assert G.nodes[0] == {} + assert G.nodes[1]["hi"] == 0 + assert G.nodes[2]["hello"] == 200 + + +@pytest.mark.parametrize( + ("values", "name"), + ( + ({0: "red", 1: "blue"}, "color"), # values dictionary + ({0: {"color": "red"}, 1: {"color": "blue"}}, None), # dict-of-dict + ), +) +def test_set_node_attributes_ignores_extra_nodes(values, name): + """ + When `values` is a dict or dict-of-dict keyed by nodes, ensure that keys + that correspond to nodes not in G are ignored. + """ + G = nx.Graph() + G.add_node(0) + nx.set_node_attributes(G, values, name) + assert G.nodes[0]["color"] == "red" + assert 1 not in G.nodes + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_set_edge_attributes(graph_type): + # Test single value + G = nx.path_graph(3, create_using=graph_type) + attr = "hello" + vals = 3 + nx.set_edge_attributes(G, vals, attr) + assert G[0][1][attr] == vals + assert G[1][2][attr] == vals + + # Test multiple values + G = nx.path_graph(3, create_using=graph_type) + attr = "hi" + edges = [(0, 1), (1, 2)] + vals = dict(zip(edges, range(len(edges)))) + nx.set_edge_attributes(G, vals, attr) + assert G[0][1][attr] == 0 + assert G[1][2][attr] == 1 + + # Test dictionary of dictionaries + G = nx.path_graph(3, create_using=graph_type) + d = {"hi": 0, "hello": 200} + edges = [(0, 1)] + vals = dict.fromkeys(edges, d) + nx.set_edge_attributes(G, vals) + assert G[0][1]["hi"] == 0 + assert G[0][1]["hello"] == 200 + assert G[1][2] == {} + + +@pytest.mark.parametrize( + ("values", "name"), + ( + ({(0, 1): 1.0, (0, 2): 2.0}, "weight"), # values dict + ({(0, 1): {"weight": 1.0}, (0, 2): {"weight": 2.0}}, None), # values dod + ), +) +def test_set_edge_attributes_ignores_extra_edges(values, name): + """If `values` is a dict or dict-of-dicts containing edges that are not in + G, data associate with these edges should be ignored. + """ + G = nx.Graph([(0, 1)]) + nx.set_edge_attributes(G, values, name) + assert G[0][1]["weight"] == 1.0 + assert (0, 2) not in G.edges + + +@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.MultiDiGraph)) +def test_set_edge_attributes_multi(graph_type): + # Test single value + G = nx.path_graph(3, create_using=graph_type) + attr = "hello" + vals = 3 + nx.set_edge_attributes(G, vals, attr) + assert G[0][1][0][attr] == vals + assert G[1][2][0][attr] == vals + + # Test multiple values + G = nx.path_graph(3, create_using=graph_type) + attr = "hi" + edges = [(0, 1, 0), (1, 2, 0)] + vals = dict(zip(edges, range(len(edges)))) + nx.set_edge_attributes(G, vals, attr) + assert G[0][1][0][attr] == 0 + assert G[1][2][0][attr] == 1 + + # Test dictionary of dictionaries + G = nx.path_graph(3, create_using=graph_type) + d = {"hi": 0, "hello": 200} + edges = [(0, 1, 0)] + vals = dict.fromkeys(edges, d) + nx.set_edge_attributes(G, vals) + assert G[0][1][0]["hi"] == 0 + assert G[0][1][0]["hello"] == 200 + assert G[1][2][0] == {} + + +@pytest.mark.parametrize( + ("values", "name"), + ( + ({(0, 1, 0): 1.0, (0, 2, 0): 2.0}, "weight"), # values dict + ({(0, 1, 0): {"weight": 1.0}, (0, 2, 0): {"weight": 2.0}}, None), # values dod + ), +) +def test_set_edge_attributes_multi_ignores_extra_edges(values, name): + """If `values` is a dict or dict-of-dicts containing edges that are not in + G, data associate with these edges should be ignored. + """ + G = nx.MultiGraph([(0, 1, 0), (0, 1, 1)]) + nx.set_edge_attributes(G, values, name) + assert G[0][1][0]["weight"] == 1.0 + assert G[0][1][1] == {} + assert (0, 2) not in G.edges() + + +def test_get_node_attributes(): + graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()] + for G in graphs: + G = nx.path_graph(3, create_using=G) + attr = "hello" + vals = 100 + nx.set_node_attributes(G, vals, attr) + attrs = nx.get_node_attributes(G, attr) + assert attrs[0] == vals + assert attrs[1] == vals + assert attrs[2] == vals + default_val = 1 + G.add_node(4) + attrs = nx.get_node_attributes(G, attr, default=default_val) + assert attrs[4] == default_val + + +def test_get_edge_attributes(): + graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()] + for G in graphs: + G = nx.path_graph(3, create_using=G) + attr = "hello" + vals = 100 + nx.set_edge_attributes(G, vals, attr) + attrs = nx.get_edge_attributes(G, attr) + assert len(attrs) == 2 + + for edge in G.edges: + assert attrs[edge] == vals + + default_val = vals + G.add_edge(4, 5) + deafult_attrs = nx.get_edge_attributes(G, attr, default=default_val) + assert len(deafult_attrs) == 3 + + for edge in G.edges: + assert deafult_attrs[edge] == vals + + +@pytest.mark.parametrize( + "graph_type", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph) +) +def test_remove_node_attributes(graph_type): + # Test removing single attribute + G = nx.path_graph(3, create_using=graph_type) + vals = 100 + attr = "hello" + nx.set_node_attributes(G, vals, attr) + nx.remove_node_attributes(G, attr) + assert attr not in G.nodes[0] + assert attr not in G.nodes[1] + assert attr not in G.nodes[2] + + # Test removing single attribute when multiple present + G = nx.path_graph(3, create_using=graph_type) + other_vals = 200 + other_attr = "other" + nx.set_node_attributes(G, vals, attr) + nx.set_node_attributes(G, other_vals, other_attr) + nx.remove_node_attributes(G, attr) + assert attr not in G.nodes[0] + assert G.nodes[0][other_attr] == other_vals + assert attr not in G.nodes[1] + assert G.nodes[1][other_attr] == other_vals + assert attr not in G.nodes[2] + assert G.nodes[2][other_attr] == other_vals + + # Test removing multiple attributes + G = nx.path_graph(3, create_using=graph_type) + nx.set_node_attributes(G, vals, attr) + nx.set_node_attributes(G, other_vals, other_attr) + nx.remove_node_attributes(G, attr, other_attr) + assert attr not in G.nodes[0] and other_attr not in G.nodes[0] + assert attr not in G.nodes[1] and other_attr not in G.nodes[1] + assert attr not in G.nodes[2] and other_attr not in G.nodes[2] + + # Test removing multiple (but not all) attributes + G = nx.path_graph(3, create_using=graph_type) + third_vals = 300 + third_attr = "three" + nx.set_node_attributes( + G, + { + n: {attr: vals, other_attr: other_vals, third_attr: third_vals} + for n in G.nodes() + }, + ) + nx.remove_node_attributes(G, other_attr, third_attr) + assert other_attr not in G.nodes[0] and third_attr not in G.nodes[0] + assert other_attr not in G.nodes[1] and third_attr not in G.nodes[1] + assert other_attr not in G.nodes[2] and third_attr not in G.nodes[2] + assert G.nodes[0][attr] == vals + assert G.nodes[1][attr] == vals + assert G.nodes[2][attr] == vals + + # Test incomplete node attributes + G = nx.path_graph(3, create_using=graph_type) + nx.set_node_attributes( + G, + { + 1: {attr: vals, other_attr: other_vals}, + 2: {attr: vals, other_attr: other_vals}, + }, + ) + nx.remove_node_attributes(G, attr) + assert attr not in G.nodes[0] + assert attr not in G.nodes[1] + assert attr not in G.nodes[2] + assert G.nodes[1][other_attr] == other_vals + assert G.nodes[2][other_attr] == other_vals + + # Test removing on a subset of nodes + G = nx.path_graph(3, create_using=graph_type) + nx.set_node_attributes( + G, + { + n: {attr: vals, other_attr: other_vals, third_attr: third_vals} + for n in G.nodes() + }, + ) + nx.remove_node_attributes(G, attr, other_attr, nbunch=[0, 1]) + assert attr not in G.nodes[0] and other_attr not in G.nodes[0] + assert attr not in G.nodes[1] and other_attr not in G.nodes[1] + assert attr in G.nodes[2] and other_attr in G.nodes[2] + assert third_attr in G.nodes[0] and G.nodes[0][third_attr] == third_vals + assert third_attr in G.nodes[1] and G.nodes[1][third_attr] == third_vals + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_remove_edge_attributes(graph_type): + # Test removing single attribute + G = nx.path_graph(3, create_using=graph_type) + attr = "hello" + vals = 100 + nx.set_edge_attributes(G, vals, attr) + nx.remove_edge_attributes(G, attr) + assert len(nx.get_edge_attributes(G, attr)) == 0 + + # Test removing only some attributes + G = nx.path_graph(3, create_using=graph_type) + other_attr = "other" + other_vals = 200 + nx.set_edge_attributes(G, vals, attr) + nx.set_edge_attributes(G, other_vals, other_attr) + nx.remove_edge_attributes(G, attr) + + assert attr not in G[0][1] + assert attr not in G[1][2] + assert G[0][1][other_attr] == 200 + assert G[1][2][other_attr] == 200 + + # Test removing multiple attributes + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, vals, attr) + nx.set_edge_attributes(G, other_vals, other_attr) + nx.remove_edge_attributes(G, attr, other_attr) + assert attr not in G[0][1] and other_attr not in G[0][1] + assert attr not in G[1][2] and other_attr not in G[1][2] + + # Test removing multiple (not all) attributes + G = nx.path_graph(3, create_using=graph_type) + third_attr = "third" + third_vals = 300 + nx.set_edge_attributes( + G, + { + (u, v): {attr: vals, other_attr: other_vals, third_attr: third_vals} + for u, v in G.edges() + }, + ) + nx.remove_edge_attributes(G, other_attr, third_attr) + assert other_attr not in G[0][1] and third_attr not in G[0][1] + assert other_attr not in G[1][2] and third_attr not in G[1][2] + assert G[0][1][attr] == vals + assert G[1][2][attr] == vals + + # Test removing incomplete edge attributes + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, {(0, 1): {attr: vals, other_attr: other_vals}}) + nx.remove_edge_attributes(G, other_attr) + assert other_attr not in G[0][1] and G[0][1][attr] == vals + assert other_attr not in G[1][2] + + # Test removing subset of edge attributes + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes( + G, + { + (u, v): {attr: vals, other_attr: other_vals, third_attr: third_vals} + for u, v in G.edges() + }, + ) + nx.remove_edge_attributes(G, other_attr, third_attr, ebunch=[(0, 1)]) + assert other_attr not in G[0][1] and third_attr not in G[0][1] + assert other_attr in G[1][2] and third_attr in G[1][2] + + +@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.MultiDiGraph)) +def test_remove_multi_edge_attributes(graph_type): + # Test removing single attribute + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + attr = "hello" + vals = 100 + nx.set_edge_attributes(G, vals, attr) + nx.remove_edge_attributes(G, attr) + assert attr not in G[0][1][0] + assert attr not in G[1][2][0] + assert attr not in G[1][2][1] + + # Test removing only some attributes + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + other_attr = "other" + other_vals = 200 + nx.set_edge_attributes(G, vals, attr) + nx.set_edge_attributes(G, other_vals, other_attr) + nx.remove_edge_attributes(G, attr) + assert attr not in G[0][1][0] + assert attr not in G[1][2][0] + assert attr not in G[1][2][1] + assert G[0][1][0][other_attr] == other_vals + assert G[1][2][0][other_attr] == other_vals + assert G[1][2][1][other_attr] == other_vals + + # Test removing multiple attributes + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + nx.set_edge_attributes(G, vals, attr) + nx.set_edge_attributes(G, other_vals, other_attr) + nx.remove_edge_attributes(G, attr, other_attr) + assert attr not in G[0][1][0] and other_attr not in G[0][1][0] + assert attr not in G[1][2][0] and other_attr not in G[1][2][0] + assert attr not in G[1][2][1] and other_attr not in G[1][2][1] + + # Test removing multiple (not all) attributes + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + third_attr = "third" + third_vals = 300 + nx.set_edge_attributes( + G, + { + (u, v, k): {attr: vals, other_attr: other_vals, third_attr: third_vals} + for u, v, k in G.edges(keys=True) + }, + ) + nx.remove_edge_attributes(G, other_attr, third_attr) + assert other_attr not in G[0][1][0] and third_attr not in G[0][1][0] + assert other_attr not in G[1][2][0] and other_attr not in G[1][2][0] + assert other_attr not in G[1][2][1] and other_attr not in G[1][2][1] + assert G[0][1][0][attr] == vals + assert G[1][2][0][attr] == vals + assert G[1][2][1][attr] == vals + + # Test removing incomplete edge attributes + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + nx.set_edge_attributes( + G, + { + (0, 1, 0): {attr: vals, other_attr: other_vals}, + (1, 2, 1): {attr: vals, other_attr: other_vals}, + }, + ) + nx.remove_edge_attributes(G, other_attr) + assert other_attr not in G[0][1][0] and G[0][1][0][attr] == vals + assert other_attr not in G[1][2][0] + assert other_attr not in G[1][2][1] + + # Test removing subset of edge attributes + G = nx.path_graph(3, create_using=graph_type) + G.add_edge(1, 2) + nx.set_edge_attributes( + G, + { + (0, 1, 0): {attr: vals, other_attr: other_vals}, + (1, 2, 0): {attr: vals, other_attr: other_vals}, + (1, 2, 1): {attr: vals, other_attr: other_vals}, + }, + ) + nx.remove_edge_attributes(G, attr, ebunch=[(0, 1, 0), (1, 2, 0)]) + assert attr not in G[0][1][0] and other_attr in G[0][1][0] + assert attr not in G[1][2][0] and other_attr in G[1][2][0] + assert attr in G[1][2][1] and other_attr in G[1][2][1] + + +def test_is_empty(): + graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()] + for G in graphs: + assert nx.is_empty(G) + G.add_nodes_from(range(5)) + assert nx.is_empty(G) + G.add_edges_from([(1, 2), (3, 4)]) + assert not nx.is_empty(G) + + +@pytest.mark.parametrize( + "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +) +def test_selfloops(graph_type): + G = nx.complete_graph(3, create_using=graph_type) + G.add_edge(0, 0) + assert nodes_equal(nx.nodes_with_selfloops(G), [0]) + assert edges_equal(nx.selfloop_edges(G), [(0, 0)]) + assert edges_equal(nx.selfloop_edges(G, data=True), [(0, 0, {})]) + assert nx.number_of_selfloops(G) == 1 + + +@pytest.mark.parametrize( + "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +) +def test_selfloop_edges_attr(graph_type): + G = nx.complete_graph(3, create_using=graph_type) + G.add_edge(0, 0) + G.add_edge(1, 1, weight=2) + assert edges_equal( + nx.selfloop_edges(G, data=True), [(0, 0, {}), (1, 1, {"weight": 2})] + ) + assert edges_equal(nx.selfloop_edges(G, data="weight"), [(0, 0, None), (1, 1, 2)]) + + +def test_selfloop_edges_multi_with_data_and_keys(): + G = nx.complete_graph(3, create_using=nx.MultiGraph) + G.add_edge(0, 0, weight=10) + G.add_edge(0, 0, weight=100) + assert edges_equal( + nx.selfloop_edges(G, data="weight", keys=True), [(0, 0, 0, 10), (0, 0, 1, 100)] + ) + + +@pytest.mark.parametrize("graph_type", [nx.Graph, nx.DiGraph]) +def test_selfloops_removal(graph_type): + G = nx.complete_graph(3, create_using=graph_type) + G.add_edge(0, 0) + G.remove_edges_from(nx.selfloop_edges(G, keys=True)) + G.add_edge(0, 0) + G.remove_edges_from(nx.selfloop_edges(G, data=True)) + G.add_edge(0, 0) + G.remove_edges_from(nx.selfloop_edges(G, keys=True, data=True)) + + +@pytest.mark.parametrize("graph_type", [nx.MultiGraph, nx.MultiDiGraph]) +def test_selfloops_removal_multi(graph_type): + """test removing selfloops behavior vis-a-vis altering a dict while iterating. + cf. gh-4068""" + G = nx.complete_graph(3, create_using=graph_type) + # Defaults - see gh-4080 + G.add_edge(0, 0) + G.add_edge(0, 0) + G.remove_edges_from(nx.selfloop_edges(G)) + assert (0, 0) not in G.edges() + # With keys + G.add_edge(0, 0) + G.add_edge(0, 0) + with pytest.raises(RuntimeError): + G.remove_edges_from(nx.selfloop_edges(G, keys=True)) + # With data + G.add_edge(0, 0) + G.add_edge(0, 0) + with pytest.raises(TypeError): + G.remove_edges_from(nx.selfloop_edges(G, data=True)) + # With keys and data + G.add_edge(0, 0) + G.add_edge(0, 0) + with pytest.raises(RuntimeError): + G.remove_edges_from(nx.selfloop_edges(G, data=True, keys=True)) + + +def test_pathweight(): + valid_path = [1, 2, 3] + invalid_path = [1, 3, 2] + graphs = [nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()] + edges = [ + (1, 2, {"cost": 5, "dist": 6}), + (2, 3, {"cost": 3, "dist": 4}), + (1, 2, {"cost": 1, "dist": 2}), + ] + for graph in graphs: + graph.add_edges_from(edges) + assert nx.path_weight(graph, valid_path, "cost") == 4 + assert nx.path_weight(graph, valid_path, "dist") == 6 + pytest.raises(nx.NetworkXNoPath, nx.path_weight, graph, invalid_path, "cost") + + +@pytest.mark.parametrize( + "G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()) +) +def test_ispath(G): + G.add_edges_from([(1, 2), (2, 3), (1, 2), (3, 4)]) + valid_path = [1, 2, 3, 4] + invalid_path = [1, 2, 4, 3] # wrong node order + another_invalid_path = [1, 2, 3, 4, 5] # contains node not in G + assert nx.is_path(G, valid_path) + assert not nx.is_path(G, invalid_path) + assert not nx.is_path(G, another_invalid_path) + + +@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) +def test_restricted_view(G): + G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2)]) + G.add_node(4) + H = nx.restricted_view(G, [0, 2, 5], [(1, 2), (3, 4)]) + assert set(H.nodes()) == {1, 3, 4} + assert set(H.edges()) == {(1, 1)} + + +@pytest.mark.parametrize("G", (nx.MultiGraph(), nx.MultiDiGraph())) +def test_restricted_view_multi(G): + G.add_edges_from( + [(0, 1, 0), (0, 2, 0), (0, 3, 0), (0, 1, 1), (1, 0, 0), (1, 1, 0), (1, 2, 0)] + ) + G.add_node(4) + H = nx.restricted_view(G, [0, 2, 5], [(1, 2, 0), (3, 4, 0)]) + assert set(H.nodes()) == {1, 3, 4} + assert set(H.edges()) == {(1, 1)} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..bd76f8da72a6609630207fcc24fab36990882513 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph.py @@ -0,0 +1,950 @@ +import gc +import pickle +import platform +import weakref + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, graphs_equal, nodes_equal + + +def test_degree_node_not_found_exception_message(): + """See gh-7740""" + G = nx.path_graph(5) + with pytest.raises(nx.NetworkXError, match="Node.*is not in the graph"): + G.degree(100) + + +class BaseGraphTester: + """Tests for data-structure independent graph class features.""" + + def test_contains(self): + G = self.K3 + assert 1 in G + assert 4 not in G + assert "b" not in G + assert [] not in G # no exception for nonhashable + assert {1: 1} not in G # no exception for nonhashable + + def test_order(self): + G = self.K3 + assert len(G) == 3 + assert G.order() == 3 + assert G.number_of_nodes() == 3 + + def test_nodes(self): + G = self.K3 + assert isinstance(G._node, G.node_dict_factory) + assert isinstance(G._adj, G.adjlist_outer_dict_factory) + assert all( + isinstance(adj, G.adjlist_inner_dict_factory) for adj in G._adj.values() + ) + assert sorted(G.nodes()) == self.k3nodes + assert sorted(G.nodes(data=True)) == [(0, {}), (1, {}), (2, {})] + + def test_none_node(self): + G = self.Graph() + with pytest.raises(ValueError): + G.add_node(None) + with pytest.raises(ValueError): + G.add_nodes_from([None]) + with pytest.raises(ValueError): + G.add_edge(0, None) + with pytest.raises(ValueError): + G.add_edges_from([(0, None)]) + + def test_has_node(self): + G = self.K3 + assert G.has_node(1) + assert not G.has_node(4) + assert not G.has_node([]) # no exception for nonhashable + assert not G.has_node({1: 1}) # no exception for nonhashable + + def test_has_edge(self): + G = self.K3 + assert G.has_edge(0, 1) + assert not G.has_edge(0, -1) + + def test_neighbors(self): + G = self.K3 + assert sorted(G.neighbors(0)) == [1, 2] + with pytest.raises(nx.NetworkXError): + G.neighbors(-1) + + @pytest.mark.skipif( + platform.python_implementation() == "PyPy", reason="PyPy gc is different" + ) + def test_memory_leak(self): + G = self.Graph() + + def count_objects_of_type(_type): + # Iterating over all objects tracked by gc can include weak references + # whose weakly-referenced objects may no longer exist. Calling `isinstance` + # on such a weak reference will raise ReferenceError. There are at least + # three workarounds for this: one is to compare type names instead of using + # `isinstance` such as `type(obj).__name__ == typename`, another is to use + # `type(obj) == _type`, and the last is to ignore ProxyTypes as we do below. + # NOTE: even if this safeguard is deemed unnecessary to pass NetworkX tests, + # we should still keep it for maximum safety for other NetworkX backends. + return sum( + 1 + for obj in gc.get_objects() + if not isinstance(obj, weakref.ProxyTypes) and isinstance(obj, _type) + ) + + gc.collect() + before = count_objects_of_type(self.Graph) + G.copy() + gc.collect() + after = count_objects_of_type(self.Graph) + assert before == after + + # test a subgraph of the base class + class MyGraph(self.Graph): + pass + + gc.collect() + G = MyGraph() + before = count_objects_of_type(MyGraph) + G.copy() + gc.collect() + after = count_objects_of_type(MyGraph) + assert before == after + + def test_edges(self): + G = self.K3 + assert isinstance(G._adj, G.adjlist_outer_dict_factory) + assert edges_equal(G.edges(), [(0, 1), (0, 2), (1, 2)]) + assert edges_equal(G.edges(0), [(0, 1), (0, 2)]) + assert edges_equal(G.edges([0, 1]), [(0, 1), (0, 2), (1, 2)]) + with pytest.raises(nx.NetworkXError): + G.edges(-1) + + def test_degree(self): + G = self.K3 + assert sorted(G.degree()) == [(0, 2), (1, 2), (2, 2)] + assert dict(G.degree()) == {0: 2, 1: 2, 2: 2} + assert G.degree(0) == 2 + with pytest.raises(nx.NetworkXError): + G.degree(-1) # node not in graph + + def test_size(self): + G = self.K3 + assert G.size() == 3 + assert G.number_of_edges() == 3 + + def test_nbunch_iter(self): + G = self.K3 + assert nodes_equal(G.nbunch_iter(), self.k3nodes) # all nodes + assert nodes_equal(G.nbunch_iter(0), [0]) # single node + assert nodes_equal(G.nbunch_iter([0, 1]), [0, 1]) # sequence + # sequence with none in graph + assert nodes_equal(G.nbunch_iter([-1]), []) + # string sequence with none in graph + assert nodes_equal(G.nbunch_iter("foo"), []) + # node not in graph doesn't get caught upon creation of iterator + bunch = G.nbunch_iter(-1) + # but gets caught when iterator used + with pytest.raises(nx.NetworkXError, match="is not in the graph"): + list(bunch) + # unhashable doesn't get caught upon creation of iterator + bunch = G.nbunch_iter([0, 1, 2, {}]) + # but gets caught when iterator hits the unhashable + with pytest.raises( + nx.NetworkXError, match="in sequence nbunch is not a valid node" + ): + list(bunch) + + def test_nbunch_iter_node_format_raise(self): + # Tests that a node that would have failed string formatting + # doesn't cause an error when attempting to raise a + # :exc:`nx.NetworkXError`. + + # For more information, see pull request #1813. + G = self.Graph() + nbunch = [("x", set())] + with pytest.raises(nx.NetworkXError): + list(G.nbunch_iter(nbunch)) + + def test_selfloop_degree(self): + G = self.Graph() + G.add_edge(1, 1) + assert sorted(G.degree()) == [(1, 2)] + assert dict(G.degree()) == {1: 2} + assert G.degree(1) == 2 + assert sorted(G.degree([1])) == [(1, 2)] + assert G.degree(1, weight="weight") == 2 + + def test_selfloops(self): + G = self.K3.copy() + G.add_edge(0, 0) + assert nodes_equal(nx.nodes_with_selfloops(G), [0]) + assert edges_equal(nx.selfloop_edges(G), [(0, 0)]) + assert nx.number_of_selfloops(G) == 1 + G.remove_edge(0, 0) + G.add_edge(0, 0) + G.remove_edges_from([(0, 0)]) + G.add_edge(1, 1) + G.remove_node(1) + G.add_edge(0, 0) + G.add_edge(1, 1) + G.remove_nodes_from([0, 1]) + + def test_cache_reset(self): + G = self.K3.copy() + old_adj = G.adj + assert id(G.adj) == id(old_adj) + G._adj = {} + assert id(G.adj) != id(old_adj) + + old_nodes = G.nodes + assert id(G.nodes) == id(old_nodes) + G._node = {} + assert id(G.nodes) != id(old_nodes) + + def test_attributes_cached(self): + G = self.K3.copy() + assert id(G.nodes) == id(G.nodes) + assert id(G.edges) == id(G.edges) + assert id(G.degree) == id(G.degree) + assert id(G.adj) == id(G.adj) + + +class BaseAttrGraphTester(BaseGraphTester): + """Tests of graph class attribute features.""" + + def test_weighted_degree(self): + G = self.Graph() + G.add_edge(1, 2, weight=2, other=3) + G.add_edge(2, 3, weight=3, other=4) + assert sorted(d for n, d in G.degree(weight="weight")) == [2, 3, 5] + assert dict(G.degree(weight="weight")) == {1: 2, 2: 5, 3: 3} + assert G.degree(1, weight="weight") == 2 + assert nodes_equal((G.degree([1], weight="weight")), [(1, 2)]) + + assert nodes_equal((d for n, d in G.degree(weight="other")), [3, 7, 4]) + assert dict(G.degree(weight="other")) == {1: 3, 2: 7, 3: 4} + assert G.degree(1, weight="other") == 3 + assert edges_equal((G.degree([1], weight="other")), [(1, 3)]) + + def add_attributes(self, G): + G.graph["foo"] = [] + G.nodes[0]["foo"] = [] + G.remove_edge(1, 2) + ll = [] + G.add_edge(1, 2, foo=ll) + G.add_edge(2, 1, foo=ll) + + def test_name(self): + G = self.Graph(name="") + assert G.name == "" + G = self.Graph(name="test") + assert G.name == "test" + + def test_str_unnamed(self): + G = self.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + assert str(G) == f"{type(G).__name__} with 3 nodes and 2 edges" + + def test_str_named(self): + G = self.Graph(name="foo") + G.add_edges_from([(1, 2), (2, 3)]) + assert str(G) == f"{type(G).__name__} named 'foo' with 3 nodes and 2 edges" + + def test_graph_chain(self): + G = self.Graph([(0, 1), (1, 2)]) + DG = G.to_directed(as_view=True) + SDG = DG.subgraph([0, 1]) + RSDG = SDG.reverse(copy=False) + assert G is DG._graph + assert DG is SDG._graph + assert SDG is RSDG._graph + + def test_copy(self): + G = self.Graph() + G.add_node(0) + G.add_edge(1, 2) + self.add_attributes(G) + # copy edge datadict but any container attr are same + H = G.copy() + self.graphs_equal(H, G) + self.different_attrdict(H, G) + self.shallow_copy_attrdict(H, G) + + def test_class_copy(self): + G = self.Graph() + G.add_node(0) + G.add_edge(1, 2) + self.add_attributes(G) + # copy edge datadict but any container attr are same + H = G.__class__(G) + self.graphs_equal(H, G) + self.different_attrdict(H, G) + self.shallow_copy_attrdict(H, G) + + def test_fresh_copy(self): + G = self.Graph() + G.add_node(0) + G.add_edge(1, 2) + self.add_attributes(G) + # copy graph structure but use fresh datadict + H = G.__class__() + H.add_nodes_from(G) + H.add_edges_from(G.edges()) + assert len(G.nodes[0]) == 1 + ddict = G.adj[1][2][0] if G.is_multigraph() else G.adj[1][2] + assert len(ddict) == 1 + assert len(H.nodes[0]) == 0 + ddict = H.adj[1][2][0] if H.is_multigraph() else H.adj[1][2] + assert len(ddict) == 0 + + def is_deepcopy(self, H, G): + self.graphs_equal(H, G) + self.different_attrdict(H, G) + self.deep_copy_attrdict(H, G) + + def deep_copy_attrdict(self, H, G): + self.deepcopy_graph_attr(H, G) + self.deepcopy_node_attr(H, G) + self.deepcopy_edge_attr(H, G) + + def deepcopy_graph_attr(self, H, G): + assert G.graph["foo"] == H.graph["foo"] + G.graph["foo"].append(1) + assert G.graph["foo"] != H.graph["foo"] + + def deepcopy_node_attr(self, H, G): + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + G.nodes[0]["foo"].append(1) + assert G.nodes[0]["foo"] != H.nodes[0]["foo"] + + def deepcopy_edge_attr(self, H, G): + assert G[1][2]["foo"] == H[1][2]["foo"] + G[1][2]["foo"].append(1) + assert G[1][2]["foo"] != H[1][2]["foo"] + + def is_shallow_copy(self, H, G): + self.graphs_equal(H, G) + self.shallow_copy_attrdict(H, G) + + def shallow_copy_attrdict(self, H, G): + self.shallow_copy_graph_attr(H, G) + self.shallow_copy_node_attr(H, G) + self.shallow_copy_edge_attr(H, G) + + def shallow_copy_graph_attr(self, H, G): + assert G.graph["foo"] == H.graph["foo"] + G.graph["foo"].append(1) + assert G.graph["foo"] == H.graph["foo"] + + def shallow_copy_node_attr(self, H, G): + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + G.nodes[0]["foo"].append(1) + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + + def shallow_copy_edge_attr(self, H, G): + assert G[1][2]["foo"] == H[1][2]["foo"] + G[1][2]["foo"].append(1) + assert G[1][2]["foo"] == H[1][2]["foo"] + + def same_attrdict(self, H, G): + old_foo = H[1][2]["foo"] + H.adj[1][2]["foo"] = "baz" + assert G.edges == H.edges + H.adj[1][2]["foo"] = old_foo + assert G.edges == H.edges + + old_foo = H.nodes[0]["foo"] + H.nodes[0]["foo"] = "baz" + assert G.nodes == H.nodes + H.nodes[0]["foo"] = old_foo + assert G.nodes == H.nodes + + def different_attrdict(self, H, G): + old_foo = H[1][2]["foo"] + H.adj[1][2]["foo"] = "baz" + assert G._adj != H._adj + H.adj[1][2]["foo"] = old_foo + assert G._adj == H._adj + + old_foo = H.nodes[0]["foo"] + H.nodes[0]["foo"] = "baz" + assert G._node != H._node + H.nodes[0]["foo"] = old_foo + assert G._node == H._node + + def graphs_equal(self, H, G): + assert G._adj == H._adj + assert G._node == H._node + assert G.graph == H.graph + assert G.name == H.name + if not G.is_directed() and not H.is_directed(): + assert H._adj[1][2] is H._adj[2][1] + assert G._adj[1][2] is G._adj[2][1] + else: # at least one is directed + if not G.is_directed(): + G._pred = G._adj + G._succ = G._adj + if not H.is_directed(): + H._pred = H._adj + H._succ = H._adj + assert G._pred == H._pred + assert G._succ == H._succ + assert H._succ[1][2] is H._pred[2][1] + assert G._succ[1][2] is G._pred[2][1] + + def test_graph_attr(self): + G = self.K3.copy() + G.graph["foo"] = "bar" + assert isinstance(G.graph, G.graph_attr_dict_factory) + assert G.graph["foo"] == "bar" + del G.graph["foo"] + assert G.graph == {} + H = self.Graph(foo="bar") + assert H.graph["foo"] == "bar" + + def test_node_attr(self): + G = self.K3.copy() + G.add_node(1, foo="bar") + assert all( + isinstance(d, G.node_attr_dict_factory) for u, d in G.nodes(data=True) + ) + assert nodes_equal(G.nodes(), [0, 1, 2]) + assert nodes_equal(G.nodes(data=True), [(0, {}), (1, {"foo": "bar"}), (2, {})]) + G.nodes[1]["foo"] = "baz" + assert nodes_equal(G.nodes(data=True), [(0, {}), (1, {"foo": "baz"}), (2, {})]) + assert nodes_equal(G.nodes(data="foo"), [(0, None), (1, "baz"), (2, None)]) + assert nodes_equal( + G.nodes(data="foo", default="bar"), [(0, "bar"), (1, "baz"), (2, "bar")] + ) + + def test_node_attr2(self): + G = self.K3.copy() + a = {"foo": "bar"} + G.add_node(3, **a) + assert nodes_equal(G.nodes(), [0, 1, 2, 3]) + assert nodes_equal( + G.nodes(data=True), [(0, {}), (1, {}), (2, {}), (3, {"foo": "bar"})] + ) + + def test_edge_lookup(self): + G = self.Graph() + G.add_edge(1, 2, foo="bar") + assert edges_equal(G.edges[1, 2], {"foo": "bar"}) + + def test_edge_attr(self): + G = self.Graph() + G.add_edge(1, 2, foo="bar") + assert all( + isinstance(d, G.edge_attr_dict_factory) for u, v, d in G.edges(data=True) + ) + assert edges_equal(G.edges(data=True), [(1, 2, {"foo": "bar"})]) + assert edges_equal(G.edges(data="foo"), [(1, 2, "bar")]) + + def test_edge_attr2(self): + G = self.Graph() + G.add_edges_from([(1, 2), (3, 4)], foo="foo") + assert edges_equal( + G.edges(data=True), [(1, 2, {"foo": "foo"}), (3, 4, {"foo": "foo"})] + ) + assert edges_equal(G.edges(data="foo"), [(1, 2, "foo"), (3, 4, "foo")]) + + def test_edge_attr3(self): + G = self.Graph() + G.add_edges_from([(1, 2, {"weight": 32}), (3, 4, {"weight": 64})], foo="foo") + assert edges_equal( + G.edges(data=True), + [ + (1, 2, {"foo": "foo", "weight": 32}), + (3, 4, {"foo": "foo", "weight": 64}), + ], + ) + + G.remove_edges_from([(1, 2), (3, 4)]) + G.add_edge(1, 2, data=7, spam="bar", bar="foo") + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})] + ) + + def test_edge_attr4(self): + G = self.Graph() + G.add_edge(1, 2, data=7, spam="bar", bar="foo") + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})] + ) + G[1][2]["data"] = 10 # OK to set data like this + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 10, "spam": "bar", "bar": "foo"})] + ) + + G.adj[1][2]["data"] = 20 + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 20, "spam": "bar", "bar": "foo"})] + ) + G.edges[1, 2]["data"] = 21 # another spelling, "edge" + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 21, "spam": "bar", "bar": "foo"})] + ) + G.adj[1][2]["listdata"] = [20, 200] + G.adj[1][2]["weight"] = 20 + dd = { + "data": 21, + "spam": "bar", + "bar": "foo", + "listdata": [20, 200], + "weight": 20, + } + assert edges_equal(G.edges(data=True), [(1, 2, dd)]) + + def test_to_undirected(self): + G = self.K3 + self.add_attributes(G) + H = nx.Graph(G) + self.is_shallow_copy(H, G) + self.different_attrdict(H, G) + H = G.to_undirected() + self.is_deepcopy(H, G) + + def test_to_directed_as_view(self): + H = nx.path_graph(2, create_using=self.Graph) + H2 = H.to_directed(as_view=True) + assert H is H2._graph + assert H2.has_edge(0, 1) + assert H2.has_edge(1, 0) or H.is_directed() + pytest.raises(nx.NetworkXError, H2.add_node, -1) + pytest.raises(nx.NetworkXError, H2.add_edge, 1, 2) + H.add_edge(1, 2) + assert H2.has_edge(1, 2) + assert H2.has_edge(2, 1) or H.is_directed() + + def test_to_undirected_as_view(self): + H = nx.path_graph(2, create_using=self.Graph) + H2 = H.to_undirected(as_view=True) + assert H is H2._graph + assert H2.has_edge(0, 1) + assert H2.has_edge(1, 0) + pytest.raises(nx.NetworkXError, H2.add_node, -1) + pytest.raises(nx.NetworkXError, H2.add_edge, 1, 2) + H.add_edge(1, 2) + assert H2.has_edge(1, 2) + assert H2.has_edge(2, 1) + + def test_directed_class(self): + G = self.Graph() + + class newGraph(G.to_undirected_class()): + def to_directed_class(self): + return newDiGraph + + def to_undirected_class(self): + return newGraph + + class newDiGraph(G.to_directed_class()): + def to_directed_class(self): + return newDiGraph + + def to_undirected_class(self): + return newGraph + + G = newDiGraph() if G.is_directed() else newGraph() + H = G.to_directed() + assert isinstance(H, newDiGraph) + H = G.to_undirected() + assert isinstance(H, newGraph) + + def test_to_directed(self): + G = self.K3 + self.add_attributes(G) + H = nx.DiGraph(G) + self.is_shallow_copy(H, G) + self.different_attrdict(H, G) + H = G.to_directed() + self.is_deepcopy(H, G) + + def test_subgraph(self): + G = self.K3 + self.add_attributes(G) + H = G.subgraph([0, 1, 2, 5]) + self.graphs_equal(H, G) + self.same_attrdict(H, G) + self.shallow_copy_attrdict(H, G) + + H = G.subgraph(0) + assert H.adj == {0: {}} + H = G.subgraph([]) + assert H.adj == {} + assert G.adj != {} + + def test_selfloops_attr(self): + G = self.K3.copy() + G.add_edge(0, 0) + G.add_edge(1, 1, weight=2) + assert edges_equal( + nx.selfloop_edges(G, data=True), [(0, 0, {}), (1, 1, {"weight": 2})] + ) + assert edges_equal( + nx.selfloop_edges(G, data="weight"), [(0, 0, None), (1, 1, 2)] + ) + + +class TestGraph(BaseAttrGraphTester): + """Tests specific to dict-of-dict-of-dict graph data structure""" + + def setup_method(self): + self.Graph = nx.Graph + # build dict-of-dict-of-dict K3 + ed1, ed2, ed3 = ({}, {}, {}) + self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}} + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._adj = self.k3adj + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + def test_pickle(self): + G = self.K3 + pg = pickle.loads(pickle.dumps(G, -1)) + self.graphs_equal(pg, G) + pg = pickle.loads(pickle.dumps(G)) + self.graphs_equal(pg, G) + + def test_data_input(self): + G = self.Graph({1: [2], 2: [1]}, name="test") + assert G.name == "test" + assert sorted(G.adj.items()) == [(1, {2: {}}), (2, {1: {}})] + + def test_adjacency(self): + G = self.K3 + assert dict(G.adjacency()) == { + 0: {1: {}, 2: {}}, + 1: {0: {}, 2: {}}, + 2: {0: {}, 1: {}}, + } + + def test_getitem(self): + G = self.K3 + assert G.adj[0] == {1: {}, 2: {}} + assert G[0] == {1: {}, 2: {}} + with pytest.raises(KeyError): + G.__getitem__("j") + with pytest.raises(TypeError): + G.__getitem__(["A"]) + + def test_add_node(self): + G = self.Graph() + G.add_node(0) + assert G.adj == {0: {}} + # test add attributes + G.add_node(1, c="red") + G.add_node(2, c="blue") + G.add_node(3, c="red") + assert G.nodes[1]["c"] == "red" + assert G.nodes[2]["c"] == "blue" + assert G.nodes[3]["c"] == "red" + # test updating attributes + G.add_node(1, c="blue") + G.add_node(2, c="red") + G.add_node(3, c="blue") + assert G.nodes[1]["c"] == "blue" + assert G.nodes[2]["c"] == "red" + assert G.nodes[3]["c"] == "blue" + + def test_add_nodes_from(self): + G = self.Graph() + G.add_nodes_from([0, 1, 2]) + assert G.adj == {0: {}, 1: {}, 2: {}} + # test add attributes + G.add_nodes_from([0, 1, 2], c="red") + assert G.nodes[0]["c"] == "red" + assert G.nodes[2]["c"] == "red" + # test that attribute dicts are not the same + assert G.nodes[0] is not G.nodes[1] + # test updating attributes + G.add_nodes_from([0, 1, 2], c="blue") + assert G.nodes[0]["c"] == "blue" + assert G.nodes[2]["c"] == "blue" + assert G.nodes[0] is not G.nodes[1] + # test tuple input + H = self.Graph() + H.add_nodes_from(G.nodes(data=True)) + assert H.nodes[0]["c"] == "blue" + assert H.nodes[2]["c"] == "blue" + assert H.nodes[0] is not H.nodes[1] + # specific overrides general + H.add_nodes_from([0, (1, {"c": "green"}), (3, {"c": "cyan"})], c="red") + assert H.nodes[0]["c"] == "red" + assert H.nodes[1]["c"] == "green" + assert H.nodes[2]["c"] == "blue" + assert H.nodes[3]["c"] == "cyan" + + def test_remove_node(self): + G = self.K3.copy() + G.remove_node(0) + assert G.adj == {1: {2: {}}, 2: {1: {}}} + with pytest.raises(nx.NetworkXError): + G.remove_node(-1) + + # generator here to implement list,set,string... + + def test_remove_nodes_from(self): + G = self.K3.copy() + G.remove_nodes_from([0, 1]) + assert G.adj == {2: {}} + G.remove_nodes_from([-1]) # silent fail + + def test_add_edge(self): + G = self.Graph() + G.add_edge(0, 1) + assert G.adj == {0: {1: {}}, 1: {0: {}}} + G = self.Graph() + G.add_edge(*(0, 1)) + assert G.adj == {0: {1: {}}, 1: {0: {}}} + G = self.Graph() + with pytest.raises(ValueError): + G.add_edge(None, "anything") + + def test_add_edges_from(self): + G = self.Graph() + G.add_edges_from([(0, 1), (0, 2, {"weight": 3})]) + assert G.adj == { + 0: {1: {}, 2: {"weight": 3}}, + 1: {0: {}}, + 2: {0: {"weight": 3}}, + } + G = self.Graph() + G.add_edges_from([(0, 1), (0, 2, {"weight": 3}), (1, 2, {"data": 4})], data=2) + assert G.adj == { + 0: {1: {"data": 2}, 2: {"weight": 3, "data": 2}}, + 1: {0: {"data": 2}, 2: {"data": 4}}, + 2: {0: {"weight": 3, "data": 2}, 1: {"data": 4}}, + } + + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0,)]) # too few in tuple + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0, 1, 2, 3)]) # too many in tuple + with pytest.raises(TypeError): + G.add_edges_from([0]) # not a tuple + with pytest.raises(ValueError): + G.add_edges_from([(None, 3), (3, 2)]) # None cannot be a node + + def test_remove_edge(self): + G = self.K3.copy() + G.remove_edge(0, 1) + assert G.adj == {0: {2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}} + with pytest.raises(nx.NetworkXError): + G.remove_edge(-1, 0) + + def test_remove_edges_from(self): + G = self.K3.copy() + G.remove_edges_from([(0, 1)]) + assert G.adj == {0: {2: {}}, 1: {2: {}}, 2: {0: {}, 1: {}}} + G.remove_edges_from([(0, 0)]) # silent fail + + def test_clear(self): + G = self.K3.copy() + G.graph["name"] = "K3" + G.clear() + assert list(G.nodes) == [] + assert G.adj == {} + assert G.graph == {} + + def test_clear_edges(self): + G = self.K3.copy() + G.graph["name"] = "K3" + nodes = list(G.nodes) + G.clear_edges() + assert list(G.nodes) == nodes + assert G.adj == {0: {}, 1: {}, 2: {}} + assert list(G.edges) == [] + assert G.graph["name"] == "K3" + + def test_edges_data(self): + G = self.K3 + all_edges = [(0, 1, {}), (0, 2, {}), (1, 2, {})] + assert edges_equal(G.edges(data=True), all_edges) + assert edges_equal(G.edges(0, data=True), [(0, 1, {}), (0, 2, {})]) + assert edges_equal(G.edges([0, 1], data=True), all_edges) + with pytest.raises(nx.NetworkXError): + G.edges(-1, True) + + def test_get_edge_data(self): + G = self.K3.copy() + assert G.get_edge_data(0, 1) == {} + assert G[0][1] == {} + assert G.get_edge_data(10, 20) is None + assert G.get_edge_data(-1, 0) is None + assert G.get_edge_data(-1, 0, default=1) == 1 + + def test_update(self): + # specify both edges and nodes + G = self.K3.copy() + G.update(nodes=[3, (4, {"size": 2})], edges=[(4, 5), (6, 7, {"weight": 2})]) + nlist = [ + (0, {}), + (1, {}), + (2, {}), + (3, {}), + (4, {"size": 2}), + (5, {}), + (6, {}), + (7, {}), + ] + assert sorted(G.nodes.data()) == nlist + if G.is_directed(): + elist = [ + (0, 1, {}), + (0, 2, {}), + (1, 0, {}), + (1, 2, {}), + (2, 0, {}), + (2, 1, {}), + (4, 5, {}), + (6, 7, {"weight": 2}), + ] + else: + elist = [ + (0, 1, {}), + (0, 2, {}), + (1, 2, {}), + (4, 5, {}), + (6, 7, {"weight": 2}), + ] + assert sorted(G.edges.data()) == elist + assert G.graph == {} + + # no keywords -- order is edges, nodes + G = self.K3.copy() + G.update([(4, 5), (6, 7, {"weight": 2})], [3, (4, {"size": 2})]) + assert sorted(G.nodes.data()) == nlist + assert sorted(G.edges.data()) == elist + assert G.graph == {} + + # update using only a graph + G = self.Graph() + G.graph["foo"] = "bar" + G.add_node(2, data=4) + G.add_edge(0, 1, weight=0.5) + GG = G.copy() + H = self.Graph() + GG.update(H) + assert graphs_equal(G, GG) + H.update(G) + assert graphs_equal(H, G) + + # update nodes only + H = self.Graph() + H.update(nodes=[3, 4]) + assert H.nodes ^ {3, 4} == set() + assert H.size() == 0 + + # update edges only + H = self.Graph() + H.update(edges=[(3, 4)]) + assert sorted(H.edges.data()) == [(3, 4, {})] + assert H.size() == 1 + + # No inputs -> exception + with pytest.raises(nx.NetworkXError): + nx.Graph().update() + + +class TestEdgeSubgraph: + """Unit tests for the :meth:`Graph.edge_subgraph` method.""" + + def setup_method(self): + # Create a path graph on five nodes. + G = nx.path_graph(5) + # Add some node, edge, and graph attributes. + for i in range(5): + G.nodes[i]["name"] = f"node{i}" + G.edges[0, 1]["name"] = "edge01" + G.edges[3, 4]["name"] = "edge34" + G.graph["name"] = "graph" + # Get the subgraph induced by the first and last edges. + self.G = G + self.H = G.edge_subgraph([(0, 1), (3, 4)]) + + def test_correct_nodes(self): + """Tests that the subgraph has the correct nodes.""" + assert [0, 1, 3, 4] == sorted(self.H.nodes()) + + def test_correct_edges(self): + """Tests that the subgraph has the correct edges.""" + assert [(0, 1, "edge01"), (3, 4, "edge34")] == sorted(self.H.edges(data="name")) + + def test_add_node(self): + """Tests that adding a node to the original graph does not + affect the nodes of the subgraph. + + """ + self.G.add_node(5) + assert [0, 1, 3, 4] == sorted(self.H.nodes()) + + def test_remove_node(self): + """Tests that removing a node in the original graph does + affect the nodes of the subgraph. + + """ + self.G.remove_node(0) + assert [1, 3, 4] == sorted(self.H.nodes()) + + def test_node_attr_dict(self): + """Tests that the node attribute dictionary of the two graphs is + the same object. + + """ + for v in self.H: + assert self.G.nodes[v] == self.H.nodes[v] + # Making a change to G should make a change in H and vice versa. + self.G.nodes[0]["name"] = "foo" + assert self.G.nodes[0] == self.H.nodes[0] + self.H.nodes[1]["name"] = "bar" + assert self.G.nodes[1] == self.H.nodes[1] + + def test_edge_attr_dict(self): + """Tests that the edge attribute dictionary of the two graphs is + the same object. + + """ + for u, v in self.H.edges(): + assert self.G.edges[u, v] == self.H.edges[u, v] + # Making a change to G should make a change in H and vice versa. + self.G.edges[0, 1]["name"] = "foo" + assert self.G.edges[0, 1]["name"] == self.H.edges[0, 1]["name"] + self.H.edges[3, 4]["name"] = "bar" + assert self.G.edges[3, 4]["name"] == self.H.edges[3, 4]["name"] + + def test_graph_attr_dict(self): + """Tests that the graph attribute dictionary of the two graphs + is the same object. + + """ + assert self.G.graph is self.H.graph + + +def test_graph_new_extra_args(): + """Test that subclasses can accept additional arguments. + + See: https://github.com/networkx/networkx/issues/8367 + """ + + class MyGraph(nx.Graph): + def __init__(self, incoming_graph_data=None, extra_arg=None, **attr): + super().__init__(incoming_graph_data, **attr) + self.extra_arg = extra_arg + + G = MyGraph(extra_arg="extra arg") + assert G.extra_arg == "extra arg" + + G = MyGraph([], "extra arg") + assert G.extra_arg == "extra arg" + + G = MyGraph([(0, 1)], extra_arg="foo", name="bar") + assert G.extra_arg == "foo" + assert G.graph["name"] == "bar" + assert nx.utils.edges_equal(G.edges, [(0, 1)]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py new file mode 100644 index 0000000000000000000000000000000000000000..6e80878191ee73cd70241a5bab944ab97b7573a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graph_historical.py @@ -0,0 +1,12 @@ +"""Original NetworkX graph tests""" + +import networkx as nx + +from .historical_tests import HistoricalTests + + +class TestGraphHistorical(HistoricalTests): + @classmethod + def setup_class(cls): + HistoricalTests.setup_class() + cls.G = nx.Graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py new file mode 100644 index 0000000000000000000000000000000000000000..113c1f87b0a983f220f61df4759a2696d8ee17ed --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_graphviews.py @@ -0,0 +1,349 @@ +import pytest + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + +# Note: SubGraph views are not tested here. They have their own testing file + + +class TestReverseView: + def setup_method(self): + self.G = nx.path_graph(9, create_using=nx.DiGraph()) + self.rv = nx.reverse_view(self.G) + + def test_pickle(self): + import pickle + + rv = self.rv + prv = pickle.loads(pickle.dumps(rv, -1)) + assert rv._node == prv._node + assert rv._adj == prv._adj + assert rv.graph == prv.graph + + def test_contains(self): + assert (2, 3) in self.G.edges + assert (3, 2) not in self.G.edges + assert (2, 3) not in self.rv.edges + assert (3, 2) in self.rv.edges + + def test_iter(self): + expected = sorted(tuple(reversed(e)) for e in self.G.edges) + assert sorted(self.rv.edges) == expected + + def test_exceptions(self): + G = nx.Graph() + pytest.raises(nx.NetworkXNotImplemented, nx.reverse_view, G) + + def test_subclass(self): + class MyGraph(nx.DiGraph): + def my_method(self): + return "me" + + def to_directed_class(self): + return MyGraph() + + M = MyGraph() + M.add_edge(1, 2) + RM = nx.reverse_view(M) + assert RM.__class__ == MyGraph + RMC = RM.copy() + assert RMC.__class__ == MyGraph + assert RMC.has_edge(2, 1) + assert RMC.my_method() == "me" + + +class TestMultiReverseView: + def setup_method(self): + self.G = nx.path_graph(9, create_using=nx.MultiDiGraph()) + self.G.add_edge(4, 5) + self.rv = nx.reverse_view(self.G) + + def test_pickle(self): + import pickle + + rv = self.rv + prv = pickle.loads(pickle.dumps(rv, -1)) + assert rv._node == prv._node + assert rv._adj == prv._adj + assert rv.graph == prv.graph + + def test_contains(self): + assert (2, 3, 0) in self.G.edges + assert (3, 2, 0) not in self.G.edges + assert (2, 3, 0) not in self.rv.edges + assert (3, 2, 0) in self.rv.edges + assert (5, 4, 1) in self.rv.edges + assert (4, 5, 1) not in self.rv.edges + + def test_iter(self): + expected = sorted((v, u, k) for u, v, k in self.G.edges) + assert sorted(self.rv.edges) == expected + + def test_exceptions(self): + MG = nx.MultiGraph(self.G) + pytest.raises(nx.NetworkXNotImplemented, nx.reverse_view, MG) + + +def test_generic_multitype(): + nxg = nx.graphviews + G = nx.DiGraph([(1, 2)]) + with pytest.raises(nx.NetworkXError): + nxg.generic_graph_view(G, create_using=nx.MultiGraph) + G = nx.MultiDiGraph([(1, 2)]) + with pytest.raises(nx.NetworkXError): + nxg.generic_graph_view(G, create_using=nx.DiGraph) + + +class TestToDirected: + def setup_method(self): + self.G = nx.path_graph(9) + self.dv = nx.to_directed(self.G) + self.MG = nx.path_graph(9, create_using=nx.MultiGraph()) + self.Mdv = nx.to_directed(self.MG) + + def test_directed(self): + assert not self.G.is_directed() + assert self.dv.is_directed() + + def test_already_directed(self): + dd = nx.to_directed(self.dv) + Mdd = nx.to_directed(self.Mdv) + assert edges_equal(dd.edges, self.dv.edges, directed=True) + assert edges_equal(Mdd.edges, self.Mdv.edges, directed=True) + + def test_pickle(self): + import pickle + + dv = self.dv + pdv = pickle.loads(pickle.dumps(dv, -1)) + assert dv._node == pdv._node + assert dv._succ == pdv._succ + assert dv._pred == pdv._pred + assert dv.graph == pdv.graph + + def test_contains(self): + assert (2, 3) in self.G.edges + assert (3, 2) in self.G.edges + assert (2, 3) in self.dv.edges + assert (3, 2) in self.dv.edges + + def test_iter(self): + revd = [tuple(reversed(e)) for e in self.G.edges] + expected = sorted(list(self.G.edges) + revd) + assert sorted(self.dv.edges) == expected + + +class TestToUndirected: + def setup_method(self): + self.DG = nx.path_graph(9, create_using=nx.DiGraph()) + self.uv = nx.to_undirected(self.DG) + self.MDG = nx.path_graph(9, create_using=nx.MultiDiGraph()) + self.Muv = nx.to_undirected(self.MDG) + + def test_directed(self): + assert self.DG.is_directed() + assert not self.uv.is_directed() + + def test_already_undirected(self): + uu = nx.to_undirected(self.uv) + Muu = nx.to_undirected(self.Muv) + assert edges_equal(uu.edges, self.uv.edges) + assert edges_equal(Muu.edges, self.Muv.edges) + + def test_pickle(self): + import pickle + + uv = self.uv + puv = pickle.loads(pickle.dumps(uv, -1)) + assert uv._node == puv._node + assert uv._adj == puv._adj + assert uv.graph == puv.graph + assert hasattr(uv, "_graph") + + def test_contains(self): + assert (2, 3) in self.DG.edges + assert (3, 2) not in self.DG.edges + assert (2, 3) in self.uv.edges + assert (3, 2) in self.uv.edges + + def test_iter(self): + expected = sorted(self.DG.edges) + assert sorted(self.uv.edges) == expected + + +class TestChainsOfViews: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.DG = nx.path_graph(9, create_using=nx.DiGraph()) + cls.MG = nx.path_graph(9, create_using=nx.MultiGraph()) + cls.MDG = nx.path_graph(9, create_using=nx.MultiDiGraph()) + cls.Gv = nx.to_undirected(cls.DG) + cls.DGv = nx.to_directed(cls.G) + cls.MGv = nx.to_undirected(cls.MDG) + cls.MDGv = nx.to_directed(cls.MG) + cls.Rv = cls.DG.reverse() + cls.MRv = cls.MDG.reverse() + cls.graphs = [ + cls.G, + cls.DG, + cls.MG, + cls.MDG, + cls.Gv, + cls.DGv, + cls.MGv, + cls.MDGv, + cls.Rv, + cls.MRv, + ] + for G in cls.graphs: + G.edges, G.nodes, G.degree + + def test_pickle(self): + import pickle + + for G in self.graphs: + H = pickle.loads(pickle.dumps(G, -1)) + assert edges_equal(H.edges, G.edges, directed=G.is_directed()) + assert nodes_equal(H.nodes, G.nodes) + + def test_subgraph_of_subgraph(self): + SGv = nx.subgraph(self.G, range(3, 7)) + SDGv = nx.subgraph(self.DG, range(3, 7)) + SMGv = nx.subgraph(self.MG, range(3, 7)) + SMDGv = nx.subgraph(self.MDG, range(3, 7)) + for G in self.graphs + [SGv, SDGv, SMGv, SMDGv]: + SG = nx.induced_subgraph(G, [4, 5, 6]) + assert list(SG) == [4, 5, 6] + SSG = SG.subgraph([6, 7]) + assert list(SSG) == [6] + # subgraph-subgraph chain is short-cut in base class method + assert SSG._graph is G + + def test_restricted_induced_subgraph_chains(self): + """Test subgraph chains that both restrict and show nodes/edges. + + A restricted_view subgraph should allow induced subgraphs using + G.subgraph that automagically without a chain (meaning the result + is a subgraph view of the original graph not a subgraph-of-subgraph. + """ + hide_nodes = [3, 4, 5] + hide_edges = [(6, 7)] + RG = nx.restricted_view(self.G, hide_nodes, hide_edges) + nodes = [4, 5, 6, 7, 8] + SG = nx.induced_subgraph(RG, nodes) + SSG = RG.subgraph(nodes) + assert RG._graph is self.G + assert SSG._graph is self.G + assert SG._graph is RG + assert edges_equal(SG.edges, SSG.edges) + # should be same as morphing the graph + CG = self.G.copy() + CG.remove_nodes_from(hide_nodes) + CG.remove_edges_from(hide_edges) + assert edges_equal(CG.edges(nodes), SSG.edges) + CG.remove_nodes_from([0, 1, 2, 3]) + assert edges_equal(CG.edges, SSG.edges) + # switch order: subgraph first, then restricted view + SSSG = self.G.subgraph(nodes) + RSG = nx.restricted_view(SSSG, hide_nodes, hide_edges) + assert RSG._graph is not self.G + assert edges_equal(RSG.edges, CG.edges) + + def test_subgraph_copy(self): + for origG in self.graphs: + G = nx.Graph(origG) + SG = G.subgraph([4, 5, 6]) + H = SG.copy() + assert type(G) is type(H) + + def test_subgraph_todirected(self): + SG = nx.induced_subgraph(self.G, [4, 5, 6]) + SSG = SG.to_directed() + assert sorted(SSG) == [4, 5, 6] + assert sorted(SSG.edges) == [(4, 5), (5, 4), (5, 6), (6, 5)] + + def test_subgraph_toundirected(self): + SG = nx.induced_subgraph(self.G, [4, 5, 6]) + SSG = SG.to_undirected() + assert list(SSG) == [4, 5, 6] + assert sorted(SSG.edges) == [(4, 5), (5, 6)] + + def test_reverse_subgraph_toundirected(self): + G = self.DG.reverse(copy=False) + SG = G.subgraph([4, 5, 6]) + SSG = SG.to_undirected() + assert list(SSG) == [4, 5, 6] + assert sorted(SSG.edges) == [(4, 5), (5, 6)] + + def test_reverse_reverse_copy(self): + G = self.DG.reverse(copy=False) + H = G.reverse(copy=True) + assert H.nodes == self.DG.nodes + assert H.edges == self.DG.edges + G = self.MDG.reverse(copy=False) + H = G.reverse(copy=True) + assert H.nodes == self.MDG.nodes + assert H.edges == self.MDG.edges + + def test_subgraph_edgesubgraph_toundirected(self): + G = self.G.copy() + SG = G.subgraph([4, 5, 6]) + SSG = SG.edge_subgraph([(4, 5), (5, 4)]) + USSG = SSG.to_undirected() + assert list(USSG) == [4, 5] + assert sorted(USSG.edges) == [(4, 5)] + + def test_copy_subgraph(self): + G = self.G.copy() + SG = G.subgraph([4, 5, 6]) + CSG = SG.copy(as_view=True) + DCSG = SG.copy(as_view=False) + assert hasattr(CSG, "_graph") # is a view + assert not hasattr(DCSG, "_graph") # not a view + + def test_copy_disubgraph(self): + G = self.DG.copy() + SG = G.subgraph([4, 5, 6]) + CSG = SG.copy(as_view=True) + DCSG = SG.copy(as_view=False) + assert hasattr(CSG, "_graph") # is a view + assert not hasattr(DCSG, "_graph") # not a view + + def test_copy_multidisubgraph(self): + G = self.MDG.copy() + SG = G.subgraph([4, 5, 6]) + CSG = SG.copy(as_view=True) + DCSG = SG.copy(as_view=False) + assert hasattr(CSG, "_graph") # is a view + assert not hasattr(DCSG, "_graph") # not a view + + def test_copy_multisubgraph(self): + G = self.MG.copy() + SG = G.subgraph([4, 5, 6]) + CSG = SG.copy(as_view=True) + DCSG = SG.copy(as_view=False) + assert hasattr(CSG, "_graph") # is a view + assert not hasattr(DCSG, "_graph") # not a view + + def test_copy_of_view(self): + G = nx.MultiGraph(self.MGv) + assert G.__class__.__name__ == "MultiGraph" + G = G.copy(as_view=True) + assert G.__class__.__name__ == "MultiGraph" + + def test_subclass(self): + class MyGraph(nx.DiGraph): + def my_method(self): + return "me" + + def to_directed_class(self): + return MyGraph() + + for origG in self.graphs: + G = MyGraph(origG) + SG = G.subgraph([4, 5, 6]) + H = SG.copy() + assert SG.my_method() == "me" + assert H.my_method() == "me" + assert 3 not in H or 3 in SG diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py new file mode 100644 index 0000000000000000000000000000000000000000..fc0bd5467d0a62dc8f533af7a6c5bbc0a57fc010 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multidigraph.py @@ -0,0 +1,459 @@ +from collections import UserDict + +import pytest + +import networkx as nx +from networkx.utils import edges_equal + +from .test_multigraph import BaseMultiGraphTester +from .test_multigraph import TestEdgeSubgraph as _TestMultiGraphEdgeSubgraph +from .test_multigraph import TestMultiGraph as _TestMultiGraph + + +class BaseMultiDiGraphTester(BaseMultiGraphTester): + def test_edges(self): + G = self.K3 + edges = [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.edges()) == edges + assert sorted(G.edges(0)) == [(0, 1), (0, 2)] + pytest.raises((KeyError, nx.NetworkXError), G.edges, -1) + + def test_edges_data(self): + G = self.K3 + edges = [(0, 1, {}), (0, 2, {}), (1, 0, {}), (1, 2, {}), (2, 0, {}), (2, 1, {})] + assert sorted(G.edges(data=True)) == edges + assert sorted(G.edges(0, data=True)) == [(0, 1, {}), (0, 2, {})] + pytest.raises((KeyError, nx.NetworkXError), G.neighbors, -1) + + def test_edges_multi(self): + G = self.K3 + assert sorted(G.edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.edges(0)) == [(0, 1), (0, 2)] + G.add_edge(0, 1) + assert sorted(G.edges()) == [ + (0, 1), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + ] + + def test_out_edges(self): + G = self.K3 + assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)] + pytest.raises((KeyError, nx.NetworkXError), G.out_edges, -1) + assert sorted(G.out_edges(0, keys=True)) == [(0, 1, 0), (0, 2, 0)] + + def test_out_edges_multi(self): + G = self.K3 + assert sorted(G.out_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.out_edges(0)) == [(0, 1), (0, 2)] + G.add_edge(0, 1, 2) + assert sorted(G.out_edges()) == [ + (0, 1), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + ] + + def test_out_edges_data(self): + G = self.K3 + assert sorted(G.edges(0, data=True)) == [(0, 1, {}), (0, 2, {})] + G.remove_edge(0, 1) + G.add_edge(0, 1, data=1) + assert sorted(G.edges(0, data=True)) == [(0, 1, {"data": 1}), (0, 2, {})] + assert sorted(G.edges(0, data="data")) == [(0, 1, 1), (0, 2, None)] + assert sorted(G.edges(0, data="data", default=-1)) == [(0, 1, 1), (0, 2, -1)] + + def test_in_edges(self): + G = self.K3 + assert sorted(G.in_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.in_edges(0)) == [(1, 0), (2, 0)] + pytest.raises((KeyError, nx.NetworkXError), G.in_edges, -1) + G.add_edge(0, 1, 2) + assert sorted(G.in_edges()) == [ + (0, 1), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + ] + assert sorted(G.in_edges(0, keys=True)) == [(1, 0, 0), (2, 0, 0)] + + def test_in_edges_no_keys(self): + G = self.K3 + assert sorted(G.in_edges()) == [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + assert sorted(G.in_edges(0)) == [(1, 0), (2, 0)] + G.add_edge(0, 1, 2) + assert sorted(G.in_edges()) == [ + (0, 1), + (0, 1), + (0, 2), + (1, 0), + (1, 2), + (2, 0), + (2, 1), + ] + + assert sorted(G.in_edges(data=True, keys=False)) == [ + (0, 1, {}), + (0, 1, {}), + (0, 2, {}), + (1, 0, {}), + (1, 2, {}), + (2, 0, {}), + (2, 1, {}), + ] + + def test_in_edges_data(self): + G = self.K3 + assert sorted(G.in_edges(0, data=True)) == [(1, 0, {}), (2, 0, {})] + G.remove_edge(1, 0) + G.add_edge(1, 0, data=1) + assert sorted(G.in_edges(0, data=True)) == [(1, 0, {"data": 1}), (2, 0, {})] + assert sorted(G.in_edges(0, data="data")) == [(1, 0, 1), (2, 0, None)] + assert sorted(G.in_edges(0, data="data", default=-1)) == [(1, 0, 1), (2, 0, -1)] + + def is_shallow(self, H, G): + # graph + assert G.graph["foo"] == H.graph["foo"] + G.graph["foo"].append(1) + assert G.graph["foo"] == H.graph["foo"] + # node + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + G.nodes[0]["foo"].append(1) + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + # edge + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + G[1][2][0]["foo"].append(1) + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + + def is_deep(self, H, G): + # graph + assert G.graph["foo"] == H.graph["foo"] + G.graph["foo"].append(1) + assert G.graph["foo"] != H.graph["foo"] + # node + assert G.nodes[0]["foo"] == H.nodes[0]["foo"] + G.nodes[0]["foo"].append(1) + assert G.nodes[0]["foo"] != H.nodes[0]["foo"] + # edge + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + G[1][2][0]["foo"].append(1) + assert G[1][2][0]["foo"] != H[1][2][0]["foo"] + + def test_to_undirected(self): + # MultiDiGraph -> MultiGraph changes number of edges so it is + # not a copy operation... use is_shallow, not is_shallow_copy + G = self.K3 + self.add_attributes(G) + H = nx.MultiGraph(G) + # self.is_shallow(H,G) + # the result is traversal order dependent so we + # can't use the is_shallow() test here. + try: + assert edges_equal(H.edges(), [(0, 1), (1, 2), (2, 0)]) + except AssertionError: + assert edges_equal(H.edges(), [(0, 1), (1, 2), (1, 2), (2, 0)]) + H = G.to_undirected() + self.is_deep(H, G) + + def test_has_successor(self): + G = self.K3 + assert G.has_successor(0, 1) + assert not G.has_successor(0, -1) + + def test_successors(self): + G = self.K3 + assert sorted(G.successors(0)) == [1, 2] + pytest.raises((KeyError, nx.NetworkXError), G.successors, -1) + + def test_has_predecessor(self): + G = self.K3 + assert G.has_predecessor(0, 1) + assert not G.has_predecessor(0, -1) + + def test_predecessors(self): + G = self.K3 + assert sorted(G.predecessors(0)) == [1, 2] + pytest.raises((KeyError, nx.NetworkXError), G.predecessors, -1) + + def test_degree(self): + G = self.K3 + assert sorted(G.degree()) == [(0, 4), (1, 4), (2, 4)] + assert dict(G.degree()) == {0: 4, 1: 4, 2: 4} + assert G.degree(0) == 4 + assert list(G.degree(iter([0]))) == [(0, 4)] + G.add_edge(0, 1, weight=0.3, other=1.2) + assert sorted(G.degree(weight="weight")) == [(0, 4.3), (1, 4.3), (2, 4)] + assert sorted(G.degree(weight="other")) == [(0, 5.2), (1, 5.2), (2, 4)] + + def test_in_degree(self): + G = self.K3 + assert sorted(G.in_degree()) == [(0, 2), (1, 2), (2, 2)] + assert dict(G.in_degree()) == {0: 2, 1: 2, 2: 2} + assert G.in_degree(0) == 2 + assert list(G.in_degree(iter([0]))) == [(0, 2)] + assert G.in_degree(0, weight="weight") == 2 + + def test_out_degree(self): + G = self.K3 + assert sorted(G.out_degree()) == [(0, 2), (1, 2), (2, 2)] + assert dict(G.out_degree()) == {0: 2, 1: 2, 2: 2} + assert G.out_degree(0) == 2 + assert list(G.out_degree(iter([0]))) == [(0, 2)] + assert G.out_degree(0, weight="weight") == 2 + + def test_size(self): + G = self.K3 + assert G.size() == 6 + assert G.number_of_edges() == 6 + G.add_edge(0, 1, weight=0.3, other=1.2) + assert round(G.size(weight="weight"), 2) == 6.3 + assert round(G.size(weight="other"), 2) == 7.2 + + def test_to_undirected_reciprocal(self): + G = self.Graph() + G.add_edge(1, 2) + assert G.to_undirected().has_edge(1, 2) + assert not G.to_undirected(reciprocal=True).has_edge(1, 2) + G.add_edge(2, 1) + assert G.to_undirected(reciprocal=True).has_edge(1, 2) + + def test_reverse_copy(self): + G = nx.MultiDiGraph([(0, 1), (0, 1)]) + R = G.reverse() + assert sorted(R.edges()) == [(1, 0), (1, 0)] + R.remove_edge(1, 0) + assert sorted(R.edges()) == [(1, 0)] + assert sorted(G.edges()) == [(0, 1), (0, 1)] + + def test_reverse_nocopy(self): + G = nx.MultiDiGraph([(0, 1), (0, 1)]) + R = G.reverse(copy=False) + assert sorted(R.edges()) == [(1, 0), (1, 0)] + pytest.raises(nx.NetworkXError, R.remove_edge, 1, 0) + + def test_di_attributes_cached(self): + G = self.K3.copy() + assert id(G.in_edges) == id(G.in_edges) + assert id(G.out_edges) == id(G.out_edges) + assert id(G.in_degree) == id(G.in_degree) + assert id(G.out_degree) == id(G.out_degree) + assert id(G.succ) == id(G.succ) + assert id(G.pred) == id(G.pred) + + +class TestMultiDiGraph(BaseMultiDiGraphTester, _TestMultiGraph): + def setup_method(self): + self.Graph = nx.MultiDiGraph + # build K3 + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._succ = {0: {}, 1: {}, 2: {}} + # K3._adj is synced with K3._succ + self.K3._pred = {0: {}, 1: {}, 2: {}} + for u in self.k3nodes: + for v in self.k3nodes: + if u == v: + continue + d = {0: {}} + self.K3._succ[u][v] = d + self.K3._pred[v][u] = d + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + def test_add_edge(self): + G = self.Graph() + G.add_edge(0, 1) + assert G._adj == {0: {1: {0: {}}}, 1: {}} + assert G._succ == {0: {1: {0: {}}}, 1: {}} + assert G._pred == {0: {}, 1: {0: {0: {}}}} + G = self.Graph() + G.add_edge(*(0, 1)) + assert G._adj == {0: {1: {0: {}}}, 1: {}} + assert G._succ == {0: {1: {0: {}}}, 1: {}} + assert G._pred == {0: {}, 1: {0: {0: {}}}} + with pytest.raises(ValueError, match="None cannot be a node"): + G.add_edge(None, 3) + + def test_add_edges_from(self): + G = self.Graph() + G.add_edges_from([(0, 1), (0, 1, {"weight": 3})]) + assert G._adj == {0: {1: {0: {}, 1: {"weight": 3}}}, 1: {}} + assert G._succ == {0: {1: {0: {}, 1: {"weight": 3}}}, 1: {}} + assert G._pred == {0: {}, 1: {0: {0: {}, 1: {"weight": 3}}}} + + G.add_edges_from([(0, 1), (0, 1, {"weight": 3})], weight=2) + assert G._succ == { + 0: {1: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}}, + 1: {}, + } + assert G._pred == { + 0: {}, + 1: {0: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}}, + } + + G = self.Graph() + edges = [ + (0, 1, {"weight": 3}), + (0, 1, (("weight", 2),)), + (0, 1, 5), + (0, 1, "s"), + ] + G.add_edges_from(edges) + keydict = {0: {"weight": 3}, 1: {"weight": 2}, 5: {}, "s": {}} + assert G._succ == {0: {1: keydict}, 1: {}} + assert G._pred == {1: {0: keydict}, 0: {}} + + # too few in tuple + pytest.raises(nx.NetworkXError, G.add_edges_from, [(0,)]) + # too many in tuple + pytest.raises(nx.NetworkXError, G.add_edges_from, [(0, 1, 2, 3, 4)]) + # not a tuple + pytest.raises(TypeError, G.add_edges_from, [0]) + with pytest.raises(ValueError, match="None cannot be a node"): + G.add_edges_from([(None, 3), (3, 2)]) + + def test_remove_edge(self): + G = self.K3 + G.remove_edge(0, 1) + assert G._succ == { + 0: {2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + assert G._pred == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, -1, 0) + pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, 0, 2, key=1) + + def test_remove_multiedge(self): + G = self.K3 + G.add_edge(0, 1, key="parallel edge") + G.remove_edge(0, 1, key="parallel edge") + assert G._adj == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + + assert G._succ == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + + assert G._pred == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + G.remove_edge(0, 1) + assert G._succ == { + 0: {2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + assert G._pred == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + pytest.raises((KeyError, nx.NetworkXError), G.remove_edge, -1, 0) + + def test_remove_edges_from(self): + G = self.K3 + G.remove_edges_from([(0, 1)]) + assert G._succ == { + 0: {2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + assert G._pred == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + G.remove_edges_from([(0, 0)]) # silent fail + + +class TestEdgeSubgraph(_TestMultiGraphEdgeSubgraph): + """Unit tests for the :meth:`MultiDiGraph.edge_subgraph` method.""" + + def setup_method(self): + # Create a quadruply-linked path graph on five nodes. + G = nx.MultiDiGraph() + nx.add_path(G, range(5)) + nx.add_path(G, range(5)) + nx.add_path(G, reversed(range(5))) + nx.add_path(G, reversed(range(5))) + # Add some node, edge, and graph attributes. + for i in range(5): + G.nodes[i]["name"] = f"node{i}" + G.adj[0][1][0]["name"] = "edge010" + G.adj[0][1][1]["name"] = "edge011" + G.adj[3][4][0]["name"] = "edge340" + G.adj[3][4][1]["name"] = "edge341" + G.graph["name"] = "graph" + # Get the subgraph induced by one of the first edges and one of + # the last edges. + self.G = G + self.H = G.edge_subgraph([(0, 1, 0), (3, 4, 1)]) + + +class CustomDictClass(UserDict): + pass + + +class MultiDiGraphSubClass(nx.MultiDiGraph): + node_dict_factory = CustomDictClass # type: ignore[assignment] + node_attr_dict_factory = CustomDictClass # type: ignore[assignment] + adjlist_outer_dict_factory = CustomDictClass # type: ignore[assignment] + adjlist_inner_dict_factory = CustomDictClass # type: ignore[assignment] + edge_key_dict_factory = CustomDictClass # type: ignore[assignment] + edge_attr_dict_factory = CustomDictClass # type: ignore[assignment] + graph_attr_dict_factory = CustomDictClass # type: ignore[assignment] + + +class TestMultiDiGraphSubclass(TestMultiDiGraph): + def setup_method(self): + self.Graph = MultiDiGraphSubClass + # build K3 + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._succ = self.K3.adjlist_outer_dict_factory( + { + 0: self.K3.adjlist_inner_dict_factory(), + 1: self.K3.adjlist_inner_dict_factory(), + 2: self.K3.adjlist_inner_dict_factory(), + } + ) + # K3._adj is synced with K3._succ + self.K3._pred = {0: {}, 1: {}, 2: {}} + for u in self.k3nodes: + for v in self.k3nodes: + if u == v: + continue + d = {0: {}} + self.K3._succ[u][v] = d + self.K3._pred[v][u] = d + self.K3._node = self.K3.node_dict_factory() + self.K3._node[0] = self.K3.node_attr_dict_factory() + self.K3._node[1] = self.K3.node_attr_dict_factory() + self.K3._node[2] = self.K3.node_attr_dict_factory() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py new file mode 100644 index 0000000000000000000000000000000000000000..cd912d1d7c33c056b3c9808221bf7b72cd10fcac --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_multigraph.py @@ -0,0 +1,528 @@ +from collections import UserDict + +import pytest + +import networkx as nx +from networkx.utils import edges_equal + +from .test_graph import BaseAttrGraphTester +from .test_graph import TestGraph as _TestGraph + + +class BaseMultiGraphTester(BaseAttrGraphTester): + def test_has_edge(self): + G = self.K3 + assert G.has_edge(0, 1) + assert not G.has_edge(0, -1) + assert G.has_edge(0, 1, 0) + assert not G.has_edge(0, 1, 1) + + def test_get_edge_data(self): + G = self.K3 + assert G.get_edge_data(0, 1) == {0: {}} + assert G[0][1] == {0: {}} + assert G[0][1][0] == {} + assert G.get_edge_data(10, 20) is None + assert G.get_edge_data(0, 1, 0) == {} + + def test_adjacency(self): + G = self.K3 + assert dict(G.adjacency()) == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + + def deepcopy_edge_attr(self, H, G): + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + G[1][2][0]["foo"].append(1) + assert G[1][2][0]["foo"] != H[1][2][0]["foo"] + + def shallow_copy_edge_attr(self, H, G): + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + G[1][2][0]["foo"].append(1) + assert G[1][2][0]["foo"] == H[1][2][0]["foo"] + + def graphs_equal(self, H, G): + assert G._adj == H._adj + assert G._node == H._node + assert G.graph == H.graph + assert G.name == H.name + if not G.is_directed() and not H.is_directed(): + assert H._adj[1][2][0] is H._adj[2][1][0] + assert G._adj[1][2][0] is G._adj[2][1][0] + else: # at least one is directed + if not G.is_directed(): + G._pred = G._adj + G._succ = G._adj + if not H.is_directed(): + H._pred = H._adj + H._succ = H._adj + assert G._pred == H._pred + assert G._succ == H._succ + assert H._succ[1][2][0] is H._pred[2][1][0] + assert G._succ[1][2][0] is G._pred[2][1][0] + + def same_attrdict(self, H, G): + # same attrdict in the edgedata + old_foo = H[1][2][0]["foo"] + H.adj[1][2][0]["foo"] = "baz" + assert G._adj == H._adj + H.adj[1][2][0]["foo"] = old_foo + assert G._adj == H._adj + + old_foo = H.nodes[0]["foo"] + H.nodes[0]["foo"] = "baz" + assert G._node == H._node + H.nodes[0]["foo"] = old_foo + assert G._node == H._node + + def different_attrdict(self, H, G): + # used by graph_equal_but_different + old_foo = H[1][2][0]["foo"] + H.adj[1][2][0]["foo"] = "baz" + assert G._adj != H._adj + H.adj[1][2][0]["foo"] = old_foo + assert G._adj == H._adj + + old_foo = H.nodes[0]["foo"] + H.nodes[0]["foo"] = "baz" + assert G._node != H._node + H.nodes[0]["foo"] = old_foo + assert G._node == H._node + + def test_to_undirected(self): + G = self.K3 + self.add_attributes(G) + H = nx.MultiGraph(G) + self.is_shallow_copy(H, G) + H = G.to_undirected() + self.is_deepcopy(H, G) + + def test_to_directed(self): + G = self.K3 + self.add_attributes(G) + H = nx.MultiDiGraph(G) + self.is_shallow_copy(H, G) + H = G.to_directed() + self.is_deepcopy(H, G) + + def test_number_of_edges_selfloops(self): + G = self.K3 + G.add_edge(0, 0) + G.add_edge(0, 0) + G.add_edge(0, 0, key="parallel edge") + G.remove_edge(0, 0, key="parallel edge") + assert G.number_of_edges(0, 0) == 2 + G.remove_edge(0, 0) + assert G.number_of_edges(0, 0) == 1 + + def test_edge_lookup(self): + G = self.Graph() + G.add_edge(1, 2, foo="bar") + G.add_edge(1, 2, "key", foo="biz") + assert edges_equal(G.edges[1, 2, 0], {"foo": "bar"}) + assert edges_equal(G.edges[1, 2, "key"], {"foo": "biz"}) + + def test_edge_attr(self): + G = self.Graph() + G.add_edge(1, 2, key="k1", foo="bar") + G.add_edge(1, 2, key="k2", foo="baz") + assert isinstance(G.get_edge_data(1, 2), G.edge_key_dict_factory) + assert all( + isinstance(d, G.edge_attr_dict_factory) for u, v, d in G.edges(data=True) + ) + assert edges_equal( + G.edges(keys=True, data=True), + [(1, 2, "k1", {"foo": "bar"}), (1, 2, "k2", {"foo": "baz"})], + ) + assert edges_equal( + G.edges(keys=True, data="foo"), [(1, 2, "k1", "bar"), (1, 2, "k2", "baz")] + ) + + def test_edge_attr4(self): + G = self.Graph() + G.add_edge(1, 2, key=0, data=7, spam="bar", bar="foo") + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 7, "spam": "bar", "bar": "foo"})] + ) + G[1][2][0]["data"] = 10 # OK to set data like this + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 10, "spam": "bar", "bar": "foo"})] + ) + + G.adj[1][2][0]["data"] = 20 + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 20, "spam": "bar", "bar": "foo"})] + ) + G.edges[1, 2, 0]["data"] = 21 # another spelling, "edge" + assert edges_equal( + G.edges(data=True), [(1, 2, {"data": 21, "spam": "bar", "bar": "foo"})] + ) + G.adj[1][2][0]["listdata"] = [20, 200] + G.adj[1][2][0]["weight"] = 20 + assert edges_equal( + G.edges(data=True), + [ + ( + 1, + 2, + { + "data": 21, + "spam": "bar", + "bar": "foo", + "listdata": [20, 200], + "weight": 20, + }, + ) + ], + ) + + +class TestMultiGraph(BaseMultiGraphTester, _TestGraph): + def setup_method(self): + self.Graph = nx.MultiGraph + # build K3 + ed1, ed2, ed3 = ({0: {}}, {0: {}}, {0: {}}) + self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}} + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._adj = self.k3adj + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + def test_data_input(self): + G = self.Graph({1: [2], 2: [1]}, name="test") + assert G.name == "test" + expected = [(1, {2: {0: {}}}), (2, {1: {0: {}}})] + assert sorted(G.adj.items()) == expected + + def test_data_multigraph_input(self): + # standard case with edge keys and edge data + edata0 = {"w": 200, "s": "foo"} + edata1 = {"w": 201, "s": "bar"} + keydict = {0: edata0, 1: edata1} + dododod = {"a": {"b": keydict}} + + multiple_edge = [("a", "b", 0, edata0), ("a", "b", 1, edata1)] + single_edge = [("a", "b", 0, keydict)] + + G = self.Graph(dododod, multigraph_input=True) + assert list(G.edges(keys=True, data=True)) == multiple_edge + G = self.Graph(dododod, multigraph_input=None) + assert list(G.edges(keys=True, data=True)) == multiple_edge + G = self.Graph(dododod, multigraph_input=False) + assert list(G.edges(keys=True, data=True)) == single_edge + + # test round-trip to_dict_of_dict and MultiGraph constructor + G = self.Graph(dododod, multigraph_input=True) + H = self.Graph(nx.to_dict_of_dicts(G)) + assert nx.is_isomorphic(G, H) is True # test that default is True + for mgi in [True, False]: + H = self.Graph(nx.to_dict_of_dicts(G), multigraph_input=mgi) + assert nx.is_isomorphic(G, H) == mgi + + # Set up cases for when incoming_graph_data is not multigraph_input + etraits = {"w": 200, "s": "foo"} + egraphics = {"color": "blue", "shape": "box"} + edata = {"traits": etraits, "graphics": egraphics} + dodod1 = {"a": {"b": edata}} + dodod2 = {"a": {"b": etraits}} + dodod3 = {"a": {"b": {"traits": etraits, "s": "foo"}}} + dol = {"a": ["b"]} + + multiple_edge = [("a", "b", "traits", etraits), ("a", "b", "graphics", egraphics)] + single_edge = [("a", "b", 0, {})] # type: ignore[var-annotated] + single_edge1 = [("a", "b", 0, edata)] + single_edge2 = [("a", "b", 0, etraits)] + single_edge3 = [("a", "b", 0, {"traits": etraits, "s": "foo"})] + + cases = [ # (dod, mgi, edges) + (dodod1, True, multiple_edge), + (dodod1, False, single_edge1), + (dodod2, False, single_edge2), + (dodod3, False, single_edge3), + (dol, False, single_edge), + ] + + @pytest.mark.parametrize("dod, mgi, edges", cases) + def test_non_multigraph_input(self, dod, mgi, edges): + G = self.Graph(dod, multigraph_input=mgi) + assert list(G.edges(keys=True, data=True)) == edges + G = nx.to_networkx_graph(dod, create_using=self.Graph, multigraph_input=mgi) + assert list(G.edges(keys=True, data=True)) == edges + + mgi_none_cases = [ + (dodod1, multiple_edge), + (dodod2, single_edge2), + (dodod3, single_edge3), + ] + + @pytest.mark.parametrize("dod, edges", mgi_none_cases) + def test_non_multigraph_input_mgi_none(self, dod, edges): + # test constructor without to_networkx_graph for mgi=None + G = self.Graph(dod) + assert list(G.edges(keys=True, data=True)) == edges + + raise_cases = [dodod2, dodod3, dol] + + @pytest.mark.parametrize("dod", raise_cases) + def test_non_multigraph_input_raise(self, dod): + # cases where NetworkXError is raised + pytest.raises(nx.NetworkXError, self.Graph, dod, multigraph_input=True) + pytest.raises( + nx.NetworkXError, + nx.to_networkx_graph, + dod, + create_using=self.Graph, + multigraph_input=True, + ) + + def test_getitem(self): + G = self.K3 + assert G[0] == {1: {0: {}}, 2: {0: {}}} + with pytest.raises(KeyError): + G.__getitem__("j") + with pytest.raises(TypeError): + G.__getitem__(["A"]) + + def test_remove_node(self): + G = self.K3 + G.remove_node(0) + assert G.adj == {1: {2: {0: {}}}, 2: {1: {0: {}}}} + with pytest.raises(nx.NetworkXError): + G.remove_node(-1) + + def test_add_edge(self): + G = self.Graph() + G.add_edge(0, 1) + assert G.adj == {0: {1: {0: {}}}, 1: {0: {0: {}}}} + G = self.Graph() + G.add_edge(*(0, 1)) + assert G.adj == {0: {1: {0: {}}}, 1: {0: {0: {}}}} + G = self.Graph() + with pytest.raises(ValueError): + G.add_edge(None, "anything") + + def test_add_edge_conflicting_key(self): + G = self.Graph() + G.add_edge(0, 1, key=1) + G.add_edge(0, 1) + assert G.number_of_edges() == 2 + G = self.Graph() + G.add_edges_from([(0, 1, 1, {})]) + G.add_edges_from([(0, 1)]) + assert G.number_of_edges() == 2 + + def test_add_edges_from(self): + G = self.Graph() + G.add_edges_from([(0, 1), (0, 1, {"weight": 3})]) + assert G.adj == { + 0: {1: {0: {}, 1: {"weight": 3}}}, + 1: {0: {0: {}, 1: {"weight": 3}}}, + } + G.add_edges_from([(0, 1), (0, 1, {"weight": 3})], weight=2) + assert G.adj == { + 0: {1: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}}, + 1: {0: {0: {}, 1: {"weight": 3}, 2: {"weight": 2}, 3: {"weight": 3}}}, + } + G = self.Graph() + edges = [ + (0, 1, {"weight": 3}), + (0, 1, (("weight", 2),)), + (0, 1, 5), + (0, 1, "s"), + ] + G.add_edges_from(edges) + keydict = {0: {"weight": 3}, 1: {"weight": 2}, 5: {}, "s": {}} + assert G._adj == {0: {1: keydict}, 1: {0: keydict}} + + # too few in tuple + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0,)]) + # too many in tuple + with pytest.raises(nx.NetworkXError): + G.add_edges_from([(0, 1, 2, 3, 4)]) + # not a tuple + with pytest.raises(TypeError): + G.add_edges_from([0]) + + def test_multigraph_add_edges_from_four_tuple_misordered(self): + """add_edges_from expects 4-tuples of the format (u, v, key, data_dict). + + Ensure 4-tuples of form (u, v, data_dict, key) raise exception. + """ + G = nx.MultiGraph() + with pytest.raises(TypeError): + # key/data values flipped in 4-tuple + G.add_edges_from([(0, 1, {"color": "red"}, 0)]) + + def test_remove_edge(self): + G = self.K3 + G.remove_edge(0, 1) + assert G.adj == {0: {2: {0: {}}}, 1: {2: {0: {}}}, 2: {0: {0: {}}, 1: {0: {}}}} + + with pytest.raises(nx.NetworkXError): + G.remove_edge(-1, 0) + with pytest.raises(nx.NetworkXError): + G.remove_edge(0, 2, key=1) + + def test_remove_edges_from(self): + G = self.K3.copy() + G.remove_edges_from([(0, 1)]) + kd = {0: {}} + assert G.adj == {0: {2: kd}, 1: {2: kd}, 2: {0: kd, 1: kd}} + G.remove_edges_from([(0, 0)]) # silent fail + self.K3.add_edge(0, 1) + G = self.K3.copy() + G.remove_edges_from(list(G.edges(data=True, keys=True))) + assert G.adj == {0: {}, 1: {}, 2: {}} + G = self.K3.copy() + G.remove_edges_from(list(G.edges(data=False, keys=True))) + assert G.adj == {0: {}, 1: {}, 2: {}} + G = self.K3.copy() + G.remove_edges_from(list(G.edges(data=False, keys=False))) + assert G.adj == {0: {}, 1: {}, 2: {}} + G = self.K3.copy() + G.remove_edges_from([(0, 1, 0), (0, 2, 0, {}), (1, 2)]) + assert G.adj == {0: {1: {1: {}}}, 1: {0: {1: {}}}, 2: {}} + + def test_remove_multiedge(self): + G = self.K3 + G.add_edge(0, 1, key="parallel edge") + G.remove_edge(0, 1, key="parallel edge") + assert G.adj == { + 0: {1: {0: {}}, 2: {0: {}}}, + 1: {0: {0: {}}, 2: {0: {}}}, + 2: {0: {0: {}}, 1: {0: {}}}, + } + G.remove_edge(0, 1) + kd = {0: {}} + assert G.adj == {0: {2: kd}, 1: {2: kd}, 2: {0: kd, 1: kd}} + with pytest.raises(nx.NetworkXError): + G.remove_edge(-1, 0) + + +class TestEdgeSubgraph: + """Unit tests for the :meth:`MultiGraph.edge_subgraph` method.""" + + def setup_method(self): + # Create a doubly-linked path graph on five nodes. + G = nx.MultiGraph() + nx.add_path(G, range(5)) + nx.add_path(G, range(5)) + # Add some node, edge, and graph attributes. + for i in range(5): + G.nodes[i]["name"] = f"node{i}" + G.adj[0][1][0]["name"] = "edge010" + G.adj[0][1][1]["name"] = "edge011" + G.adj[3][4][0]["name"] = "edge340" + G.adj[3][4][1]["name"] = "edge341" + G.graph["name"] = "graph" + # Get the subgraph induced by one of the first edges and one of + # the last edges. + self.G = G + self.H = G.edge_subgraph([(0, 1, 0), (3, 4, 1)]) + + def test_correct_nodes(self): + """Tests that the subgraph has the correct nodes.""" + assert [0, 1, 3, 4] == sorted(self.H.nodes()) + + def test_correct_edges(self): + """Tests that the subgraph has the correct edges.""" + assert [(0, 1, 0, "edge010"), (3, 4, 1, "edge341")] == sorted( + self.H.edges(keys=True, data="name") + ) + + def test_add_node(self): + """Tests that adding a node to the original graph does not + affect the nodes of the subgraph. + + """ + self.G.add_node(5) + assert [0, 1, 3, 4] == sorted(self.H.nodes()) + + def test_remove_node(self): + """Tests that removing a node in the original graph does + affect the nodes of the subgraph. + + """ + self.G.remove_node(0) + assert [1, 3, 4] == sorted(self.H.nodes()) + + def test_node_attr_dict(self): + """Tests that the node attribute dictionary of the two graphs is + the same object. + + """ + for v in self.H: + assert self.G.nodes[v] == self.H.nodes[v] + # Making a change to G should make a change in H and vice versa. + self.G.nodes[0]["name"] = "foo" + assert self.G.nodes[0] == self.H.nodes[0] + self.H.nodes[1]["name"] = "bar" + assert self.G.nodes[1] == self.H.nodes[1] + + def test_edge_attr_dict(self): + """Tests that the edge attribute dictionary of the two graphs is + the same object. + + """ + for u, v, k in self.H.edges(keys=True): + assert self.G._adj[u][v][k] == self.H._adj[u][v][k] + # Making a change to G should make a change in H and vice versa. + self.G._adj[0][1][0]["name"] = "foo" + assert self.G._adj[0][1][0]["name"] == self.H._adj[0][1][0]["name"] + self.H._adj[3][4][1]["name"] = "bar" + assert self.G._adj[3][4][1]["name"] == self.H._adj[3][4][1]["name"] + + def test_graph_attr_dict(self): + """Tests that the graph attribute dictionary of the two graphs + is the same object. + + """ + assert self.G.graph is self.H.graph + + +class CustomDictClass(UserDict): + pass + + +class MultiGraphSubClass(nx.MultiGraph): + node_dict_factory = CustomDictClass # type: ignore[assignment] + node_attr_dict_factory = CustomDictClass # type: ignore[assignment] + adjlist_outer_dict_factory = CustomDictClass # type: ignore[assignment] + adjlist_inner_dict_factory = CustomDictClass # type: ignore[assignment] + edge_key_dict_factory = CustomDictClass # type: ignore[assignment] + edge_attr_dict_factory = CustomDictClass # type: ignore[assignment] + graph_attr_dict_factory = CustomDictClass # type: ignore[assignment] + + +class TestMultiGraphSubclass(TestMultiGraph): + def setup_method(self): + self.Graph = MultiGraphSubClass + # build K3 + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._adj = self.K3.adjlist_outer_dict_factory( + { + 0: self.K3.adjlist_inner_dict_factory(), + 1: self.K3.adjlist_inner_dict_factory(), + 2: self.K3.adjlist_inner_dict_factory(), + } + ) + self.K3._pred = {0: {}, 1: {}, 2: {}} + for u in self.k3nodes: + for v in self.k3nodes: + if u != v: + d = {0: {}} + self.K3._adj[u][v] = d + self.K3._adj[v][u] = d + self.K3._node = self.K3.node_dict_factory() + self.K3._node[0] = self.K3.node_attr_dict_factory() + self.K3._node[1] = self.K3.node_attr_dict_factory() + self.K3._node[2] = self.K3.node_attr_dict_factory() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py new file mode 100644 index 0000000000000000000000000000000000000000..8461be21838f34d9a0c49bdf0f5ad81c9fadd00f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_reportviews.py @@ -0,0 +1,1421 @@ +import pickle +from copy import deepcopy + +import pytest + +import networkx as nx +from networkx.classes import reportviews as rv +from networkx.classes.reportviews import NodeDataView + + +# Nodes +class TestNodeView: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.nv = cls.G.nodes # NodeView(G) + + def test_pickle(self): + import pickle + + nv = self.nv + pnv = pickle.loads(pickle.dumps(nv, -1)) + assert nv == pnv + assert nv.__slots__ == pnv.__slots__ + + def test_str(self): + assert str(self.nv) == "[0, 1, 2, 3, 4, 5, 6, 7, 8]" + + def test_repr(self): + assert repr(self.nv) == "NodeView((0, 1, 2, 3, 4, 5, 6, 7, 8))" + + def test_contains(self): + G = self.G.copy() + nv = G.nodes + assert 7 in nv + assert 9 not in nv + G.remove_node(7) + G.add_node(9) + assert 7 not in nv + assert 9 in nv + + def test_getitem(self): + G = self.G.copy() + nv = G.nodes + G.nodes[3]["foo"] = "bar" + assert nv[7] == {} + assert nv[3] == {"foo": "bar"} + # slicing + with pytest.raises(nx.NetworkXError): + G.nodes[0:5] + + def test_iter(self): + nv = self.nv + for i, n in enumerate(nv): + assert i == n + inv = iter(nv) + assert next(inv) == 0 + assert iter(nv) != nv + assert iter(inv) == inv + inv2 = iter(nv) + next(inv2) + assert list(inv) == list(inv2) + # odd case where NodeView calls NodeDataView with data=False + nnv = nv(data=False) + for i, n in enumerate(nnv): + assert i == n + + def test_call(self): + nodes = self.nv + assert nodes is nodes() + assert nodes is not nodes(data=True) + assert nodes is not nodes(data="weight") + + +class TestNodeDataView: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.nv = NodeDataView(cls.G) + cls.ndv = cls.G.nodes.data(True) + cls.nwv = cls.G.nodes.data("foo") + + def test_viewtype(self): + nv = self.G.nodes + ndvfalse = nv.data(False) + assert nv is ndvfalse + assert nv is not self.ndv + + def test_pickle(self): + import pickle + + nv = self.nv + pnv = pickle.loads(pickle.dumps(nv, -1)) + assert nv == pnv + assert nv.__slots__ == pnv.__slots__ + + def test_str(self): + msg = str([(n, {}) for n in range(9)]) + assert str(self.ndv) == msg + + def test_repr(self): + expected = "NodeDataView((0, 1, 2, 3, 4, 5, 6, 7, 8))" + assert repr(self.nv) == expected + expected = ( + "NodeDataView({0: {}, 1: {}, 2: {}, 3: {}, " + + "4: {}, 5: {}, 6: {}, 7: {}, 8: {}})" + ) + assert repr(self.ndv) == expected + expected = ( + "NodeDataView({0: None, 1: None, 2: None, 3: None, 4: None, " + + "5: None, 6: None, 7: None, 8: None}, data='foo')" + ) + assert repr(self.nwv) == expected + + def test_contains(self): + G = self.G.copy() + nv = G.nodes.data() + nwv = G.nodes.data("foo") + G.nodes[3]["foo"] = "bar" + assert (7, {}) in nv + assert (3, {"foo": "bar"}) in nv + assert (3, "bar") in nwv + assert (7, None) in nwv + # default + nwv_def = G.nodes(data="foo", default="biz") + assert (7, "biz") in nwv_def + assert (3, "bar") in nwv_def + + def test_getitem(self): + G = self.G.copy() + nv = G.nodes + G.nodes[3]["foo"] = "bar" + assert nv[3] == {"foo": "bar"} + # default + nwv_def = G.nodes(data="foo", default="biz") + assert nwv_def[7], "biz" + assert nwv_def[3] == "bar" + # slicing + with pytest.raises(nx.NetworkXError): + G.nodes.data()[0:5] + + def test_iter(self): + G = self.G.copy() + nv = G.nodes.data() + ndv = G.nodes.data(True) + nwv = G.nodes.data("foo") + for i, (n, d) in enumerate(nv): + assert i == n + assert d == {} + inv = iter(nv) + assert next(inv) == (0, {}) + G.nodes[3]["foo"] = "bar" + # default + for n, d in nv: + if n == 3: + assert d == {"foo": "bar"} + else: + assert d == {} + # data=True + for n, d in ndv: + if n == 3: + assert d == {"foo": "bar"} + else: + assert d == {} + # data='foo' + for n, d in nwv: + if n == 3: + assert d == "bar" + else: + assert d is None + # data='foo', default=1 + for n, d in G.nodes.data("foo", default=1): + if n == 3: + assert d == "bar" + else: + assert d == 1 + + +def test_nodedataview_unhashable(): + G = nx.path_graph(9) + G.nodes[3]["foo"] = "bar" + nvs = [G.nodes.data()] + nvs.append(G.nodes.data(True)) + H = G.copy() + H.nodes[4]["foo"] = {1, 2, 3} + nvs.append(H.nodes.data(True)) + # raise unhashable + for nv in nvs: + pytest.raises(TypeError, set, nv) + pytest.raises(TypeError, eval, "nv | nv", locals()) + # no raise... hashable + Gn = G.nodes.data(False) + set(Gn) + Gn | Gn + Gn = G.nodes.data("foo") + set(Gn) + Gn | Gn + + +class TestNodeViewSetOps: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.G.nodes[3]["foo"] = "bar" + cls.nv = cls.G.nodes + + def n_its(self, nodes): + return set(nodes) + + def test_len(self): + G = self.G.copy() + nv = G.nodes + assert len(nv) == 9 + G.remove_node(7) + assert len(nv) == 8 + G.add_node(9) + assert len(nv) == 9 + + def test_and(self): + nv = self.nv + some_nodes = self.n_its(range(5, 12)) + assert nv & some_nodes == self.n_its(range(5, 9)) + assert some_nodes & nv == self.n_its(range(5, 9)) + + def test_or(self): + nv = self.nv + some_nodes = self.n_its(range(5, 12)) + assert nv | some_nodes == self.n_its(range(12)) + assert some_nodes | nv == self.n_its(range(12)) + + def test_xor(self): + nv = self.nv + some_nodes = self.n_its(range(5, 12)) + nodes = {0, 1, 2, 3, 4, 9, 10, 11} + assert nv ^ some_nodes == self.n_its(nodes) + assert some_nodes ^ nv == self.n_its(nodes) + + def test_sub(self): + nv = self.nv + some_nodes = self.n_its(range(5, 12)) + assert nv - some_nodes == self.n_its(range(5)) + assert some_nodes - nv == self.n_its(range(9, 12)) + + +class TestNodeDataViewSetOps(TestNodeViewSetOps): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.G.nodes[3]["foo"] = "bar" + cls.nv = cls.G.nodes.data("foo") + + def n_its(self, nodes): + return {(node, "bar" if node == 3 else None) for node in nodes} + + +class TestNodeDataViewDefaultSetOps(TestNodeDataViewSetOps): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.G.nodes[3]["foo"] = "bar" + cls.nv = cls.G.nodes.data("foo", default=1) + + def n_its(self, nodes): + return {(node, "bar" if node == 3 else 1) for node in nodes} + + +# Edges Data View +class TestEdgeDataView: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.eview = nx.reportviews.EdgeView + + def test_pickle(self): + import pickle + + ev = self.eview(self.G)(data=True) + pev = pickle.loads(pickle.dumps(ev, -1)) + assert list(ev) == list(pev) + assert ev.__slots__ == pev.__slots__ + + def modify_edge(self, G, e, **kwds): + G._adj[e[0]][e[1]].update(kwds) + + def test_str(self): + ev = self.eview(self.G)(data=True) + rep = str([(n, n + 1, {}) for n in range(8)]) + assert str(ev) == rep + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "EdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_iterdata(self): + G = self.G.copy() + evr = self.eview(G) + ev = evr(data=True) + ev_def = evr(data="foo", default=1) + + for u, v, d in ev: + pass + assert d == {} + + for u, v, wt in ev_def: + pass + assert wt == 1 + + self.modify_edge(G, (2, 3), foo="bar") + for e in ev: + assert len(e) == 3 + if set(e[:2]) == {2, 3}: + assert e[2] == {"foo": "bar"} + checked = True + else: + assert e[2] == {} + assert checked + + for e in ev_def: + assert len(e) == 3 + if set(e[:2]) == {2, 3}: + assert e[2] == "bar" + checked_wt = True + else: + assert e[2] == 1 + assert checked_wt + + def test_iter(self): + evr = self.eview(self.G) + ev = evr() + for u, v in ev: + pass + iev = iter(ev) + assert next(iev) == (0, 1) + assert iter(ev) != ev + assert iter(iev) == iev + + def test_contains(self): + evr = self.eview(self.G) + ev = evr() + if self.G.is_directed(): + assert (1, 2) in ev and (2, 1) not in ev + else: + assert (1, 2) in ev and (2, 1) in ev + assert (1, 4) not in ev + assert (1, 90) not in ev + assert (90, 1) not in ev + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + if self.G.is_directed(): + assert (0, 1) in ev + assert (1, 2) not in ev + assert (2, 3) in ev + else: + assert (0, 1) in ev + assert (1, 2) in ev + assert (2, 3) in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + def test_len(self): + evr = self.eview(self.G) + ev = evr(data="foo") + assert len(ev) == 8 + assert len(evr(1)) == 2 + assert len(evr([1, 2, 3])) == 4 + + assert len(self.G.edges(1)) == 2 + assert len(self.G.edges()) == 8 + assert len(self.G.edges) == 8 + + H = self.G.copy() + H.add_edge(1, 1) + assert len(H.edges(1)) == 3 + assert len(H.edges()) == 9 + assert len(H.edges) == 9 + + +class TestOutEdgeDataView(TestEdgeDataView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=nx.DiGraph()) + cls.eview = nx.reportviews.OutEdgeView + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "OutEdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_len(self): + evr = self.eview(self.G) + ev = evr(data="foo") + assert len(ev) == 8 + assert len(evr(1)) == 1 + assert len(evr([1, 2, 3])) == 3 + + assert len(self.G.edges(1)) == 1 + assert len(self.G.edges()) == 8 + assert len(self.G.edges) == 8 + + H = self.G.copy() + H.add_edge(1, 1) + assert len(H.edges(1)) == 2 + assert len(H.edges()) == 9 + assert len(H.edges) == 9 + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + assert (0, 1) in ev + assert (1, 2) not in ev + assert (2, 3) in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + +class TestInEdgeDataView(TestOutEdgeDataView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=nx.DiGraph()) + cls.eview = nx.reportviews.InEdgeView + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "InEdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + assert (0, 1) not in ev + assert (1, 2) in ev + assert (2, 3) not in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + +class TestMultiEdgeDataView(TestEdgeDataView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=nx.MultiGraph()) + cls.eview = nx.reportviews.MultiEdgeView + + def modify_edge(self, G, e, **kwds): + G._adj[e[0]][e[1]][0].update(kwds) + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "MultiEdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + assert (0, 1) in ev + assert (1, 2) in ev + assert (2, 3) in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + +class TestOutMultiEdgeDataView(TestOutEdgeDataView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=nx.MultiDiGraph()) + cls.eview = nx.reportviews.OutMultiEdgeView + + def modify_edge(self, G, e, **kwds): + G._adj[e[0]][e[1]][0].update(kwds) + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "OutMultiEdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + assert (0, 1) in ev + assert (1, 2) not in ev + assert (2, 3) in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + +class TestInMultiEdgeDataView(TestOutMultiEdgeDataView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=nx.MultiDiGraph()) + cls.eview = nx.reportviews.InMultiEdgeView + + def test_repr(self): + ev = self.eview(self.G)(data=True) + rep = ( + "InMultiEdgeDataView([(0, 1, {}), (1, 2, {}), " + + "(2, 3, {}), (3, 4, {}), " + + "(4, 5, {}), (5, 6, {}), " + + "(6, 7, {}), (7, 8, {})])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + evr = self.eview(self.G) + ev = evr(nbunch=[0, 2]) + assert (0, 1) not in ev + assert (1, 2) in ev + assert (2, 3) not in ev + assert (3, 4) not in ev + assert (4, 5) not in ev + assert (5, 6) not in ev + assert (7, 8) not in ev + assert (8, 9) not in ev + + +# Edge Views +class TestEdgeView: + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9) + cls.eview = nx.reportviews.EdgeView + + def test_pickle(self): + import pickle + + ev = self.eview(self.G) + pev = pickle.loads(pickle.dumps(ev, -1)) + assert ev == pev + assert ev.__slots__ == pev.__slots__ + + def modify_edge(self, G, e, **kwds): + G._adj[e[0]][e[1]].update(kwds) + + def test_str(self): + ev = self.eview(self.G) + rep = str([(n, n + 1) for n in range(8)]) + assert str(ev) == rep + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "EdgeView([(0, 1), (1, 2), (2, 3), (3, 4), " + + "(4, 5), (5, 6), (6, 7), (7, 8)])" + ) + assert repr(ev) == rep + + def test_getitem(self): + G = self.G.copy() + ev = G.edges + G.edges[0, 1]["foo"] = "bar" + assert ev[0, 1] == {"foo": "bar"} + + # slicing + with pytest.raises(nx.NetworkXError, match=".*does not support slicing"): + G.edges[0:5] + + # Invalid edge + with pytest.raises(KeyError, match=r".*edge.*is not in the graph."): + G.edges[0, 9] + + def test_call(self): + ev = self.eview(self.G) + assert id(ev) == id(ev()) + assert id(ev) == id(ev(data=False)) + assert id(ev) != id(ev(data=True)) + assert id(ev) != id(ev(nbunch=1)) + + def test_data(self): + ev = self.eview(self.G) + assert id(ev) != id(ev.data()) + assert id(ev) == id(ev.data(data=False)) + assert id(ev) != id(ev.data(data=True)) + assert id(ev) != id(ev.data(nbunch=1)) + + def test_iter(self): + ev = self.eview(self.G) + for u, v in ev: + pass + iev = iter(ev) + assert next(iev) == (0, 1) + assert iter(ev) != ev + assert iter(iev) == iev + + def test_contains(self): + ev = self.eview(self.G) + edv = ev() + if self.G.is_directed(): + assert (1, 2) in ev and (2, 1) not in ev + assert (1, 2) in edv and (2, 1) not in edv + else: + assert (1, 2) in ev and (2, 1) in ev + assert (1, 2) in edv and (2, 1) in edv + assert (1, 4) not in ev + assert (1, 4) not in edv + # edge not in graph + assert (1, 90) not in ev + assert (90, 1) not in ev + assert (1, 90) not in edv + assert (90, 1) not in edv + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) in evn + assert (1, 2) in evn + assert (2, 3) in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + def test_len(self): + ev = self.eview(self.G) + num_ed = 9 if self.G.is_multigraph() else 8 + assert len(ev) == num_ed + + H = self.G.copy() + H.add_edge(1, 1) + assert len(H.edges(1)) == 3 + H.is_multigraph() - H.is_directed() + assert len(H.edges()) == num_ed + 1 + assert len(H.edges) == num_ed + 1 + + def test_and(self): + ev = self.eview(self.G) + some_edges = {(0, 1), (1, 0), (0, 2)} + if self.G.is_directed(): + assert some_edges & ev, {(0, 1)} + assert ev & some_edges, {(0, 1)} + else: + assert ev & some_edges == {(0, 1), (1, 0)} + assert some_edges & ev == {(0, 1), (1, 0)} + return + + def test_or(self): + ev = self.eview(self.G) + some_edges = {(0, 1), (1, 0), (0, 2)} + result1 = {(n, n + 1) for n in range(8)} + result1.update(some_edges) + result2 = {(n + 1, n) for n in range(8)} + result2.update(some_edges) + assert (ev | some_edges) in (result1, result2) + assert (some_edges | ev) in (result1, result2) + + def test_xor(self): + ev = self.eview(self.G) + some_edges = {(0, 1), (1, 0), (0, 2)} + if self.G.is_directed(): + result = {(n, n + 1) for n in range(1, 8)} + result.update({(1, 0), (0, 2)}) + assert ev ^ some_edges == result + else: + result = {(n, n + 1) for n in range(1, 8)} + result.update({(0, 2)}) + assert ev ^ some_edges == result + return + + def test_sub(self): + ev = self.eview(self.G) + some_edges = {(0, 1), (1, 0), (0, 2)} + result = {(n, n + 1) for n in range(8)} + result.remove((0, 1)) + assert ev - some_edges, result + + +class TestOutEdgeView(TestEdgeView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, nx.DiGraph()) + cls.eview = nx.reportviews.OutEdgeView + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "OutEdgeView([(0, 1), (1, 2), (2, 3), (3, 4), " + + "(4, 5), (5, 6), (6, 7), (7, 8)])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) in evn + assert (1, 2) not in evn + assert (2, 3) in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + +class TestInEdgeView(TestEdgeView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, nx.DiGraph()) + cls.eview = nx.reportviews.InEdgeView + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "InEdgeView([(0, 1), (1, 2), (2, 3), (3, 4), " + + "(4, 5), (5, 6), (6, 7), (7, 8)])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) not in evn + assert (1, 2) in evn + assert (2, 3) not in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + +class TestMultiEdgeView(TestEdgeView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, nx.MultiGraph()) + cls.G.add_edge(1, 2, key=3, foo="bar") + cls.eview = nx.reportviews.MultiEdgeView + + def modify_edge(self, G, e, **kwds): + if len(e) == 2: + e = e + (0,) + G._adj[e[0]][e[1]][e[2]].update(kwds) + + def test_str(self): + ev = self.eview(self.G) + replist = [(n, n + 1, 0) for n in range(8)] + replist.insert(2, (1, 2, 3)) + rep = str(replist) + assert str(ev) == rep + + def test_getitem(self): + G = self.G.copy() + ev = G.edges + G.edges[0, 1, 0]["foo"] = "bar" + assert ev[0, 1, 0] == {"foo": "bar"} + + # slicing + with pytest.raises(nx.NetworkXError): + G.edges[0:5] + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "MultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0), " + + "(3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])" + ) + assert repr(ev) == rep + + def test_call(self): + ev = self.eview(self.G) + assert id(ev) == id(ev(keys=True)) + assert id(ev) == id(ev(data=False, keys=True)) + assert id(ev) != id(ev(keys=False)) + assert id(ev) != id(ev(data=True)) + assert id(ev) != id(ev(nbunch=1)) + + def test_data(self): + ev = self.eview(self.G) + assert id(ev) != id(ev.data()) + assert id(ev) == id(ev.data(data=False, keys=True)) + assert id(ev) != id(ev.data(keys=False)) + assert id(ev) != id(ev.data(data=True)) + assert id(ev) != id(ev.data(nbunch=1)) + + def test_iter(self): + ev = self.eview(self.G) + for u, v, k in ev: + pass + iev = iter(ev) + assert next(iev) == (0, 1, 0) + assert iter(ev) != ev + assert iter(iev) == iev + + def test_iterkeys(self): + G = self.G + evr = self.eview(G) + ev = evr(keys=True) + for u, v, k in ev: + pass + assert k == 0 + ev = evr(keys=True, data="foo", default=1) + for u, v, k, wt in ev: + pass + assert wt == 1 + + self.modify_edge(G, (2, 3, 0), foo="bar") + ev = evr(keys=True, data=True) + for e in ev: + assert len(e) == 4 + if set(e[:2]) == {2, 3}: + assert e[2] == 0 + assert e[3] == {"foo": "bar"} + checked = True + elif set(e[:3]) == {1, 2, 3}: + assert e[2] == 3 + assert e[3] == {"foo": "bar"} + checked_multi = True + else: + assert e[2] == 0 + assert e[3] == {} + assert checked + assert checked_multi + ev = evr(keys=True, data="foo", default=1) + for e in ev: + if set(e[:2]) == {1, 2} and e[2] == 3: + assert e[3] == "bar" + if set(e[:2]) == {1, 2} and e[2] == 0: + assert e[3] == 1 + if set(e[:2]) == {2, 3}: + assert e[2] == 0 + assert e[3] == "bar" + assert len(e) == 4 + checked_wt = True + assert checked_wt + ev = evr(keys=True) + for e in ev: + assert len(e) == 3 + elist = sorted([(i, i + 1, 0) for i in range(8)] + [(1, 2, 3)]) + assert sorted(ev) == elist + # test that the keyword arguments are passed correctly + ev = evr((1, 2), "foo", keys=True, default=1) + with pytest.raises(TypeError): + evr((1, 2), "foo", True, 1) + with pytest.raises(TypeError): + evr((1, 2), "foo", True, default=1) + for e in ev: + if set(e[:2]) == {1, 2}: + assert e[2] in {0, 3} + if e[2] == 3: + assert e[3] == "bar" + else: # e[2] == 0 + assert e[3] == 1 + if G.is_directed(): + assert len(list(ev)) == 3 + else: + assert len(list(ev)) == 4 + + def test_or(self): + ev = self.eview(self.G) + some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)} + result = {(n, n + 1, 0) for n in range(8)} + result.update(some_edges) + result.update({(1, 2, 3)}) + assert ev | some_edges == result + assert some_edges | ev == result + + def test_sub(self): + ev = self.eview(self.G) + some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)} + result = {(n, n + 1, 0) for n in range(8)} + result.remove((0, 1, 0)) + result.update({(1, 2, 3)}) + assert ev - some_edges, result + assert some_edges - ev, result + + def test_xor(self): + ev = self.eview(self.G) + some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)} + if self.G.is_directed(): + result = {(n, n + 1, 0) for n in range(1, 8)} + result.update({(1, 0, 0), (0, 2, 0), (1, 2, 3)}) + assert ev ^ some_edges == result + assert some_edges ^ ev == result + else: + result = {(n, n + 1, 0) for n in range(1, 8)} + result.update({(0, 2, 0), (1, 2, 3)}) + assert ev ^ some_edges == result + assert some_edges ^ ev == result + + def test_and(self): + ev = self.eview(self.G) + some_edges = {(0, 1, 0), (1, 0, 0), (0, 2, 0)} + if self.G.is_directed(): + assert ev & some_edges == {(0, 1, 0)} + assert some_edges & ev == {(0, 1, 0)} + else: + assert ev & some_edges == {(0, 1, 0), (1, 0, 0)} + assert some_edges & ev == {(0, 1, 0), (1, 0, 0)} + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) in evn + assert (1, 2) in evn + assert (2, 3) in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + +class TestOutMultiEdgeView(TestMultiEdgeView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, nx.MultiDiGraph()) + cls.G.add_edge(1, 2, key=3, foo="bar") + cls.eview = nx.reportviews.OutMultiEdgeView + + def modify_edge(self, G, e, **kwds): + if len(e) == 2: + e = e + (0,) + G._adj[e[0]][e[1]][e[2]].update(kwds) + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "OutMultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0)," + + " (3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) in evn + assert (1, 2) not in evn + assert (2, 3) in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + +class TestInMultiEdgeView(TestMultiEdgeView): + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, nx.MultiDiGraph()) + cls.G.add_edge(1, 2, key=3, foo="bar") + cls.eview = nx.reportviews.InMultiEdgeView + + def modify_edge(self, G, e, **kwds): + if len(e) == 2: + e = e + (0,) + G._adj[e[0]][e[1]][e[2]].update(kwds) + + def test_repr(self): + ev = self.eview(self.G) + rep = ( + "InMultiEdgeView([(0, 1, 0), (1, 2, 0), (1, 2, 3), (2, 3, 0), " + + "(3, 4, 0), (4, 5, 0), (5, 6, 0), (6, 7, 0), (7, 8, 0)])" + ) + assert repr(ev) == rep + + def test_contains_with_nbunch(self): + ev = self.eview(self.G) + evn = ev(nbunch=[0, 2]) + assert (0, 1) not in evn + assert (1, 2) in evn + assert (2, 3) not in evn + assert (3, 4) not in evn + assert (4, 5) not in evn + assert (5, 6) not in evn + assert (7, 8) not in evn + assert (8, 9) not in evn + + +# Degrees +class TestDegreeView: + GRAPH = nx.Graph + dview = nx.reportviews.DegreeView + + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(6, cls.GRAPH()) + cls.G.add_edge(1, 3, foo=2) + cls.G.add_edge(1, 3, foo=3) + + def test_pickle(self): + import pickle + + deg = self.G.degree + pdeg = pickle.loads(pickle.dumps(deg, -1)) + assert dict(deg) == dict(pdeg) + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 1), (1, 3), (2, 2), (3, 3), (4, 2), (5, 1)]) + assert str(dv) == rep + dv = self.G.degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.dview(self.G) + rep = "DegreeView({0: 1, 1: 3, 2: 2, 3: 3, 4: 2, 5: 1})" + assert repr(dv) == rep + + def test_iter(self): + dv = self.dview(self.G) + for n, d in dv: + pass + idv = iter(dv) + assert iter(dv) != dv + assert iter(idv) == idv + assert next(idv) == (0, dv[0]) + assert next(idv) == (1, dv[1]) + # weighted + dv = self.dview(self.G, weight="foo") + for n, d in dv: + pass + idv = iter(dv) + assert iter(dv) != dv + assert iter(idv) == idv + assert next(idv) == (0, dv[0]) + assert next(idv) == (1, dv[1]) + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 1 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 2), (3, 3)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 1 + assert dv[1] == 3 + assert dv[2] == 2 + assert dv[3] == 3 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 1 + assert dv[1] == 5 + assert dv[2] == 2 + assert dv[3] == 5 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 1 + dvw = dv(1, weight="foo") + assert dvw == 5 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 2), (3, 5)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 1 + assert dvd[1] == 5 + assert dvd[2] == 2 + assert dvd[3] == 5 + + def test_len(self): + dv = self.dview(self.G) + assert len(dv) == 6 + + +class TestDiDegreeView(TestDegreeView): + GRAPH = nx.DiGraph + dview = nx.reportviews.DiDegreeView + + def test_repr(self): + dv = self.G.degree() + rep = "DiDegreeView({0: 1, 1: 3, 2: 2, 3: 3, 4: 2, 5: 1})" + assert repr(dv) == rep + + +class TestOutDegreeView(TestDegreeView): + GRAPH = nx.DiGraph + dview = nx.reportviews.OutDegreeView + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 1), (1, 2), (2, 1), (3, 1), (4, 1), (5, 0)]) + assert str(dv) == rep + dv = self.G.out_degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.G.out_degree() + rep = "OutDegreeView({0: 1, 1: 2, 2: 1, 3: 1, 4: 1, 5: 0})" + assert repr(dv) == rep + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 1 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 1), (3, 1)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 1 + assert dv[1] == 2 + assert dv[2] == 1 + assert dv[3] == 1 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 1 + assert dv[1] == 4 + assert dv[2] == 1 + assert dv[3] == 1 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 1 + dvw = dv(1, weight="foo") + assert dvw == 4 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 1), (3, 1)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 1 + assert dvd[1] == 4 + assert dvd[2] == 1 + assert dvd[3] == 1 + + +class TestInDegreeView(TestDegreeView): + GRAPH = nx.DiGraph + dview = nx.reportviews.InDegreeView + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 1)]) + assert str(dv) == rep + dv = self.G.in_degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.G.in_degree() + rep = "InDegreeView({0: 0, 1: 1, 2: 1, 3: 2, 4: 1, 5: 1})" + assert repr(dv) == rep + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 0 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 1), (3, 2)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 0 + assert dv[1] == 1 + assert dv[2] == 1 + assert dv[3] == 2 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 0 + assert dv[1] == 1 + assert dv[2] == 1 + assert dv[3] == 4 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 0 + dvw = dv(1, weight="foo") + assert dvw == 1 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 1), (3, 4)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 0 + assert dvd[1] == 1 + assert dvd[2] == 1 + assert dvd[3] == 4 + + +class TestMultiDegreeView(TestDegreeView): + GRAPH = nx.MultiGraph + dview = nx.reportviews.MultiDegreeView + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 1), (1, 4), (2, 2), (3, 4), (4, 2), (5, 1)]) + assert str(dv) == rep + dv = self.G.degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.G.degree() + rep = "MultiDegreeView({0: 1, 1: 4, 2: 2, 3: 4, 4: 2, 5: 1})" + assert repr(dv) == rep + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 1 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 2), (3, 4)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 1 + assert dv[1] == 4 + assert dv[2] == 2 + assert dv[3] == 4 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 1 + assert dv[1] == 7 + assert dv[2] == 2 + assert dv[3] == 7 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 1 + dvw = dv(1, weight="foo") + assert dvw == 7 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 2), (3, 7)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 1 + assert dvd[1] == 7 + assert dvd[2] == 2 + assert dvd[3] == 7 + + +class TestDiMultiDegreeView(TestMultiDegreeView): + GRAPH = nx.MultiDiGraph + dview = nx.reportviews.DiMultiDegreeView + + def test_repr(self): + dv = self.G.degree() + rep = "DiMultiDegreeView({0: 1, 1: 4, 2: 2, 3: 4, 4: 2, 5: 1})" + assert repr(dv) == rep + + +class TestOutMultiDegreeView(TestDegreeView): + GRAPH = nx.MultiDiGraph + dview = nx.reportviews.OutMultiDegreeView + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 1), (1, 3), (2, 1), (3, 1), (4, 1), (5, 0)]) + assert str(dv) == rep + dv = self.G.out_degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.G.out_degree() + rep = "OutMultiDegreeView({0: 1, 1: 3, 2: 1, 3: 1, 4: 1, 5: 0})" + assert repr(dv) == rep + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 1 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 1), (3, 1)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 1 + assert dv[1] == 3 + assert dv[2] == 1 + assert dv[3] == 1 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 1 + assert dv[1] == 6 + assert dv[2] == 1 + assert dv[3] == 1 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 1 + dvw = dv(1, weight="foo") + assert dvw == 6 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 1), (3, 1)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 1 + assert dvd[1] == 6 + assert dvd[2] == 1 + assert dvd[3] == 1 + + +class TestInMultiDegreeView(TestDegreeView): + GRAPH = nx.MultiDiGraph + dview = nx.reportviews.InMultiDegreeView + + def test_str(self): + dv = self.dview(self.G) + rep = str([(0, 0), (1, 1), (2, 1), (3, 3), (4, 1), (5, 1)]) + assert str(dv) == rep + dv = self.G.in_degree() + assert str(dv) == rep + + def test_repr(self): + dv = self.G.in_degree() + rep = "InMultiDegreeView({0: 0, 1: 1, 2: 1, 3: 3, 4: 1, 5: 1})" + assert repr(dv) == rep + + def test_nbunch(self): + dv = self.dview(self.G) + dvn = dv(0) + assert dvn == 0 + dvn = dv([2, 3]) + assert sorted(dvn) == [(2, 1), (3, 3)] + + def test_getitem(self): + dv = self.dview(self.G) + assert dv[0] == 0 + assert dv[1] == 1 + assert dv[2] == 1 + assert dv[3] == 3 + dv = self.dview(self.G, weight="foo") + assert dv[0] == 0 + assert dv[1] == 1 + assert dv[2] == 1 + assert dv[3] == 6 + + def test_weight(self): + dv = self.dview(self.G) + dvw = dv(0, weight="foo") + assert dvw == 0 + dvw = dv(1, weight="foo") + assert dvw == 1 + dvw = dv([2, 3], weight="foo") + assert sorted(dvw) == [(2, 1), (3, 6)] + dvd = dict(dv(weight="foo")) + assert dvd[0] == 0 + assert dvd[1] == 1 + assert dvd[2] == 1 + assert dvd[3] == 6 + + +@pytest.mark.parametrize( + ("reportview", "err_msg_terms"), + ( + (rv.NodeView, "list(G.nodes"), + (rv.NodeDataView, "list(G.nodes.data"), + (rv.EdgeView, "list(G.edges"), + # Directed EdgeViews + (rv.InEdgeView, "list(G.in_edges"), + (rv.OutEdgeView, "list(G.edges"), + # Multi EdgeViews + (rv.MultiEdgeView, "list(G.edges"), + (rv.InMultiEdgeView, "list(G.in_edges"), + (rv.OutMultiEdgeView, "list(G.edges"), + ), +) +def test_slicing_reportviews(reportview, err_msg_terms): + G = nx.complete_graph(3) + view = reportview(G) + with pytest.raises(nx.NetworkXError) as exc: + view[0:2] + errmsg = str(exc.value) + assert type(view).__name__ in errmsg + assert err_msg_terms in errmsg + + +@pytest.mark.parametrize( + "graph", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +) +def test_cache_dict_get_set_state(graph): + G = nx.path_graph(5, graph()) + G.nodes, G.edges, G.adj, G.degree + if G.is_directed(): + G.pred, G.succ, G.in_edges, G.out_edges, G.in_degree, G.out_degree + cached_dict = G.__dict__ + assert "nodes" in cached_dict + assert "edges" in cached_dict + assert "adj" in cached_dict + assert "degree" in cached_dict + if G.is_directed(): + assert "pred" in cached_dict + assert "succ" in cached_dict + assert "in_edges" in cached_dict + assert "out_edges" in cached_dict + assert "in_degree" in cached_dict + assert "out_degree" in cached_dict + + # Raises error if the cached properties and views do not work + pickle.loads(pickle.dumps(G, -1)) + deepcopy(G) + + +def test_edge_views_inherit_from_EdgeViewABC(): + all_edge_view_classes = (v for v in dir(nx.reportviews) if "Edge" in v) + for eview_class in all_edge_view_classes: + assert issubclass( + getattr(nx.reportviews, eview_class), nx.reportviews.EdgeViewABC + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_special.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_special.py new file mode 100644 index 0000000000000000000000000000000000000000..33b61a4b64975bfd0044215825ae5510bec68073 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_special.py @@ -0,0 +1,131 @@ +import networkx as nx + +from .test_digraph import BaseDiGraphTester +from .test_digraph import TestDiGraph as _TestDiGraph +from .test_graph import BaseGraphTester +from .test_graph import TestGraph as _TestGraph +from .test_multidigraph import TestMultiDiGraph as _TestMultiDiGraph +from .test_multigraph import TestMultiGraph as _TestMultiGraph + + +def test_factories(): + class mydict1(dict): + pass + + class mydict2(dict): + pass + + class mydict3(dict): + pass + + class mydict4(dict): + pass + + class mydict5(dict): + pass + + for Graph in (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph): + + class MyGraph(Graph): + node_dict_factory = mydict1 + adjlist_outer_dict_factory = mydict2 + adjlist_inner_dict_factory = mydict3 + edge_key_dict_factory = mydict4 + edge_attr_dict_factory = mydict5 + + G = MyGraph() + assert isinstance(G._node, mydict1) + assert isinstance(G._adj, mydict2) + G.add_node(1) + assert isinstance(G._adj[1], mydict3) + if G.is_directed(): + assert isinstance(G._pred, mydict2) + assert isinstance(G._succ, mydict2) + assert isinstance(G._pred[1], mydict3) + G.add_edge(1, 2) + if G.is_multigraph(): + assert isinstance(G._adj[1][2], mydict4) + assert isinstance(G._adj[1][2][0], mydict5) + else: + assert isinstance(G._adj[1][2], mydict5) + + +class TestSpecialGraph(_TestGraph): + def setup_method(self): + _TestGraph.setup_method(self) + self.Graph = nx.Graph + + +class TestThinGraph(BaseGraphTester): + def setup_method(self): + all_edge_dict = {"weight": 1} + + class MyGraph(nx.Graph): + def edge_attr_dict_factory(self): + return all_edge_dict + + self.Graph = MyGraph + # build dict-of-dict-of-dict K3 + ed1, ed2, ed3 = (all_edge_dict, all_edge_dict, all_edge_dict) + self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed1, 2: ed3}, 2: {0: ed2, 1: ed3}} + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._adj = self.k3adj + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + +class TestSpecialDiGraph(_TestDiGraph): + def setup_method(self): + _TestDiGraph.setup_method(self) + self.Graph = nx.DiGraph + + +class TestThinDiGraph(BaseDiGraphTester): + def setup_method(self): + all_edge_dict = {"weight": 1} + + class MyGraph(nx.DiGraph): + def edge_attr_dict_factory(self): + return all_edge_dict + + self.Graph = MyGraph + # build dict-of-dict-of-dict K3 + ed1, ed2, ed3 = (all_edge_dict, all_edge_dict, all_edge_dict) + ed4, ed5, ed6 = (all_edge_dict, all_edge_dict, all_edge_dict) + self.k3adj = {0: {1: ed1, 2: ed2}, 1: {0: ed3, 2: ed4}, 2: {0: ed5, 1: ed6}} + self.k3edges = [(0, 1), (0, 2), (1, 2)] + self.k3nodes = [0, 1, 2] + self.K3 = self.Graph() + self.K3._succ = self.k3adj + # K3._adj is synced with K3._succ + self.K3._pred = {0: {1: ed3, 2: ed5}, 1: {0: ed1, 2: ed6}, 2: {0: ed2, 1: ed4}} + self.K3._node = {} + self.K3._node[0] = {} + self.K3._node[1] = {} + self.K3._node[2] = {} + + ed1, ed2 = (all_edge_dict, all_edge_dict) + self.P3 = self.Graph() + self.P3._succ = {0: {1: ed1}, 1: {2: ed2}, 2: {}} + # P3._adj is synced with P3._succ + self.P3._pred = {0: {}, 1: {0: ed1}, 2: {1: ed2}} + self.P3._node = {} + self.P3._node[0] = {} + self.P3._node[1] = {} + self.P3._node[2] = {} + + +class TestSpecialMultiGraph(_TestMultiGraph): + def setup_method(self): + _TestMultiGraph.setup_method(self) + self.Graph = nx.MultiGraph + + +class TestSpecialMultiDiGraph(_TestMultiDiGraph): + def setup_method(self): + _TestMultiDiGraph.setup_method(self) + self.Graph = nx.MultiDiGraph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py new file mode 100644 index 0000000000000000000000000000000000000000..66d570cedcecf4e8bb2f485b0ee892ddae2140dd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/classes/tests/test_subgraphviews.py @@ -0,0 +1,371 @@ +import pytest + +import networkx as nx +from networkx.utils import edges_equal + + +class TestSubGraphView: + gview = staticmethod(nx.subgraph_view) + graph = nx.Graph + hide_edges_filter = staticmethod(nx.filters.hide_edges) + show_edges_filter = staticmethod(nx.filters.show_edges) + + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=cls.graph()) + cls.hide_edges_w_hide_nodes = {(3, 4), (4, 5), (5, 6)} + + def test_hidden_nodes(self): + hide_nodes = [4, 5, 111] + nodes_gone = nx.filters.hide_nodes(hide_nodes) + gview = self.gview + G = gview(self.G, filter_node=nodes_gone) + assert self.G.nodes - G.nodes == {4, 5} + assert self.G.edges - G.edges == self.hide_edges_w_hide_nodes + if G.is_directed(): + assert list(G[3]) == [] + assert list(G[2]) == [3] + else: + assert list(G[3]) == [2] + assert set(G[2]) == {1, 3} + pytest.raises(KeyError, G.__getitem__, 4) + pytest.raises(KeyError, G.__getitem__, 112) + pytest.raises(KeyError, G.__getitem__, 111) + assert G.degree(3) == (3 if G.is_multigraph() else 1) + assert G.size() == (7 if G.is_multigraph() else 5) + + def test_hidden_edges(self): + hide_edges = [(2, 3), (8, 7), (222, 223)] + edges_gone = self.hide_edges_filter(hide_edges) + gview = self.gview + G = gview(self.G, filter_edge=edges_gone) + assert self.G.nodes == G.nodes + if G.is_directed(): + assert self.G.edges - G.edges == {(2, 3)} + assert list(G[2]) == [] + assert list(G.pred[3]) == [] + assert list(G.pred[2]) == [1] + assert G.size() == 7 + else: + assert self.G.edges - G.edges == {(2, 3), (7, 8)} + assert list(G[2]) == [1] + assert G.size() == 6 + assert list(G[3]) == [4] + pytest.raises(KeyError, G.__getitem__, 221) + pytest.raises(KeyError, G.__getitem__, 222) + assert G.degree(3) == 1 + + def test_shown_node(self): + induced_subgraph = nx.filters.show_nodes([2, 3, 111]) + gview = self.gview + G = gview(self.G, filter_node=induced_subgraph) + assert set(G.nodes) == {2, 3} + if G.is_directed(): + assert list(G[3]) == [] + else: + assert list(G[3]) == [2] + assert list(G[2]) == [3] + pytest.raises(KeyError, G.__getitem__, 4) + pytest.raises(KeyError, G.__getitem__, 112) + pytest.raises(KeyError, G.__getitem__, 111) + assert G.degree(3) == (3 if G.is_multigraph() else 1) + assert G.size() == (3 if G.is_multigraph() else 1) + + def test_shown_edges(self): + show_edges = [(2, 3), (8, 7), (222, 223)] + edge_subgraph = self.show_edges_filter(show_edges) + G = self.gview(self.G, filter_edge=edge_subgraph) + assert self.G.nodes == G.nodes + if G.is_directed(): + assert G.edges == {(2, 3)} + assert list(G[3]) == [] + assert list(G[2]) == [3] + assert list(G.pred[3]) == [2] + assert list(G.pred[2]) == [] + assert G.size() == 1 + else: + assert G.edges == {(2, 3), (7, 8)} + assert list(G[3]) == [2] + assert list(G[2]) == [3] + assert G.size() == 2 + pytest.raises(KeyError, G.__getitem__, 221) + pytest.raises(KeyError, G.__getitem__, 222) + assert G.degree(3) == 1 + + +class TestSubDiGraphView(TestSubGraphView): + gview = staticmethod(nx.subgraph_view) + graph = nx.DiGraph + hide_edges_filter = staticmethod(nx.filters.hide_diedges) + show_edges_filter = staticmethod(nx.filters.show_diedges) + hide_edges = [(2, 3), (8, 7), (222, 223)] + excluded = {(2, 3), (3, 4), (4, 5), (5, 6)} + + def test_inoutedges(self): + edges_gone = self.hide_edges_filter(self.hide_edges) + hide_nodes = [4, 5, 111] + nodes_gone = nx.filters.hide_nodes(hide_nodes) + G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone) + + assert self.G.in_edges - G.in_edges == self.excluded + assert self.G.out_edges - G.out_edges == self.excluded + + def test_pred(self): + edges_gone = self.hide_edges_filter(self.hide_edges) + hide_nodes = [4, 5, 111] + nodes_gone = nx.filters.hide_nodes(hide_nodes) + G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone) + + assert list(G.pred[2]) == [1] + assert list(G.pred[6]) == [] + + def test_inout_degree(self): + edges_gone = self.hide_edges_filter(self.hide_edges) + hide_nodes = [4, 5, 111] + nodes_gone = nx.filters.hide_nodes(hide_nodes) + G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone) + + assert G.degree(2) == 1 + assert G.out_degree(2) == 0 + assert G.in_degree(2) == 1 + assert G.size() == 4 + + +# multigraph +class TestMultiGraphView(TestSubGraphView): + gview = staticmethod(nx.subgraph_view) + graph = nx.MultiGraph + hide_edges_filter = staticmethod(nx.filters.hide_multiedges) + show_edges_filter = staticmethod(nx.filters.show_multiedges) + + @classmethod + def setup_class(cls): + cls.G = nx.path_graph(9, create_using=cls.graph()) + multiedges = {(2, 3, 4), (2, 3, 5)} + cls.G.add_edges_from(multiedges) + cls.hide_edges_w_hide_nodes = {(3, 4, 0), (4, 5, 0), (5, 6, 0)} + + def test_hidden_edges(self): + hide_edges = [(2, 3, 4), (2, 3, 3), (8, 7, 0), (222, 223, 0)] + edges_gone = self.hide_edges_filter(hide_edges) + G = self.gview(self.G, filter_edge=edges_gone) + assert self.G.nodes == G.nodes + if G.is_directed(): + assert self.G.edges - G.edges == {(2, 3, 4)} + assert list(G[3]) == [4] + assert list(G[2]) == [3] + assert list(G.pred[3]) == [2] # only one 2 but two edges + assert list(G.pred[2]) == [1] + assert G.size() == 9 + else: + assert self.G.edges - G.edges == {(2, 3, 4), (7, 8, 0)} + assert list(G[3]) == [2, 4] + assert list(G[2]) == [1, 3] + assert G.size() == 8 + assert G.degree(3) == 3 + pytest.raises(KeyError, G.__getitem__, 221) + pytest.raises(KeyError, G.__getitem__, 222) + + def test_shown_edges(self): + show_edges = [(2, 3, 4), (2, 3, 3), (8, 7, 0), (222, 223, 0)] + edge_subgraph = self.show_edges_filter(show_edges) + G = self.gview(self.G, filter_edge=edge_subgraph) + assert self.G.nodes == G.nodes + if G.is_directed(): + assert G.edges == {(2, 3, 4)} + assert list(G[3]) == [] + assert list(G.pred[3]) == [2] + assert list(G.pred[2]) == [] + assert G.size() == 1 + else: + assert G.edges == {(2, 3, 4), (7, 8, 0)} + assert G.size() == 2 + assert list(G[3]) == [2] + assert G.degree(3) == 1 + assert list(G[2]) == [3] + pytest.raises(KeyError, G.__getitem__, 221) + pytest.raises(KeyError, G.__getitem__, 222) + + +# multidigraph +class TestMultiDiGraphView(TestMultiGraphView, TestSubDiGraphView): + gview = staticmethod(nx.subgraph_view) + graph = nx.MultiDiGraph + hide_edges_filter = staticmethod(nx.filters.hide_multidiedges) + show_edges_filter = staticmethod(nx.filters.show_multidiedges) + hide_edges = [(2, 3, 0), (8, 7, 0), (222, 223, 0)] + excluded = {(2, 3, 0), (3, 4, 0), (4, 5, 0), (5, 6, 0)} + + def test_inout_degree(self): + edges_gone = self.hide_edges_filter(self.hide_edges) + hide_nodes = [4, 5, 111] + nodes_gone = nx.filters.hide_nodes(hide_nodes) + G = self.gview(self.G, filter_node=nodes_gone, filter_edge=edges_gone) + + assert G.degree(2) == 3 + assert G.out_degree(2) == 2 + assert G.in_degree(2) == 1 + assert G.size() == 6 + + +# induced_subgraph +class TestInducedSubGraph: + @classmethod + def setup_class(cls): + cls.K3 = G = nx.complete_graph(3) + G.graph["foo"] = [] + G.nodes[0]["foo"] = [] + G.remove_edge(1, 2) + ll = [] + G.add_edge(1, 2, foo=ll) + G.add_edge(2, 1, foo=ll) + + def test_full_graph(self): + G = self.K3 + H = nx.induced_subgraph(G, [0, 1, 2, 5]) + assert H.name == G.name + self.graphs_equal(H, G) + self.same_attrdict(H, G) + + def test_partial_subgraph(self): + G = self.K3 + H = nx.induced_subgraph(G, 0) + assert dict(H.adj) == {0: {}} + assert dict(G.adj) != {0: {}} + + H = nx.induced_subgraph(G, [0, 1]) + assert dict(H.adj) == {0: {1: {}}, 1: {0: {}}} + + def same_attrdict(self, H, G): + old_foo = H[1][2]["foo"] + H.edges[1, 2]["foo"] = "baz" + assert G.edges == H.edges + H.edges[1, 2]["foo"] = old_foo + assert G.edges == H.edges + old_foo = H.nodes[0]["foo"] + H.nodes[0]["foo"] = "baz" + assert G.nodes == H.nodes + H.nodes[0]["foo"] = old_foo + assert G.nodes == H.nodes + + def graphs_equal(self, H, G): + assert G._adj == H._adj + assert G._node == H._node + assert G.graph == H.graph + assert G.name == H.name + if not G.is_directed() and not H.is_directed(): + assert H._adj[1][2] is H._adj[2][1] + assert G._adj[1][2] is G._adj[2][1] + else: # at least one is directed + if not G.is_directed(): + G._pred = G._adj + G._succ = G._adj + if not H.is_directed(): + H._pred = H._adj + H._succ = H._adj + assert G._pred == H._pred + assert G._succ == H._succ + assert H._succ[1][2] is H._pred[2][1] + assert G._succ[1][2] is G._pred[2][1] + + +# edge_subgraph +class TestEdgeSubGraph: + @classmethod + def setup_class(cls): + # Create a path graph on five nodes. + cls.G = G = nx.path_graph(5) + # Add some node, edge, and graph attributes. + for i in range(5): + G.nodes[i]["name"] = f"node{i}" + G.edges[0, 1]["name"] = "edge01" + G.edges[3, 4]["name"] = "edge34" + G.graph["name"] = "graph" + # Get the subgraph induced by the first and last edges. + cls.H = nx.edge_subgraph(G, [(0, 1), (3, 4)]) + + def test_correct_nodes(self): + """Tests that the subgraph has the correct nodes.""" + assert [(0, "node0"), (1, "node1"), (3, "node3"), (4, "node4")] == sorted( + self.H.nodes.data("name") + ) + + def test_correct_edges(self): + """Tests that the subgraph has the correct edges.""" + assert edges_equal( + [(0, 1, "edge01"), (3, 4, "edge34")], self.H.edges.data("name") + ) + + def test_add_node(self): + """Tests that adding a node to the original graph does not + affect the nodes of the subgraph. + + """ + self.G.add_node(5) + assert [0, 1, 3, 4] == sorted(self.H.nodes) + self.G.remove_node(5) + + def test_remove_node(self): + """Tests that removing a node in the original graph + removes the nodes of the subgraph. + + """ + self.G.remove_node(0) + assert [1, 3, 4] == sorted(self.H.nodes) + self.G.add_node(0, name="node0") + self.G.add_edge(0, 1, name="edge01") + + def test_node_attr_dict(self): + """Tests that the node attribute dictionary of the two graphs is + the same object. + + """ + for v in self.H: + assert self.G.nodes[v] == self.H.nodes[v] + # Making a change to G should make a change in H and vice versa. + self.G.nodes[0]["name"] = "foo" + assert self.G.nodes[0] == self.H.nodes[0] + self.H.nodes[1]["name"] = "bar" + assert self.G.nodes[1] == self.H.nodes[1] + # Revert the change, so tests pass with pytest-randomly + self.G.nodes[0]["name"] = "node0" + self.H.nodes[1]["name"] = "node1" + + def test_edge_attr_dict(self): + """Tests that the edge attribute dictionary of the two graphs is + the same object. + + """ + for u, v in self.H.edges(): + assert self.G.edges[u, v] == self.H.edges[u, v] + # Making a change to G should make a change in H and vice versa. + self.G.edges[0, 1]["name"] = "foo" + assert self.G.edges[0, 1]["name"] == self.H.edges[0, 1]["name"] + self.H.edges[3, 4]["name"] = "bar" + assert self.G.edges[3, 4]["name"] == self.H.edges[3, 4]["name"] + # Revert the change, so tests pass with pytest-randomly + self.G.edges[0, 1]["name"] = "edge01" + self.H.edges[3, 4]["name"] = "edge34" + + def test_graph_attr_dict(self): + """Tests that the graph attribute dictionary of the two graphs + is the same object. + + """ + assert self.G.graph is self.H.graph + + def test_readonly(self): + """Tests that the subgraph cannot change the graph structure""" + pytest.raises(nx.NetworkXError, self.H.add_node, 5) + pytest.raises(nx.NetworkXError, self.H.remove_node, 0) + pytest.raises(nx.NetworkXError, self.H.add_edge, 5, 6) + pytest.raises(nx.NetworkXError, self.H.remove_edge, 0, 1) + + @pytest.mark.parametrize("multigraph", (nx.MultiGraph, nx.MultiDiGraph)) + def test_multigraph_filtered_edges(self, multigraph): + """Check edge visibility in FilterMultiInner on edge_subgraph's of + multigraphs. See gh-7724.""" + G = multigraph([("a", "b"), ("a", "c"), ("c", "b")]) + H = nx.edge_subgraph(G, [("a", "b", 0), ("c", "b", 0)]) + assert "c" not in H["a"] + assert not H.has_edge("a", "c") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0f53309d4da23a445bcce8cb7570a6de364452b5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__init__.py @@ -0,0 +1,7 @@ +# graph drawing and interface to graphviz + +from .layout import * +from .nx_latex import * +from .nx_pylab import * +from . import nx_agraph +from . import nx_pydot diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14bbf11409d795fe99a4dd2802c0f26b975a228e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/layout.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/layout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd0b7e5cbfc676c96ec2a116f411a6537dd72edd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/layout.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_agraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_agraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f66aa82ae13205b4083570e187df1a059d85660 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_agraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_latex.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_latex.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c10d3fcb82c9bd4d0e6b8df4f2aacfcaefa372e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_latex.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_pydot.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_pydot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbbb065c660f62638af02cbcc4740507caad8a23 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/__pycache__/nx_pydot.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/layout.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/layout.py new file mode 100644 index 0000000000000000000000000000000000000000..b46d9f77b81f27acd8f4eaef2aa5c5a3e87872ed --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/layout.py @@ -0,0 +1,2036 @@ +""" +****** +Layout +****** + +Node positioning algorithms for graph drawing. + +For `random_layout()` the possible resulting shape +is a square of side [0, scale] (default: [0, 1]) +Changing `center` shifts the layout by that amount. + +For the other layout routines, the extent is +[center - scale, center + scale] (default: [-1, 1]). + +Warning: Most layout routines have only been tested in 2-dimensions. + +""" + +import networkx as nx +from networkx.utils import np_random_state + +__all__ = [ + "bipartite_layout", + "circular_layout", + "forceatlas2_layout", + "kamada_kawai_layout", + "random_layout", + "rescale_layout", + "rescale_layout_dict", + "shell_layout", + "spring_layout", + "spectral_layout", + "planar_layout", + "fruchterman_reingold_layout", + "spiral_layout", + "multipartite_layout", + "bfs_layout", + "arf_layout", +] + + +def _process_params(G, center, dim): + # Some boilerplate code. + import numpy as np + + if not isinstance(G, nx.Graph): + empty_graph = nx.Graph() + empty_graph.add_nodes_from(G) + G = empty_graph + + if center is None: + center = np.zeros(dim) + else: + center = np.asarray(center) + + if len(center) != dim: + msg = "length of center coordinates must match dimension of layout" + raise ValueError(msg) + + return G, center + + +@np_random_state(3) +def random_layout(G, center=None, dim=2, seed=None, store_pos_as=None): + """Position nodes uniformly at random in the unit square. + + For every node, a position is generated by choosing each of dim + coordinates uniformly at random on the interval [0.0, 1.0). + + NumPy (http://scipy.org) is required for this function. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout. + + seed : int, RandomState instance or None optional (default=None) + Set the random state for deterministic node layouts. + If int, `seed` is the seed used by the random number generator, + if numpy.random.RandomState instance, `seed` is the random + number generator, + if None, the random number generator is the RandomState instance used + by numpy.random. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.lollipop_graph(4, 3) + >>> pos = nx.random_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.random_layout(G, seed=42, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([0.37454012, 0.9507143 ], dtype=float32), + 1: array([0.7319939, 0.5986585], dtype=float32), + 2: array([0.15601864, 0.15599452], dtype=float32), + 3: array([0.05808361, 0.8661761 ], dtype=float32), + 4: array([0.601115 , 0.7080726], dtype=float32), + 5: array([0.02058449, 0.96990985], dtype=float32), + 6: array([0.83244264, 0.21233912], dtype=float32)} + """ + import numpy as np + + G, center = _process_params(G, center, dim) + pos = seed.rand(len(G), dim) + center + pos = pos.astype(np.float32) + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + return pos + + +def circular_layout(G, scale=1, center=None, dim=2, store_pos_as=None): + # dim=2 only + """Position nodes on a circle. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout. + If dim>2, the remaining dimensions are set to zero + in the returned positions. + If dim<2, a ValueError is raised. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Raises + ------ + ValueError + If dim < 2 + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.circular_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.circular_layout(G, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([9.99999986e-01, 2.18556937e-08]), + 1: array([-3.57647606e-08, 1.00000000e+00]), + 2: array([-9.9999997e-01, -6.5567081e-08]), + 3: array([ 1.98715071e-08, -9.99999956e-01])} + + + Notes + ----- + This algorithm currently only works in two dimensions and does not + try to minimize edge crossings. + + """ + import numpy as np + + if dim < 2: + raise ValueError("cannot handle dimensions < 2") + + G, center = _process_params(G, center, dim) + + paddims = max(0, (dim - 2)) + + if len(G) == 0: + pos = {} + elif len(G) == 1: + pos = {nx.utils.arbitrary_element(G): center} + else: + # Discard the extra angle since it matches 0 radians. + theta = np.linspace(0, 1, len(G) + 1)[:-1] * 2 * np.pi + theta = theta.astype(np.float32) + pos = np.column_stack( + [np.cos(theta), np.sin(theta), np.zeros((len(G), paddims))] + ) + pos = rescale_layout(pos, scale=scale) + center + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +def shell_layout( + G, nlist=None, rotate=None, scale=1, center=None, dim=2, store_pos_as=None +): + """Position nodes in concentric circles. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + nlist : list of lists + List of node lists for each shell. + + rotate : angle in radians (default=pi/len(nlist)) + Angle by which to rotate the starting position of each shell + relative to the starting position of the previous shell. + To recreate behavior before v2.5 use rotate=0. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout, currently only dim=2 is supported. + Other dimension values result in a ValueError. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Raises + ------ + ValueError + If dim != 2 + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> shells = [[0], [1, 2, 3]] + >>> pos = nx.shell_layout(G, shells) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.shell_layout(G, shells, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([0., 0.]), + 1: array([-5.00000000e-01, -4.37113883e-08]), + 2: array([ 0.24999996, -0.43301272]), + 3: array([0.24999981, 0.43301281])} + + Notes + ----- + This algorithm currently only works in two dimensions and does not + try to minimize edge crossings. + + """ + import numpy as np + + if dim != 2: + raise ValueError("can only handle 2 dimensions") + + G, center = _process_params(G, center, dim) + + if len(G) == 0: + return {} + if len(G) == 1: + return {nx.utils.arbitrary_element(G): center} + + if nlist is None: + # draw the whole graph in one shell + nlist = [list(G)] + + radius_bump = scale / len(nlist) + + if len(nlist[0]) == 1: + # single node at center + radius = 0.0 + else: + # else start at r=1 + radius = radius_bump + + if rotate is None: + rotate = np.pi / len(nlist) + first_theta = rotate + npos = {} + for nodes in nlist: + # Discard the last angle (endpoint=False) since 2*pi matches 0 radians + theta = ( + np.linspace(0, 2 * np.pi, len(nodes), endpoint=False, dtype=np.float32) + + first_theta + ) + pos = radius * np.column_stack([np.cos(theta), np.sin(theta)]) + center + npos.update(zip(nodes, pos)) + radius += radius_bump + first_theta += rotate + + if store_pos_as is not None: + nx.set_node_attributes(G, npos, store_pos_as) + return npos + + +def bipartite_layout( + G, + nodes=None, + align="vertical", + scale=1, + center=None, + aspect_ratio=4 / 3, + store_pos_as=None, +): + """Position nodes in two straight lines. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + nodes : collection of nodes + Nodes in one node set of the graph. This set will be placed on + left or top. If `None` (the default), a node set is chosen arbitrarily + if the graph if bipartite. + + align : string (default='vertical') + The alignment of nodes. Vertical or horizontal. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + aspect_ratio : number (default=4/3): + The ratio of the width to the height of the layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node. + + Raises + ------ + NetworkXError + If ``nodes=None`` and `G` is not bipartite. + + Examples + -------- + >>> G = nx.complete_bipartite_graph(3, 3) + >>> pos = nx.bipartite_layout(G) + + The ordering of the layout (i.e. which nodes appear on the left/top) can + be specified with the `nodes` parameter: + + >>> top, bottom = nx.bipartite.sets(G) + >>> pos = nx.bipartite_layout(G, nodes=bottom) # "bottom" set appears on the left + + `store_pos_as` can be used to store the node positions for the computed layout + directly on the nodes: + + >>> _ = nx.bipartite_layout(G, nodes=bottom, store_pos_as="pos") + >>> from pprint import pprint + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([ 1. , -0.75]), + 1: array([1., 0.]), + 2: array([1. , 0.75]), + 3: array([-1. , -0.75]), + 4: array([-1., 0.]), + 5: array([-1. , 0.75])} + + + The ``bipartite_layout`` function can be used with non-bipartite graphs + by explicitly specifying how the layout should be partitioned with `nodes`: + + >>> G = nx.complete_graph(5) # Non-bipartite + >>> pos = nx.bipartite_layout(G, nodes={0, 1, 2}) + + Notes + ----- + This algorithm currently only works in two dimensions and does not + try to minimize edge crossings. + + """ + + import numpy as np + + if align not in ("vertical", "horizontal"): + msg = "align must be either vertical or horizontal." + raise ValueError(msg) + + G, center = _process_params(G, center=center, dim=2) + if len(G) == 0: + return {} + + height = 1 + width = aspect_ratio * height + offset = (width / 2, height / 2) + + if nodes is None: + top, bottom = nx.bipartite.sets(G) + nodes = list(G) + else: + top = set(nodes) + bottom = set(G) - top + # Preserves backward-compatible node ordering in returned pos dict + nodes = list(top) + list(bottom) + + left_xs = np.repeat(0, len(top)) + right_xs = np.repeat(width, len(bottom)) + left_ys = np.linspace(0, height, len(top)) + right_ys = np.linspace(0, height, len(bottom)) + + top_pos = np.column_stack([left_xs, left_ys]) - offset + bottom_pos = np.column_stack([right_xs, right_ys]) - offset + + pos = np.concatenate([top_pos, bottom_pos]) + pos = rescale_layout(pos, scale=scale) + center + if align == "horizontal": + pos = pos[:, ::-1] # swap x and y coords + pos = dict(zip(nodes, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +@np_random_state("seed") +def spring_layout( + G, + k=None, + pos=None, + fixed=None, + iterations=50, + threshold=1e-4, + weight="weight", + scale=1, + center=None, + dim=2, + seed=None, + store_pos_as=None, + *, + method="auto", + gravity=1.0, +): + """Position nodes using Fruchterman-Reingold force-directed algorithm. + + The algorithm simulates a force-directed representation of the network + treating edges as springs holding nodes close, while treating nodes + as repelling objects, sometimes called an anti-gravity force. + Simulation continues until the positions are close to an equilibrium. + + There are some hard-coded values: minimal distance between + nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away. + During the simulation, `k` helps determine the distance between nodes, + though `scale` and `center` determine the size and place after + rescaling occurs at the end of the simulation. + + Fixing some nodes doesn't allow them to move in the simulation. + It also turns off the rescaling feature at the simulation's end. + In addition, setting `scale` to `None` turns off rescaling. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + k : float (default=None) + Optimal distance between nodes. If None the distance is set to + 1/sqrt(n) where n is the number of nodes. Increase this value + to move nodes farther apart. + + pos : dict or None optional (default=None) + Initial positions for nodes as a dictionary with node as keys + and values as a coordinate list or tuple. If None, then use + random initial positions. + + fixed : list or None optional (default=None) + Nodes to keep fixed at initial position. + Nodes not in ``G.nodes`` are ignored. + ValueError raised if `fixed` specified and `pos` not. + + iterations : int optional (default=50) + Maximum number of iterations taken + + threshold: float optional (default = 1e-4) + Threshold for relative error in node position changes. + The iteration stops if the error is below this threshold. + + weight : string or None optional (default='weight') + The edge attribute that holds the numerical value used for + the edge weight. Larger means a stronger attractive force. + If None, then all edge weights are 1. + + scale : number or None (default: 1) + Scale factor for positions. Not used unless `fixed is None`. + If scale is None, no rescaling is performed. + + center : array-like or None + Coordinate pair around which to center the layout. + Not used unless `fixed is None`. + + dim : int + Dimension of layout. + + seed : int, RandomState instance or None optional (default=None) + Used only for the initial positions in the algorithm. + Set the random state for deterministic node layouts. + If int, `seed` is the seed used by the random number generator, + if numpy.random.RandomState instance, `seed` is the random + number generator, + if None, the random number generator is the RandomState instance used + by numpy.random. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + method : str optional (default='auto') + The method to compute the layout. + If 'force', the force-directed Fruchterman-Reingold algorithm [1]_ is used. + If 'energy', the energy-based optimization algorithm [2]_ is used with absolute + values of edge weights and gravitational forces acting on each connected component. + If 'auto', we use 'force' if ``len(G) < 500`` and 'energy' otherwise. + + gravity: float optional (default=1.0) + Used only for the method='energy'. + The positive coefficient of gravitational forces per connected component. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.spring_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.spring_layout(G, seed=123, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([-0.61495802, -1. ]), + 1: array([-0.21789544, -0.35432583]), + 2: array([0.21847843, 0.35527369]), + 3: array([0.61437502, 0.99905215])} + + + # The same using longer but equivalent function name + >>> pos = nx.fruchterman_reingold_layout(G) + + References + ---------- + .. [1] Fruchterman, Thomas MJ, and Edward M. Reingold. + "Graph drawing by force-directed placement." + Software: Practice and experience 21, no. 11 (1991): 1129-1164. + http://dx.doi.org/10.1002/spe.4380211102 + .. [2] Hamaguchi, Hiroki, Naoki Marumo, and Akiko Takeda. + "Initial Placement for Fruchterman--Reingold Force Model With Coordinate Newton Direction." + arXiv preprint arXiv:2412.20317 (2024). + https://arxiv.org/abs/2412.20317 + """ + import numpy as np + + if method not in ("auto", "force", "energy"): + raise ValueError("the method must be either auto, force, or energy.") + if method == "auto": + method = "force" if len(G) < 500 else "energy" + + G, center = _process_params(G, center, dim) + + if fixed is not None: + if pos is None: + raise ValueError("nodes are fixed without positions given") + for node in fixed: + if node not in pos: + raise ValueError("nodes are fixed without positions given") + nfixed = {node: i for i, node in enumerate(G)} + fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed]) + + if pos is not None: + # Determine size of existing domain to adjust initial positions + dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup) + if dom_size == 0: + dom_size = 1 + pos_arr = seed.rand(len(G), dim) * dom_size + center + + for i, n in enumerate(G): + if n in pos: + pos_arr[i] = np.asarray(pos[n]) + else: + pos_arr = None + dom_size = 1 + + if len(G) == 0: + return {} + if len(G) == 1: + pos = {nx.utils.arbitrary_element(G.nodes()): center} + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + return pos + + # Sparse matrix + if len(G) >= 500 or method == "energy": + A = nx.to_scipy_sparse_array(G, weight=weight, dtype="f") + if k is None and fixed is not None: + # We must adjust k by domain size for layouts not near 1x1 + nnodes, _ = A.shape + k = dom_size / np.sqrt(nnodes) + pos = _sparse_fruchterman_reingold( + A, k, pos_arr, fixed, iterations, threshold, dim, seed, method, gravity + ) + else: + A = nx.to_numpy_array(G, weight=weight) + if k is None and fixed is not None: + # We must adjust k by domain size for layouts not near 1x1 + nnodes, _ = A.shape + k = dom_size / np.sqrt(nnodes) + pos = _fruchterman_reingold( + A, k, pos_arr, fixed, iterations, threshold, dim, seed + ) + if fixed is None and scale is not None: + pos = rescale_layout(pos, scale=scale) + center + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +fruchterman_reingold_layout = spring_layout + + +@np_random_state(7) +def _fruchterman_reingold( + A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None +): + # Position nodes in adjacency matrix A using Fruchterman-Reingold + # Entry point for NetworkX graph is fruchterman_reingold_layout() + import numpy as np + + try: + nnodes, _ = A.shape + except AttributeError as err: + msg = "fruchterman_reingold() takes an adjacency matrix as input" + raise nx.NetworkXError(msg) from err + + if pos is None: + # random initial positions + pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype) + else: + # make sure positions are of same type as matrix + pos = pos.astype(A.dtype) + + # optimal distance between nodes + if k is None: + k = np.sqrt(1.0 / nnodes) + # the initial "temperature" is about .1 of domain area (=1x1) + # this is the largest step allowed in the dynamics. + # We need to calculate this in case our fixed positions force our domain + # to be much bigger than 1x1 + t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1 + # simple cooling scheme. + # linearly step down by dt on each iteration so last iteration is size dt. + dt = t / (iterations + 1) + delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype) + # the inscrutable (but fast) version + # this is still O(V^2) + # could use multilevel methods to speed this up significantly + for iteration in range(iterations): + # matrix of difference between points + delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :] + # distance between points + distance = np.linalg.norm(delta, axis=-1) + # enforce minimum distance of 0.01 + np.clip(distance, 0.01, None, out=distance) + # displacement "force" + displacement = np.einsum( + "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k) + ) + # update positions + length = np.linalg.norm(displacement, axis=-1) + # Threshold the minimum length prior to position scaling + # See gh-8113 for detailed discussion of the threshold + length = np.clip(length, a_min=0.01, a_max=None) + delta_pos = np.einsum("ij,i->ij", displacement, t / length) + if fixed is not None: + # don't change positions of fixed nodes + delta_pos[fixed] = 0.0 + pos += delta_pos + # cool temperature + t -= dt + if (np.linalg.norm(delta_pos) / nnodes) < threshold: + break + return pos + + +@np_random_state(7) +def _sparse_fruchterman_reingold( + A, + k=None, + pos=None, + fixed=None, + iterations=50, + threshold=1e-4, + dim=2, + seed=None, + method="energy", + gravity=1.0, +): + # Position nodes in adjacency matrix A using Fruchterman-Reingold + # Entry point for NetworkX graph is fruchterman_reingold_layout() + # Sparse version + import numpy as np + import scipy as sp + + try: + nnodes, _ = A.shape + except AttributeError as err: + msg = "fruchterman_reingold() takes an adjacency matrix as input" + raise nx.NetworkXError(msg) from err + + if pos is None: + # random initial positions + pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype) + else: + # make sure positions are of same type as matrix + pos = pos.astype(A.dtype) + + # no fixed nodes + if fixed is None: + fixed = [] + + # optimal distance between nodes + if k is None: + k = np.sqrt(1.0 / nnodes) + + if method == "energy": + return _energy_fruchterman_reingold( + A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity + ) + + # make sure we have a LIst of Lists representation + try: + A = A.tolil() + except AttributeError: + A = (sp.sparse.coo_array(A)).tolil() + + # the initial "temperature" is about .1 of domain area (=1x1) + # this is the largest step allowed in the dynamics. + t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1 + # simple cooling scheme. + # linearly step down by dt on each iteration so last iteration is size dt. + dt = t / (iterations + 1) + + displacement = np.zeros((dim, nnodes)) + for iteration in range(iterations): + displacement *= 0 + # loop over rows + for i in range(A.shape[0]): + if i in fixed: + continue + # difference between this row's node position and all others + delta = (pos[i] - pos).T + # distance between points + distance = np.sqrt((delta**2).sum(axis=0)) + # enforce minimum distance of 0.01 + distance = np.clip(distance, a_min=0.01, a_max=None) + # the adjacency matrix row + Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container + # displacement "force" + displacement[:, i] += ( + delta * (k * k / distance**2 - Ai * distance / k) + ).sum(axis=1) + # update positions + length = np.sqrt((displacement**2).sum(axis=0)) + # Threshold the minimum length prior to position scaling + # See gh-8113 for detailed discussion of the threshold + length = np.clip(length, a_min=0.01, a_max=None) + delta_pos = (displacement * t / length).T + pos += delta_pos + # cool temperature + t -= dt + if (np.linalg.norm(delta_pos) / nnodes) < threshold: + break + return pos + + +def _energy_fruchterman_reingold( + A, nnodes, k, pos, fixed, iterations, threshold, dim, gravity +): + # Entry point for NetworkX graph is fruchterman_reingold_layout() + # energy-based version + import numpy as np + import scipy as sp + + if gravity <= 0: + raise ValueError(f"the gravity must be positive.") + + # make sure we have a Compressed Sparse Row format + try: + A = A.tocsr() + except AttributeError: + A = sp.sparse.csr_array(A) + + # Take absolute values of edge weights and symmetrize it + A = np.abs(A) + A = (A + A.T) / 2 + + n_components, labels = sp.sparse.csgraph.connected_components(A, directed=False) + bincount = np.bincount(labels) + batchsize = 500 + + def _cost_FR(x): + pos = x.reshape((nnodes, dim)) + grad = np.zeros((nnodes, dim)) + cost = 0.0 + for l in range(0, nnodes, batchsize): + r = min(l + batchsize, nnodes) + # difference between selected node positions and all others + delta = pos[l:r, np.newaxis, :] - pos[np.newaxis, :, :] + # distance between points with a minimum distance of 1e-5 + distance2 = np.sum(delta * delta, axis=2) + distance2 = np.maximum(distance2, 1e-10) + distance = np.sqrt(distance2) + # temporary variable for calculation + Ad = A[l:r] * distance + # attractive forces and repulsive forces + grad[l:r] = 2 * np.einsum("ij,ijk->ik", Ad / k - k**2 / distance2, delta) + # integrated attractive forces + cost += np.sum(Ad * distance2) / (3 * k) + # integrated repulsive forces + cost -= k**2 * np.sum(np.log(distance)) + # gravitational force from the centroids of connected components to (0.5, ..., 0.5)^T + centers = np.zeros((n_components, dim)) + np.add.at(centers, labels, pos) + delta0 = centers / bincount[:, np.newaxis] - 0.5 + grad += gravity * delta0[labels] + cost += gravity * 0.5 * np.sum(bincount * np.linalg.norm(delta0, axis=1) ** 2) + # fix positions of fixed nodes + grad[fixed] = 0.0 + return cost, grad.ravel() + + # Optimization of the energy function by L-BFGS algorithm + options = {"maxiter": iterations, "gtol": threshold} + return sp.optimize.minimize( + _cost_FR, pos.ravel(), method="L-BFGS-B", jac=True, options=options + ).x.reshape((nnodes, dim)) + + +def kamada_kawai_layout( + G, + dist=None, + pos=None, + weight="weight", + scale=1, + center=None, + dim=2, + store_pos_as=None, +): + """Position nodes using Kamada-Kawai path-length cost-function. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + dist : dict (default=None) + A two-level dictionary of optimal distances between nodes, + indexed by source and destination node. + If None, the distance is computed using shortest_path_length(). + + pos : dict or None optional (default=None) + Initial positions for nodes as a dictionary with node as keys + and values as a coordinate list or tuple. If None, then use + circular_layout() for dim >= 2 and a linear layout for dim == 1. + + weight : string or None optional (default='weight') + The edge attribute that holds the numerical value used for + the edge weight. If None, then all edge weights are 1. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.kamada_kawai_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.kamada_kawai_layout(G, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([0.99996577, 0.99366857]), + 1: array([0.32913544, 0.33543827]), + 2: array([-0.33544334, -0.32910684]), + 3: array([-0.99365787, -1. ])} + """ + import numpy as np + + G, center = _process_params(G, center, dim) + nNodes = len(G) + if nNodes == 0: + return {} + + if dist is None: + dist = dict(nx.shortest_path_length(G, weight=weight)) + dist_mtx = 1e6 * np.ones((nNodes, nNodes)) + for row, nr in enumerate(G): + if nr not in dist: + continue + rdist = dist[nr] + for col, nc in enumerate(G): + if nc not in rdist: + continue + dist_mtx[row][col] = rdist[nc] + + if pos is None: + if dim >= 3: + pos = random_layout(G, dim=dim) + elif dim == 2: + pos = circular_layout(G, dim=dim) + else: + pos = dict(zip(G, np.linspace(0, 1, len(G)))) + pos_arr = np.array([pos[n] for n in G]) + + pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim) + + pos = rescale_layout(pos, scale=scale) + center + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +def _kamada_kawai_solve(dist_mtx, pos_arr, dim): + # Anneal node locations based on the Kamada-Kawai cost-function, + # using the supplied matrix of preferred inter-node distances, + # and starting locations. + + import numpy as np + import scipy as sp + + meanwt = 1e-3 + costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim) + + optresult = sp.optimize.minimize( + _kamada_kawai_costfn, + pos_arr.ravel(), + method="L-BFGS-B", + args=costargs, + jac=True, + ) + + return optresult.x.reshape((-1, dim)) + + +def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim): + # Cost-function and gradient for Kamada-Kawai layout algorithm + nNodes = invdist.shape[0] + pos_arr = pos_vec.reshape((nNodes, dim)) + + delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :] + nodesep = np.linalg.norm(delta, axis=-1) + direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3)) + + offset = nodesep * invdist - 1.0 + offset[np.diag_indices(nNodes)] = 0 + + cost = 0.5 * np.sum(offset**2) + grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum( + "ij,ij,ijk->jk", invdist, offset, direction + ) + + # Additional parabolic term to encourage mean position to be near origin: + sumpos = np.sum(pos_arr, axis=0) + cost += 0.5 * meanweight * np.sum(sumpos**2) + grad += meanweight * sumpos + + return (cost, grad.ravel()) + + +def spectral_layout(G, weight="weight", scale=1, center=None, dim=2, store_pos_as=None): + """Position nodes using the eigenvectors of the graph Laplacian. + + Using the unnormalized Laplacian, the layout shows possible clusters of + nodes which are an approximation of the ratio cut. If dim is the number of + dimensions then the positions are the entries of the dim eigenvectors + corresponding to the ascending eigenvalues starting from the second one. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + weight : string or None optional (default='weight') + The edge attribute that holds the numerical value used for + the edge weight. If None, then all edge weights are 1. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.spectral_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.spectral_layout(G, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([-1. , 0.76536686]), + 1: array([-0.41421356, -0.76536686]), + 2: array([ 0.41421356, -0.76536686]), + 3: array([1. , 0.76536686])} + + + Notes + ----- + Directed graphs will be considered as undirected graphs when + positioning the nodes. + + For larger graphs (>500 nodes) this will use the SciPy sparse + eigenvalue solver (ARPACK). + """ + # handle some special cases that break the eigensolvers + import numpy as np + + G, center = _process_params(G, center, dim) + + if len(G) <= 2: + if len(G) == 0: + pos = np.array([]) + elif len(G) == 1: + pos = np.array([center]) + else: + pos = np.array([np.zeros(dim), np.array(center) * 2.0]) + return dict(zip(G, pos)) + try: + # Sparse matrix + if len(G) < 500: # dense solver is faster for small graphs + raise ValueError + A = nx.to_scipy_sparse_array(G, weight=weight, dtype="d") + # Symmetrize directed graphs + if G.is_directed(): + A = A + np.transpose(A) + pos = _sparse_spectral(A, dim) + except (ImportError, ValueError): + # Dense matrix + A = nx.to_numpy_array(G, weight=weight) + # Symmetrize directed graphs + if G.is_directed(): + A += A.T + pos = _spectral(A, dim) + + pos = rescale_layout(pos, scale=scale) + center + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +def _spectral(A, dim=2): + # Input adjacency matrix A + # Uses dense eigenvalue solver from numpy + import numpy as np + + try: + nnodes, _ = A.shape + except AttributeError as err: + msg = "spectral() takes an adjacency matrix as input" + raise nx.NetworkXError(msg) from err + + # form Laplacian matrix where D is diagonal of degrees + D = np.identity(nnodes, dtype=A.dtype) * np.sum(A, axis=1) + L = D - A + + eigenvalues, eigenvectors = np.linalg.eig(L) + # sort and keep smallest nonzero + index = np.argsort(eigenvalues)[1 : dim + 1] # 0 index is zero eigenvalue + return np.real(eigenvectors[:, index]) + + +def _sparse_spectral(A, dim=2): + # Input adjacency matrix A + # Uses sparse eigenvalue solver from scipy + # Could use multilevel methods here, see Koren "On spectral graph drawing" + import numpy as np + import scipy as sp + + try: + nnodes, _ = A.shape + except AttributeError as err: + msg = "sparse_spectral() takes an adjacency matrix as input" + raise nx.NetworkXError(msg) from err + + # form Laplacian matrix + D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(nnodes, nnodes)).tocsr() + L = D - A + + k = dim + 1 + # number of Lanczos vectors for ARPACK solver.What is the right scaling? + ncv = max(2 * k + 1, int(np.sqrt(nnodes))) + # return smallest k eigenvalues and eigenvectors + eigenvalues, eigenvectors = sp.sparse.linalg.eigsh(L, k, which="SM", ncv=ncv) + index = np.argsort(eigenvalues)[1:k] # 0 index is zero eigenvalue + return np.real(eigenvectors[:, index]) + + +def planar_layout(G, scale=1, center=None, dim=2, store_pos_as=None): + """Position nodes without edge intersections. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. If G is of type + nx.PlanarEmbedding, the positions are selected accordingly. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int + Dimension of layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Raises + ------ + NetworkXException + If G is not planar + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.planar_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.planar_layout(G, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([-0.77777778, -0.33333333]), + 1: array([ 1. , -0.33333333]), + 2: array([0.11111111, 0.55555556]), + 3: array([-0.33333333, 0.11111111])} + """ + import numpy as np + + if dim != 2: + raise ValueError("can only handle 2 dimensions") + + G, center = _process_params(G, center, dim) + + if len(G) == 0: + return {} + + if isinstance(G, nx.PlanarEmbedding): + embedding = G + else: + is_planar, embedding = nx.check_planarity(G) + if not is_planar: + raise nx.NetworkXException("G is not planar.") + pos = nx.combinatorial_embedding_to_pos(embedding) + node_list = list(embedding) + pos = np.vstack([pos[x] for x in node_list]) + pos = pos.astype(np.float64) + pos = rescale_layout(pos, scale=scale) + center + pos = dict(zip(node_list, pos)) + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + return pos + + +def spiral_layout( + G, + scale=1, + center=None, + dim=2, + resolution=0.35, + equidistant=False, + store_pos_as=None, +): + """Position nodes in a spiral layout. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + dim : int, default=2 + Dimension of layout, currently only dim=2 is supported. + Other dimension values result in a ValueError. + + resolution : float, default=0.35 + The compactness of the spiral layout returned. + Lower values result in more compressed spiral layouts. + + equidistant : bool, default=False + If True, nodes will be positioned equidistant from each other + by decreasing angle further from center. + If False, nodes will be positioned at equal angles + from each other by increasing separation further from center. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node + + Raises + ------ + ValueError + If dim != 2 + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.spiral_layout(G) + >>> nx.draw(G, pos=pos) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.spiral_layout(G, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([-0.64153279, -0.68555087]), + 1: array([-0.03307913, -0.46344795]), + 2: array([0.34927952, 0.14899882]), + 3: array([0.32533239, 1. ])} + + Notes + ----- + This algorithm currently only works in two dimensions. + + """ + import numpy as np + + if dim != 2: + raise ValueError("can only handle 2 dimensions") + + G, center = _process_params(G, center, dim) + + if len(G) == 0: + return {} + if len(G) == 1: + pos = {nx.utils.arbitrary_element(G): center} + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + return pos + + pos = [] + if equidistant: + chord = 1 + step = 0.5 + theta = resolution + theta += chord / (step * theta) + for _ in range(len(G)): + r = step * theta + theta += chord / r + pos.append([np.cos(theta) * r, np.sin(theta) * r]) + + else: + dist = np.arange(len(G), dtype=float) + angle = resolution * dist + pos = np.transpose(dist * np.array([np.cos(angle), np.sin(angle)])) + + pos = rescale_layout(np.array(pos), scale=scale) + center + + pos = dict(zip(G, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +def multipartite_layout( + G, subset_key="subset", align="vertical", scale=1, center=None, store_pos_as=None +): + """Position nodes in layers of straight lines. + + Parameters + ---------- + G : NetworkX graph or list of nodes + A position will be assigned to every node in G. + + subset_key : string or dict (default='subset') + If a string, the key of node data in G that holds the node subset. + If a dict, keyed by layer number to the nodes in that layer/subset. + + align : string (default='vertical') + The alignment of nodes. Vertical or horizontal. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node. + + Examples + -------- + >>> G = nx.complete_multipartite_graph(28, 16, 10) + >>> pos = nx.multipartite_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> G = nx.complete_multipartite_graph(28, 16, 10) + >>> _ = nx.multipartite_layout(G, store_pos_as="pos") + + or use a dict to provide the layers of the layout + + >>> G = nx.Graph([(0, 1), (1, 2), (1, 3), (3, 4)]) + >>> layers = {"a": [0], "b": [1], "c": [2, 3], "d": [4]} + >>> pos = nx.multipartite_layout(G, subset_key=layers) + + Notes + ----- + This algorithm currently only works in two dimensions and does not + try to minimize edge crossings. + + Network does not need to be a complete multipartite graph. As long as nodes + have subset_key data, they will be placed in the corresponding layers. + + """ + import numpy as np + + if align not in ("vertical", "horizontal"): + msg = "align must be either vertical or horizontal." + raise ValueError(msg) + + G, center = _process_params(G, center=center, dim=2) + if len(G) == 0: + return {} + + try: + # check if subset_key is dict-like + if len(G) != sum(len(nodes) for nodes in subset_key.values()): + raise nx.NetworkXError( + "all nodes must be in one subset of `subset_key` dict" + ) + except AttributeError: + # subset_key is not a dict, hence a string + node_to_subset = nx.get_node_attributes(G, subset_key) + if len(node_to_subset) != len(G): + raise nx.NetworkXError( + f"all nodes need a subset_key attribute: {subset_key}" + ) + subset_key = nx.utils.groups(node_to_subset) + + # Sort by layer, if possible + try: + layers = dict(sorted(subset_key.items())) + except TypeError: + layers = subset_key + + pos = None + nodes = [] + width = len(layers) + for i, layer in enumerate(layers.values()): + height = len(layer) + xs = np.repeat(i, height) + ys = np.arange(0, height, dtype=float) + offset = ((width - 1) / 2, (height - 1) / 2) + layer_pos = np.column_stack([xs, ys]) - offset + if pos is None: + pos = layer_pos + else: + pos = np.concatenate([pos, layer_pos]) + nodes.extend(layer) + pos = rescale_layout(pos, scale=scale) + center + if align == "horizontal": + pos = pos[:, ::-1] # swap x and y coords + pos = dict(zip(nodes, pos)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +@np_random_state("seed") +def arf_layout( + G, + pos=None, + scaling=1, + a=1.1, + etol=1e-6, + dt=1e-3, + max_iter=1000, + *, + seed=None, + store_pos_as=None, +): + """Arf layout for networkx + + The attractive and repulsive forces (arf) layout [1] improves the spring + layout in three ways. First, it prevents congestion of highly connected nodes + due to strong forcing between nodes. Second, it utilizes the layout space + more effectively by preventing large gaps that spring layout tends to create. + Lastly, the arf layout represents symmetries in the layout better than the + default spring layout. + + Parameters + ---------- + G : nx.Graph or nx.DiGraph + Networkx graph. + pos : dict + Initial position of the nodes. If set to None a + random layout will be used. + scaling : float + Scales the radius of the circular layout space. + a : float + Strength of springs between connected nodes. Should be larger than 1. + The greater a, the clearer the separation of unconnected sub clusters. + etol : float + Gradient sum of spring forces must be larger than `etol` before successful + termination. + dt : float + Time step for force differential equation simulations. + max_iter : int + Max iterations before termination of the algorithm. + seed : int, RandomState instance or None optional (default=None) + Set the random state for deterministic node layouts. + If int, `seed` is the seed used by the random number generator, + if numpy.random.RandomState instance, `seed` is the random + number generator, + if None, the random number generator is the RandomState instance used + by numpy.random. + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node. + + Examples + -------- + >>> G = nx.grid_graph((5, 5)) + >>> pos = nx.arf_layout(G) + >>> # suppress the returned dict and store on the graph directly + >>> G = nx.grid_graph((5, 5)) + >>> _ = nx.arf_layout(G, store_pos_as="pos") + + References + ---------- + .. [1] "Self-Organization Applied to Dynamic Network Layout", M. Geipel, + International Journal of Modern Physics C, 2007, Vol 18, No 10, + pp. 1537-1549. + https://doi.org/10.1142/S0129183107011558 https://arxiv.org/abs/0704.1748 + """ + import warnings + + import numpy as np + + if a <= 1: + msg = "The parameter a should be larger than 1" + raise ValueError(msg) + + pos_tmp = nx.random_layout(G, seed=seed) + if pos is None: + pos = pos_tmp + else: + for node in G.nodes(): + if node not in pos: + pos[node] = pos_tmp[node].copy() + + # Initialize spring constant matrix + N = len(G) + # No nodes no computation + if N == 0: + return pos + + # init force of springs + K = np.ones((N, N)) - np.eye(N) + node_order = {node: i for i, node in enumerate(G)} + for x, y in G.edges(): + if x != y: + idx, jdx = (node_order[i] for i in (x, y)) + K[idx, jdx] = a + + # vectorize values + p = np.asarray(list(pos.values())) + + # equation 10 in [1] + rho = scaling * np.sqrt(N) + + # looping variables + error = etol + 1 + n_iter = 0 + while error > etol: + diff = p[:, np.newaxis] - p[np.newaxis] + A = np.linalg.norm(diff, axis=-1)[..., np.newaxis] + # attraction_force - repulsions force + # suppress nans due to division; caused by diagonal set to zero. + # Does not affect the computation due to nansum + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + change = K[..., np.newaxis] * diff - rho / A * diff + change = np.nansum(change, axis=0) + p += change * dt + + error = np.linalg.norm(change, axis=-1).sum() + if n_iter > max_iter: + break + n_iter += 1 + + pos = dict(zip(G.nodes(), p)) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +@np_random_state("seed") +@nx._dispatchable(edge_attrs="weight", mutates_input={"store_pos_as": 15}) +def forceatlas2_layout( + G, + pos=None, + *, + max_iter=100, + jitter_tolerance=1.0, + scaling_ratio=2.0, + gravity=1.0, + distributed_action=False, + strong_gravity=False, + node_mass=None, + node_size=None, + weight=None, + linlog=False, + seed=None, + dim=2, + store_pos_as=None, +): + """Position nodes using the ForceAtlas2 force-directed layout algorithm. + + This function applies the ForceAtlas2 layout algorithm [1]_ to a NetworkX graph, + positioning the nodes in a way that visually represents the structure of the graph. + The algorithm uses physical simulation to minimize the energy of the system, + resulting in a more readable layout. + + Parameters + ---------- + G : nx.Graph + A NetworkX graph to be laid out. + pos : dict or None, optional + Initial positions of the nodes. If None, random initial positions are used. + max_iter : int (default: 100) + Number of iterations for the layout optimization. + jitter_tolerance : float (default: 1.0) + Controls the tolerance for adjusting the speed of layout generation. + scaling_ratio : float (default: 2.0) + Determines the scaling of attraction and repulsion forces. + gravity : float (default: 1.0) + Determines the amount of attraction on nodes to the center. Prevents islands + (i.e. weakly connected or disconnected parts of the graph) + from drifting away. + distributed_action : bool (default: False) + Distributes the attraction force evenly among nodes. + strong_gravity : bool (default: False) + Applies a strong gravitational pull towards the center. + node_mass : dict or None, optional + Maps nodes to their masses, influencing the attraction to other nodes. + node_size : dict or None, optional + Maps nodes to their sizes, preventing crowding by creating a halo effect. + weight : string or None, optional (default: None) + The edge attribute that holds the numerical value used for + the edge weight. If None, then all edge weights are 1. + linlog : bool (default: False) + Uses logarithmic attraction instead of linear. + seed : int, RandomState instance or None optional (default=None) + Used only for the initial positions in the algorithm. + Set the random state for deterministic node layouts. + If int, `seed` is the seed used by the random number generator, + if numpy.random.RandomState instance, `seed` is the random + number generator, + if None, the random number generator is the RandomState instance used + by numpy.random. + dim : int (default: 2) + Sets the dimensions for the layout. Ignored if `pos` is provided. + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Examples + -------- + >>> import networkx as nx + >>> G = nx.florentine_families_graph() + >>> pos = nx.forceatlas2_layout(G) + >>> nx.draw(G, pos=pos) + >>> # suppress the returned dict and store on the graph directly + >>> pos = nx.forceatlas2_layout(G, store_pos_as="pos") + >>> _ = nx.forceatlas2_layout(G, store_pos_as="pos") + + References + ---------- + .. [1] Jacomy, M., Venturini, T., Heymann, S., & Bastian, M. (2014). + ForceAtlas2, a continuous graph layout algorithm for handy network + visualization designed for the Gephi software. PloS one, 9(6), e98679. + https://doi.org/10.1371/journal.pone.0098679 + """ + import numpy as np + + if len(G) == 0: + return {} + # parse optional pos positions + if pos is None: + pos = nx.random_layout(G, dim=dim, seed=seed) + pos_arr = np.array(list(pos.values())) + elif len(pos) == len(G): + pos_arr = np.array([pos[node].copy() for node in G]) + else: + # set random node pos within the initial pos values + pos_init = np.array(list(pos.values())) + max_pos = pos_init.max(axis=0) + min_pos = pos_init.min(axis=0) + dim = max_pos.size + pos_arr = min_pos + seed.rand(len(G), dim) * (max_pos - min_pos) + for idx, node in enumerate(G): + if node in pos: + pos_arr[idx] = pos[node].copy() + + mass = np.zeros(len(G)) + size = np.zeros(len(G)) + + # Only adjust for size when the users specifies size other than default (1) + adjust_sizes = False + if node_size is None: + node_size = {} + else: + adjust_sizes = True + + if node_mass is None: + node_mass = {} + + for idx, node in enumerate(G): + mass[idx] = node_mass.get(node, G.degree(node) + 1) + size[idx] = node_size.get(node, 1) + + n = len(G) + gravities = np.zeros((n, dim)) + attraction = np.zeros((n, dim)) + repulsion = np.zeros((n, dim)) + A = nx.to_numpy_array(G, weight=weight) + + def estimate_factor(n, swing, traction, speed, speed_efficiency, jitter_tolerance): + """Computes the scaling factor for the force in the ForceAtlas2 layout algorithm. + + This helper function adjusts the speed and + efficiency of the layout generation based on the + current state of the system, such as the number of + nodes, current swing, and traction forces. + + Parameters + ---------- + n : int + Number of nodes in the graph. + swing : float + The current swing, representing the oscillation of the nodes. + traction : float + The current traction force, representing the attraction between nodes. + speed : float + The current speed of the layout generation. + speed_efficiency : float + The efficiency of the current speed, influencing how fast the layout converges. + jitter_tolerance : float + The tolerance for jitter, affecting how much speed adjustment is allowed. + + Returns + ------- + tuple + A tuple containing the updated speed and speed efficiency. + + Notes + ----- + This function is a part of the ForceAtlas2 layout algorithm and is used to dynamically adjust the + layout parameters to achieve an optimal and stable visualization. + + """ + import numpy as np + + # estimate jitter + opt_jitter = 0.05 * np.sqrt(n) + min_jitter = np.sqrt(opt_jitter) + max_jitter = 10 + min_speed_efficiency = 0.05 + + other = min(max_jitter, opt_jitter * traction / n**2) + jitter = jitter_tolerance * max(min_jitter, other) + + if swing / traction > 2.0: + if speed_efficiency > min_speed_efficiency: + speed_efficiency *= 0.5 + jitter = max(jitter, jitter_tolerance) + if swing == 0: + target_speed = np.inf + else: + target_speed = jitter * speed_efficiency * traction / swing + + if swing > jitter * traction: + if speed_efficiency > min_speed_efficiency: + speed_efficiency *= 0.7 + elif speed < 1000: + speed_efficiency *= 1.3 + + max_rise = 0.5 + speed = speed + min(target_speed - speed, max_rise * speed) + return speed, speed_efficiency + + speed = 1 + speed_efficiency = 1 + swing = 1 + traction = 1 + for _ in range(max_iter): + # compute pairwise difference + diff = pos_arr[:, None] - pos_arr[None] + # compute pairwise distance + distance = np.linalg.norm(diff, axis=-1) + + # linear attraction + if linlog: + attraction = -np.log(1 + distance) / distance + np.fill_diagonal(attraction, 0) + attraction = np.einsum("ij, ij -> ij", attraction, A) + attraction = np.einsum("ijk, ij -> ik", diff, attraction) + + else: + attraction = -np.einsum("ijk, ij -> ik", diff, A) + + if distributed_action: + attraction /= mass[:, None] + + # repulsion + tmp = mass[:, None] @ mass[None] + if adjust_sizes: + distance += -size[:, None] - size[None] + + d2 = distance**2 + # remove self-interaction + np.fill_diagonal(tmp, 0) + np.fill_diagonal(d2, 1) + factor = (tmp / d2) * scaling_ratio + repulsion = np.einsum("ijk, ij -> ik", diff, factor) + + # gravity + pos_centered = pos_arr - np.mean(pos_arr, axis=0) + if strong_gravity: + gravities = -gravity * mass[:, None] * pos_centered + else: + # hide warnings for divide by zero. Then change nan to 0 + with np.errstate(divide="ignore", invalid="ignore"): + unit_vec = pos_centered / np.linalg.norm(pos_centered, axis=-1)[:, None] + unit_vec = np.nan_to_num(unit_vec, nan=0) + gravities = -gravity * mass[:, None] * unit_vec + + # total forces + update = attraction + repulsion + gravities + + # compute total swing and traction + swing += (mass * np.linalg.norm(pos_arr - update, axis=-1)).sum() + traction += (0.5 * mass * np.linalg.norm(pos_arr + update, axis=-1)).sum() + + speed, speed_efficiency = estimate_factor( + n, + swing, + traction, + speed, + speed_efficiency, + jitter_tolerance, + ) + + # update pos + if adjust_sizes: + df = np.linalg.norm(update, axis=-1) + swinging = mass * df + factor = 0.1 * speed / (1 + np.sqrt(speed * swinging)) + factor = np.minimum(factor * df, 10.0 * np.ones(df.shape)) / df + else: + swinging = mass * np.linalg.norm(update, axis=-1) + factor = speed / (1 + np.sqrt(speed * swinging)) + + factored_update = update * factor[:, None] + pos_arr += factored_update + if abs(factored_update).sum() < 1e-10: + break + + pos = dict(zip(G, pos_arr)) + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos + + +def rescale_layout(pos, scale=1): + """Returns scaled position array to (-scale, scale) in all axes. + + The function acts on NumPy arrays which hold position information. + Each position is one row of the array. The dimension of the space + equals the number of columns. Each coordinate in one column. + + To rescale, the mean (center) is subtracted from each axis separately. + Then all values are scaled so that the largest magnitude value + from all axes equals `scale` (thus, the aspect ratio is preserved). + The resulting NumPy Array is returned (order of rows unchanged). + + Parameters + ---------- + pos : numpy array + positions to be scaled. Each row is a position. + + scale : number (default: 1) + The size of the resulting extent in all directions. + + attribute : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute named `attribute` which can be accessed with + `G.nodes[...][attribute]`. The function still returns the dictionary. + + Returns + ------- + pos : numpy array + scaled positions. Each row is a position. + + See Also + -------- + rescale_layout_dict + """ + import numpy as np + + # Find max length over all dimensions + pos -= pos.mean(axis=0) + lim = np.abs(pos).max() # max coordinate for all axes + # rescale to (-scale, scale) in all directions, preserves aspect + if lim > 0: + pos *= scale / lim + return pos + + +def rescale_layout_dict(pos, scale=1): + """Return a dictionary of scaled positions keyed by node + + Parameters + ---------- + pos : A dictionary of positions keyed by node + + scale : number (default: 1) + The size of the resulting extent in all directions. + + Returns + ------- + pos : A dictionary of positions keyed by node + + Examples + -------- + >>> import numpy as np + >>> pos = {0: np.array((0, 0)), 1: np.array((1, 1)), 2: np.array((0.5, 0.5))} + >>> nx.rescale_layout_dict(pos) + {0: array([-1., -1.]), 1: array([1., 1.]), 2: array([0., 0.])} + + >>> pos = {0: np.array((0, 0)), 1: np.array((-1, 1)), 2: np.array((-0.5, 0.5))} + >>> nx.rescale_layout_dict(pos, scale=2) + {0: array([ 2., -2.]), 1: array([-2., 2.]), 2: array([0., 0.])} + + See Also + -------- + rescale_layout + """ + import numpy as np + + if not pos: # empty_graph + return {} + pos_v = np.array(list(pos.values())) + pos_v = rescale_layout(pos_v, scale=scale) + return dict(zip(pos, pos_v)) + + +def bfs_layout(G, start, *, align="vertical", scale=1, center=None, store_pos_as=None): + """Position nodes according to breadth-first search algorithm. + + Parameters + ---------- + G : NetworkX graph + A position will be assigned to every node in G. + + start : node in `G` + Starting node for bfs + + align : string (default='vertical') + The alignment of nodes within a layer, either `"vertical"` or + `"horizontal"`. + + scale : number (default: 1) + Scale factor for positions. + + center : array-like or None + Coordinate pair around which to center the layout. + + store_pos_as : str, default None + If non-None, the position of each node will be stored on the graph as + an attribute with this string as its name, which can be accessed with + ``G.nodes[...][store_pos_as]``. The function still returns the dictionary. + + Returns + ------- + pos : dict + A dictionary of positions keyed by node. + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(4) + >>> pos = nx.bfs_layout(G, 0) + >>> # suppress the returned dict and store on the graph directly + >>> _ = nx.bfs_layout(G, 0, store_pos_as="pos") + >>> pprint(nx.get_node_attributes(G, "pos")) + {0: array([-1., 0.]), + 1: array([-0.33333333, 0. ]), + 2: array([0.33333333, 0. ]), + 3: array([1., 0.])} + + + + Notes + ----- + This algorithm currently only works in two dimensions and does not + try to minimize edge crossings. + + """ + G, center = _process_params(G, center, 2) + + # Compute layers with BFS + layers = dict(enumerate(nx.bfs_layers(G, start))) + + if len(G) != sum(len(nodes) for nodes in layers.values()): + raise nx.NetworkXError( + "bfs_layout didn't include all nodes. Perhaps use input graph:\n" + " G.subgraph(nx.node_connected_component(G, start))" + ) + + # Compute node positions with multipartite_layout + pos = multipartite_layout( + G, subset_key=layers, align=align, scale=scale, center=center + ) + + if store_pos_as is not None: + nx.set_node_attributes(G, pos, store_pos_as) + + return pos diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_agraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_agraph.py new file mode 100644 index 0000000000000000000000000000000000000000..897ab7f38a973bdf3ed1cbf7ac9504f4e93354f3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_agraph.py @@ -0,0 +1,470 @@ +""" +*************** +Graphviz AGraph +*************** + +Interface to pygraphviz AGraph class. + +Examples +-------- +>>> G = nx.complete_graph(5) +>>> A = nx.nx_agraph.to_agraph(G) +>>> H = nx.nx_agraph.from_agraph(A) + +See Also +-------- + - Pygraphviz: http://pygraphviz.github.io/ + - Graphviz: https://www.graphviz.org + - DOT Language: http://www.graphviz.org/doc/info/lang.html +""" + +import tempfile + +import networkx as nx + +__all__ = [ + "from_agraph", + "to_agraph", + "write_dot", + "read_dot", + "graphviz_layout", + "pygraphviz_layout", + "view_pygraphviz", +] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def from_agraph(A, create_using=None): + """Returns a NetworkX Graph or DiGraph from a PyGraphviz graph. + + Parameters + ---------- + A : PyGraphviz AGraph + A graph created with PyGraphviz + + create_using : NetworkX graph constructor, optional (default=None) + Graph type to create. If graph instance, then cleared before populated. + If `None`, then the appropriate Graph type is inferred from `A`. + + Examples + -------- + >>> K5 = nx.complete_graph(5) + >>> A = nx.nx_agraph.to_agraph(K5) + >>> G = nx.nx_agraph.from_agraph(A) + + Notes + ----- + The Graph G will have a dictionary G.graph_attr containing + the default graphviz attributes for graphs, nodes and edges. + + Default node attributes will be in the dictionary G.node_attr + which is keyed by node. + + Edge attributes will be returned as edge data in G. With + edge_attr=False the edge data will be the Graphviz edge weight + attribute or the value 1 if no edge weight attribute is found. + + """ + if create_using is None: + if A.is_directed(): + if A.is_strict(): + create_using = nx.DiGraph + else: + create_using = nx.MultiDiGraph + else: + if A.is_strict(): + create_using = nx.Graph + else: + create_using = nx.MultiGraph + + # assign defaults + N = nx.empty_graph(0, create_using) + if A.name is not None: + N.name = A.name + + # add graph attributes + N.graph.update(A.graph_attr) + + # add nodes, attributes to N.node_attr + for n in A.nodes(): + str_attr = {str(k): v for k, v in n.attr.items()} + N.add_node(str(n), **str_attr) + + # add edges, assign edge data as dictionary of attributes + for e in A.edges(): + u, v = str(e[0]), str(e[1]) + attr = dict(e.attr) + str_attr = {str(k): v for k, v in attr.items()} + if not N.is_multigraph(): + if e.name is not None: + str_attr["key"] = e.name + N.add_edge(u, v, **str_attr) + else: + N.add_edge(u, v, key=e.name, **str_attr) + + # add default attributes for graph, nodes, and edges + # hang them on N.graph_attr + graph_default_dict = dict(A.graph_attr) + if graph_default_dict: + N.graph["graph"] = graph_default_dict + node_default_dict = dict(A.node_attr) + if node_default_dict and node_default_dict != {"label": "\\N"}: + N.graph["node"] = node_default_dict + edge_default_dict = dict(A.edge_attr) + if edge_default_dict: + N.graph["edge"] = edge_default_dict + return N + + +def to_agraph(N): + """Returns a pygraphviz graph from a NetworkX graph N. + + Parameters + ---------- + N : NetworkX graph + A graph created with NetworkX + + Examples + -------- + >>> K5 = nx.complete_graph(5) + >>> A = nx.nx_agraph.to_agraph(K5) + + Notes + ----- + If N has an dict N.graph_attr an attempt will be made first + to copy properties attached to the graph (see from_agraph) + and then updated with the calling arguments if any. + + """ + try: + import pygraphviz + except ImportError as err: + raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err + directed = N.is_directed() + strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph() + + A = pygraphviz.AGraph(name=N.name, strict=strict, directed=directed) + + # default graph attributes + A.graph_attr.update(N.graph.get("graph", {})) + A.node_attr.update(N.graph.get("node", {})) + A.edge_attr.update(N.graph.get("edge", {})) + + A.graph_attr.update( + (k, v) for k, v in N.graph.items() if k not in ("graph", "node", "edge") + ) + + # add nodes + for n, nodedata in N.nodes(data=True): + A.add_node(n) + # Add node data + a = A.get_node(n) + for key, val in nodedata.items(): + if key == "pos": + a.attr["pos"] = f"{val[0]},{val[1]}!" + else: + a.attr[key] = str(val) + + # loop over edges + if N.is_multigraph(): + for u, v, key, edgedata in N.edges(data=True, keys=True): + str_edgedata = {k: str(v) for k, v in edgedata.items() if k != "key"} + A.add_edge(u, v, key=str(key)) + # Add edge data + a = A.get_edge(u, v) + a.attr.update(str_edgedata) + + else: + for u, v, edgedata in N.edges(data=True): + str_edgedata = {k: str(v) for k, v in edgedata.items()} + A.add_edge(u, v) + # Add edge data + a = A.get_edge(u, v) + a.attr.update(str_edgedata) + + return A + + +def write_dot(G, path): + """Write NetworkX graph G to Graphviz dot format on path. + + Parameters + ---------- + G : graph + A networkx graph + path : filename + Filename or file handle to write + + Notes + ----- + To use a specific graph layout, call ``A.layout`` prior to `write_dot`. + Note that some graphviz layouts are not guaranteed to be deterministic, + see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info. + """ + A = to_agraph(G) + A.write(path) + A.clear() + return + + +@nx._dispatchable(name="agraph_read_dot", graphs=None, returns_graph=True) +def read_dot(path): + """Returns a NetworkX graph from a dot file on path. + + Parameters + ---------- + path : file or string + File name or file handle to read. + """ + try: + import pygraphviz + except ImportError as err: + raise ImportError( + "read_dot() requires pygraphviz http://pygraphviz.github.io/" + ) from err + A = pygraphviz.AGraph(file=path) + gr = from_agraph(A) + A.clear() + return gr + + +def graphviz_layout(G, prog="neato", root=None, args=""): + """Create node positions for G using Graphviz. + + Parameters + ---------- + G : NetworkX graph + A graph created with NetworkX + prog : string + Name of Graphviz layout program + root : string, optional + Root node for twopi layout + args : string, optional + Extra arguments to Graphviz layout program + + Returns + ------- + Dictionary of x, y, positions keyed by node. + + Examples + -------- + >>> G = nx.petersen_graph() + >>> pos = nx.nx_agraph.graphviz_layout(G) + >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot") + + Notes + ----- + This is a wrapper for pygraphviz_layout. + + Note that some graphviz layouts are not guaranteed to be deterministic, + see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info. + """ + return pygraphviz_layout(G, prog=prog, root=root, args=args) + + +def pygraphviz_layout(G, prog="neato", root=None, args=""): + """Create node positions for G using Graphviz. + + Parameters + ---------- + G : NetworkX graph + A graph created with NetworkX + prog : string + Name of Graphviz layout program + root : string, optional + Root node for twopi layout + args : string, optional + Extra arguments to Graphviz layout program + + Returns + ------- + node_pos : dict + Dictionary of x, y, positions keyed by node. + + Examples + -------- + >>> G = nx.petersen_graph() + >>> pos = nx.nx_agraph.graphviz_layout(G) + >>> pos = nx.nx_agraph.graphviz_layout(G, prog="dot") + + Notes + ----- + If you use complex node objects, they may have the same string + representation and GraphViz could treat them as the same node. + The layout may assign both nodes a single location. See Issue #1568 + If this occurs in your case, consider relabeling the nodes just + for the layout computation using something similar to:: + + >>> H = nx.convert_node_labels_to_integers(G, label_attribute="node_label") + >>> H_layout = nx.nx_agraph.pygraphviz_layout(H, prog="dot") + >>> G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()} + + Note that some graphviz layouts are not guaranteed to be deterministic, + see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info. + """ + try: + import pygraphviz + except ImportError as err: + raise ImportError("requires pygraphviz http://pygraphviz.github.io/") from err + if root is not None: + args += f"-Groot={root}" + A = to_agraph(G) + A.layout(prog=prog, args=args) + node_pos = {} + for n in G: + node = pygraphviz.Node(A, n) + try: + xs = node.attr["pos"].split(",") + node_pos[n] = tuple(float(x) for x in xs) + except: + print("no position for node", n) + node_pos[n] = (0.0, 0.0) + return node_pos + + +@nx.utils.open_file(5, "w+b") +def view_pygraphviz( + G, edgelabel=None, prog="dot", args="", suffix="", path=None, show=True +): + """Views the graph G using the specified layout algorithm. + + Parameters + ---------- + G : NetworkX graph + The machine to draw. + edgelabel : str, callable, None + If a string, then it specifies the edge attribute to be displayed + on the edge labels. If a callable, then it is called for each + edge and it should return the string to be displayed on the edges. + The function signature of `edgelabel` should be edgelabel(data), + where `data` is the edge attribute dictionary. + prog : string + Name of Graphviz layout program. + args : str + Additional arguments to pass to the Graphviz layout program. + suffix : str + If `filename` is None, we save to a temporary file. The value of + `suffix` will appear at the tail end of the temporary filename. + path : str, None + The filename used to save the image. If None, save to a temporary + file. File formats are the same as those from pygraphviz.agraph.draw. + Filenames ending in .gz or .bz2 will be compressed. + show : bool, default = True + Whether to display the graph with :mod:`PIL.Image.show`, + default is `True`. If `False`, the rendered graph is still available + at `path`. + + Returns + ------- + path : str + The filename of the generated image. + A : PyGraphviz graph + The PyGraphviz graph instance used to generate the image. + + Notes + ----- + If this function is called in succession too quickly, sometimes the + image is not displayed. So you might consider time.sleep(.5) between + calls if you experience problems. + + Note that some graphviz layouts are not guaranteed to be deterministic, + see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info. + + """ + if not len(G): + raise nx.NetworkXException("An empty graph cannot be drawn.") + + # If we are providing default values for graphviz, these must be set + # before any nodes or edges are added to the PyGraphviz graph object. + # The reason for this is that default values only affect incoming objects. + # If you change the default values after the objects have been added, + # then they inherit no value and are set only if explicitly set. + + # to_agraph() uses these values. + attrs = ["edge", "node", "graph"] + for attr in attrs: + if attr not in G.graph: + G.graph[attr] = {} + + # These are the default values. + edge_attrs = {"fontsize": "10"} + node_attrs = { + "style": "filled", + "fillcolor": "#0000FF40", + "height": "0.75", + "width": "0.75", + "shape": "circle", + } + graph_attrs = {} + + def update_attrs(which, attrs): + # Update graph attributes. Return list of those which were added. + added = [] + for k, v in attrs.items(): + if k not in G.graph[which]: + G.graph[which][k] = v + added.append(k) + + def clean_attrs(which, added): + # Remove added attributes + for attr in added: + del G.graph[which][attr] + if not G.graph[which]: + del G.graph[which] + + # Update all default values + update_attrs("edge", edge_attrs) + update_attrs("node", node_attrs) + update_attrs("graph", graph_attrs) + + # Convert to agraph, so we inherit default values + A = to_agraph(G) + + # Remove the default values we added to the original graph. + clean_attrs("edge", edge_attrs) + clean_attrs("node", node_attrs) + clean_attrs("graph", graph_attrs) + + # If the user passed in an edgelabel, we update the labels for all edges. + if edgelabel is not None: + if not callable(edgelabel): + + def func(data): + return "".join([" ", str(data[edgelabel]), " "]) + + else: + func = edgelabel + + # update all the edge labels + if G.is_multigraph(): + for u, v, key, data in G.edges(keys=True, data=True): + # PyGraphviz doesn't convert the key to a string. See #339 + edge = A.get_edge(u, v, str(key)) + edge.attr["label"] = str(func(data)) + else: + for u, v, data in G.edges(data=True): + edge = A.get_edge(u, v) + edge.attr["label"] = str(func(data)) + + if path is None: + ext = "png" + if suffix: + suffix = f"_{suffix}.{ext}" + else: + suffix = f".{ext}" + path = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) + else: + # Assume the decorator worked and it is a file-object. + pass + + # Write graph to file + A.draw(path=path, format=None, prog=prog, args=args) + path.close() + + # Show graph in a new window (depends on platform configuration) + if show: + from PIL import Image + + Image.open(path.name).show() + + return path.name, A diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_latex.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_latex.py new file mode 100644 index 0000000000000000000000000000000000000000..677def8e75144afe5d73fbdea9f5643222a4ea01 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_latex.py @@ -0,0 +1,570 @@ +r""" +***** +LaTeX +***** + +Export NetworkX graphs in LaTeX format using the TikZ library within TeX/LaTeX. +Usually, you will want the drawing to appear in a figure environment so +you use ``to_latex(G, caption="A caption")``. If you want the raw +drawing commands without a figure environment use :func:`to_latex_raw`. +And if you want to write to a file instead of just returning the latex +code as a string, use ``write_latex(G, "filename.tex", caption="A caption")``. + +To construct a figure with subfigures for each graph to be shown, provide +``to_latex`` or ``write_latex`` a list of graphs, a list of subcaptions, +and a number of rows of subfigures inside the figure. + +To be able to refer to the figures or subfigures in latex using ``\\ref``, +the keyword ``latex_label`` is available for figures and `sub_labels` for +a list of labels, one for each subfigure. + +We intend to eventually provide an interface to the TikZ Graph +features which include e.g. layout algorithms. + +Let us know via github what you'd like to see available, or better yet +give us some code to do it, or even better make a github pull request +to add the feature. + +The TikZ approach +================= +Drawing options can be stored on the graph as node/edge attributes, or +can be provided as dicts keyed by node/edge to a string of the options +for that node/edge. Similarly a label can be shown for each node/edge +by specifying the labels as graph node/edge attributes or by providing +a dict keyed by node/edge to the text to be written for that node/edge. + +Options for the tikzpicture environment (e.g. "[scale=2]") can be provided +via a keyword argument. Similarly default node and edge options can be +provided through keywords arguments. The default node options are applied +to the single TikZ "path" that draws all nodes (and no edges). The default edge +options are applied to a TikZ "scope" which contains a path for each edge. + +Examples +======== +>>> G = nx.path_graph(3) +>>> nx.write_latex(G, "just_my_figure.tex", as_document=True) +>>> nx.write_latex(G, "my_figure.tex", caption="A path graph", latex_label="fig1") +>>> latex_code = nx.to_latex(G) # a string rather than a file + +You can change many features of the nodes and edges. + +>>> G = nx.path_graph(4, create_using=nx.DiGraph) +>>> pos = {n: (n, n) for n in G} # nodes set on a line + +>>> G.nodes[0]["style"] = "blue" +>>> G.nodes[2]["style"] = "line width=3,draw" +>>> G.nodes[3]["label"] = "Stop" +>>> G.edges[(0, 1)]["label"] = "1st Step" +>>> G.edges[(0, 1)]["label_opts"] = "near start" +>>> G.edges[(1, 2)]["style"] = "line width=3" +>>> G.edges[(1, 2)]["label"] = "2nd Step" +>>> G.edges[(2, 3)]["style"] = "green" +>>> G.edges[(2, 3)]["label"] = "3rd Step" +>>> G.edges[(2, 3)]["label_opts"] = "near end" + +>>> nx.write_latex(G, "latex_graph.tex", pos=pos, as_document=True) + +Then compile the LaTeX using something like ``pdflatex latex_graph.tex`` +and view the pdf file created: ``latex_graph.pdf``. + +If you want **subfigures** each containing one graph, you can input a list of graphs. + +>>> H1 = nx.path_graph(4) +>>> H2 = nx.complete_graph(4) +>>> H3 = nx.path_graph(8) +>>> H4 = nx.complete_graph(8) +>>> graphs = [H1, H2, H3, H4] +>>> caps = ["Path 4", "Complete graph 4", "Path 8", "Complete graph 8"] +>>> lbls = ["fig2a", "fig2b", "fig2c", "fig2d"] +>>> nx.write_latex(graphs, "subfigs.tex", n_rows=2, sub_captions=caps, sub_labels=lbls) +>>> latex_code = nx.to_latex(graphs, n_rows=2, sub_captions=caps, sub_labels=lbls) + +>>> node_color = {0: "red", 1: "orange", 2: "blue", 3: "gray!90"} +>>> edge_width = {e: "line width=1.5" for e in H3.edges} +>>> pos = nx.circular_layout(H3) +>>> latex_code = nx.to_latex(H3, pos, node_options=node_color, edge_options=edge_width) +>>> print(latex_code) +\documentclass{report} +\usepackage{tikz} +\usepackage{subcaption} + +\begin{document} +\begin{figure} + \begin{tikzpicture} + \draw + (1.0, 0.0) node[red] (0){0} + (0.707, 0.707) node[orange] (1){1} + (-0.0, 1.0) node[blue] (2){2} + (-0.707, 0.707) node[gray!90] (3){3} + (-1.0, -0.0) node (4){4} + (-0.707, -0.707) node (5){5} + (0.0, -1.0) node (6){6} + (0.707, -0.707) node (7){7}; + \begin{scope}[-] + \draw[line width=1.5] (0) to (1); + \draw[line width=1.5] (1) to (2); + \draw[line width=1.5] (2) to (3); + \draw[line width=1.5] (3) to (4); + \draw[line width=1.5] (4) to (5); + \draw[line width=1.5] (5) to (6); + \draw[line width=1.5] (6) to (7); + \end{scope} + \end{tikzpicture} +\end{figure} +\end{document} + +Notes +----- +If you want to change the preamble/postamble of the figure/document/subfigure +environment, use the keyword arguments: `figure_wrapper`, `document_wrapper`, +`subfigure_wrapper`. The default values are stored in private variables +e.g. ``nx.nx_layout._DOCUMENT_WRAPPER`` + +References +---------- +TikZ: https://tikz.dev/ + +TikZ options details: https://tikz.dev/tikz-actions +""" + +import networkx as nx + +__all__ = [ + "to_latex_raw", + "to_latex", + "write_latex", +] + + +@nx.utils.not_implemented_for("multigraph") +def to_latex_raw( + G, + pos="pos", + tikz_options="", + default_node_options="", + node_options="node_options", + node_label="label", + default_edge_options="", + edge_options="edge_options", + edge_label="label", + edge_label_options="edge_label_options", +): + """Return a string of the LaTeX/TikZ code to draw `G` + + This function produces just the code for the tikzpicture + without any enclosing environment. + + Parameters + ========== + G : NetworkX graph + The NetworkX graph to be drawn + pos : string or dict (default "pos") + The name of the node attribute on `G` that holds the position of each node. + Positions can be sequences of length 2 with numbers for (x,y) coordinates. + They can also be strings to denote positions in TikZ style, such as (x, y) + or (angle:radius). + If a dict, it should be keyed by node to a position. + If an empty dict, a circular layout is computed by TikZ. + tikz_options : string + The tikzpicture options description defining the options for the picture. + Often large scale options like `[scale=2]`. + default_node_options : string + The draw options for a path of nodes. Individual node options override these. + node_options : string or dict + The name of the node attribute on `G` that holds the options for each node. + Or a dict keyed by node to a string holding the options for that node. + node_label : string or dict + The name of the node attribute on `G` that holds the node label (text) + displayed for each node. If the attribute is "" or not present, the node + itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed. + Or a dict keyed by node to a string holding the label for that node. + default_edge_options : string + The options for the scope drawing all edges. The default is "[-]" for + undirected graphs and "[->]" for directed graphs. + edge_options : string or dict + The name of the edge attribute on `G` that holds the options for each edge. + If the edge is a self-loop and ``"loop" not in edge_options`` the option + "loop," is added to the options for the self-loop edge. Hence you can + use "[loop above]" explicitly, but the default is "[loop]". + Or a dict keyed by edge to a string holding the options for that edge. + edge_label : string or dict + The name of the edge attribute on `G` that holds the edge label (text) + displayed for each edge. If the attribute is "" or not present, no edge + label is drawn. + Or a dict keyed by edge to a string holding the label for that edge. + edge_label_options : string or dict + The name of the edge attribute on `G` that holds the label options for + each edge. For example, "[sloped,above,blue]". The default is no options. + Or a dict keyed by edge to a string holding the label options for that edge. + + Returns + ======= + latex_code : string + The text string which draws the desired graph(s) when compiled by LaTeX. + + See Also + ======== + to_latex + write_latex + """ + i4 = "\n " + i8 = "\n " + + # set up position dict + # TODO allow pos to be None and use a nice TikZ default + if not isinstance(pos, dict): + pos = nx.get_node_attributes(G, pos) + if not pos: + # circular layout with radius 2 + pos = {n: f"({round(360.0 * i / len(G), 3)}:2)" for i, n in enumerate(G)} + for node in G: + if node not in pos: + raise nx.NetworkXError(f"node {node} has no specified pos {pos}") + posnode = pos[node] + if not isinstance(posnode, str): + try: + posx, posy = posnode + pos[node] = f"({round(posx, 3)}, {round(posy, 3)})" + except (TypeError, ValueError): + msg = f"position pos[{node}] is not 2-tuple or a string: {posnode}" + raise nx.NetworkXError(msg) + + # set up all the dicts + if not isinstance(node_options, dict): + node_options = nx.get_node_attributes(G, node_options) + if not isinstance(node_label, dict): + node_label = nx.get_node_attributes(G, node_label) + if not isinstance(edge_options, dict): + edge_options = nx.get_edge_attributes(G, edge_options) + if not isinstance(edge_label, dict): + edge_label = nx.get_edge_attributes(G, edge_label) + if not isinstance(edge_label_options, dict): + edge_label_options = nx.get_edge_attributes(G, edge_label_options) + + # process default options (add brackets or not) + topts = "" if tikz_options == "" else f"[{tikz_options.strip('[]')}]" + defn = "" if default_node_options == "" else f"[{default_node_options.strip('[]')}]" + linestyle = f"{'->' if G.is_directed() else '-'}" + if default_edge_options == "": + defe = "[" + linestyle + "]" + elif "-" in default_edge_options: + defe = default_edge_options + else: + defe = f"[{linestyle},{default_edge_options.strip('[]')}]" + + # Construct the string line by line + result = " \\begin{tikzpicture}" + topts + result += i4 + " \\draw" + defn + # load the nodes + for n in G: + # node options goes inside square brackets + nopts = f"[{node_options[n].strip('[]')}]" if n in node_options else "" + # node text goes inside curly brackets {} + ntext = f"{{{node_label[n]}}}" if n in node_label else f"{{{n}}}" + + result += i8 + f"{pos[n]} node{nopts} ({n}){ntext}" + result += ";\n" + + # load the edges + result += " \\begin{scope}" + defe + for edge in G.edges: + u, v = edge[:2] + e_opts = f"{edge_options[edge]}".strip("[]") if edge in edge_options else "" + # add loop options for selfloops if not present + if u == v and "loop" not in e_opts: + e_opts = "loop," + e_opts + e_opts = f"[{e_opts}]" if e_opts != "" else "" + # TODO -- handle bending of multiedges + + els = edge_label_options[edge] if edge in edge_label_options else "" + # edge label options goes inside square brackets [] + els = f"[{els.strip('[]')}]" + # edge text is drawn using the TikZ node command inside curly brackets {} + e_label = f" node{els} {{{edge_label[edge]}}}" if edge in edge_label else "" + + result += i8 + f"\\draw{e_opts} ({u}) to{e_label} ({v});" + + result += "\n \\end{scope}\n \\end{tikzpicture}\n" + return result + + +_DOC_WRAPPER_TIKZ = r"""\documentclass{{report}} +\usepackage{{tikz}} +\usepackage{{subcaption}} + +\begin{{document}} +{content} +\end{{document}}""" + + +_FIG_WRAPPER = r"""\begin{{figure}} +{content}{caption}{label} +\end{{figure}}""" + + +_SUBFIG_WRAPPER = r""" \begin{{subfigure}}{{{size}\textwidth}} +{content}{caption}{label} + \end{{subfigure}}""" + + +def to_latex( + Gbunch, + pos="pos", + tikz_options="", + default_node_options="", + node_options="node_options", + node_label="node_label", + default_edge_options="", + edge_options="edge_options", + edge_label="edge_label", + edge_label_options="edge_label_options", + caption="", + latex_label="", + sub_captions=None, + sub_labels=None, + n_rows=1, + as_document=True, + document_wrapper=_DOC_WRAPPER_TIKZ, + figure_wrapper=_FIG_WRAPPER, + subfigure_wrapper=_SUBFIG_WRAPPER, +): + """Return latex code to draw the graph(s) in `Gbunch` + + The TikZ drawing utility in LaTeX is used to draw the graph(s). + If `Gbunch` is a graph, it is drawn in a figure environment. + If `Gbunch` is an iterable of graphs, each is drawn in a subfigure environment + within a single figure environment. + + If `as_document` is True, the figure is wrapped inside a document environment + so that the resulting string is ready to be compiled by LaTeX. Otherwise, + the string is ready for inclusion in a larger tex document using ``\\include`` + or ``\\input`` statements. + + Parameters + ========== + Gbunch : NetworkX graph or iterable of NetworkX graphs + The NetworkX graph to be drawn or an iterable of graphs + to be drawn inside subfigures of a single figure. + pos : string or list of strings + The name of the node attribute on `G` that holds the position of each node. + Positions can be sequences of length 2 with numbers for (x,y) coordinates. + They can also be strings to denote positions in TikZ style, such as (x, y) + or (angle:radius). + If a dict, it should be keyed by node to a position. + If an empty dict, a circular layout is computed by TikZ. + If you are drawing many graphs in subfigures, use a list of position dicts. + tikz_options : string + The tikzpicture options description defining the options for the picture. + Often large scale options like `[scale=2]`. + default_node_options : string + The draw options for a path of nodes. Individual node options override these. + node_options : string or dict + The name of the node attribute on `G` that holds the options for each node. + Or a dict keyed by node to a string holding the options for that node. + node_label : string or dict + The name of the node attribute on `G` that holds the node label (text) + displayed for each node. If the attribute is "" or not present, the node + itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed. + Or a dict keyed by node to a string holding the label for that node. + default_edge_options : string + The options for the scope drawing all edges. The default is "[-]" for + undirected graphs and "[->]" for directed graphs. + edge_options : string or dict + The name of the edge attribute on `G` that holds the options for each edge. + If the edge is a self-loop and ``"loop" not in edge_options`` the option + "loop," is added to the options for the self-loop edge. Hence you can + use "[loop above]" explicitly, but the default is "[loop]". + Or a dict keyed by edge to a string holding the options for that edge. + edge_label : string or dict + The name of the edge attribute on `G` that holds the edge label (text) + displayed for each edge. If the attribute is "" or not present, no edge + label is drawn. + Or a dict keyed by edge to a string holding the label for that edge. + edge_label_options : string or dict + The name of the edge attribute on `G` that holds the label options for + each edge. For example, "[sloped,above,blue]". The default is no options. + Or a dict keyed by edge to a string holding the label options for that edge. + caption : string + The caption string for the figure environment + latex_label : string + The latex label used for the figure for easy referral from the main text + sub_captions : list of strings + The sub_caption string for each subfigure in the figure + sub_latex_labels : list of strings + The latex label for each subfigure in the figure + n_rows : int + The number of rows of subfigures to arrange for multiple graphs + as_document : bool + Whether to wrap the latex code in a document environment for compiling + document_wrapper : formatted text string with variable ``content``. + This text is called to evaluate the content embedded in a document + environment with a preamble setting up TikZ. + figure_wrapper : formatted text string + This text is evaluated with variables ``content``, ``caption`` and ``label``. + It wraps the content and if a caption is provided, adds the latex code for + that caption, and if a label is provided, adds the latex code for a label. + subfigure_wrapper : formatted text string + This text evaluate variables ``size``, ``content``, ``caption`` and ``label``. + It wraps the content and if a caption is provided, adds the latex code for + that caption, and if a label is provided, adds the latex code for a label. + The size is the vertical size of each row of subfigures as a fraction. + + Returns + ======= + latex_code : string + The text string which draws the desired graph(s) when compiled by LaTeX. + + See Also + ======== + write_latex + to_latex_raw + """ + if hasattr(Gbunch, "adj"): + raw = to_latex_raw( + Gbunch, + pos, + tikz_options, + default_node_options, + node_options, + node_label, + default_edge_options, + edge_options, + edge_label, + edge_label_options, + ) + else: # iterator of graphs + sbf = subfigure_wrapper + size = 1 / n_rows + + N = len(Gbunch) + if isinstance(pos, str | dict): + pos = [pos] * N + if sub_captions is None: + sub_captions = [""] * N + if sub_labels is None: + sub_labels = [""] * N + if not (len(Gbunch) == len(pos) == len(sub_captions) == len(sub_labels)): + raise nx.NetworkXError( + "length of Gbunch, sub_captions and sub_figures must agree" + ) + + raw = "" + for G, pos, subcap, sublbl in zip(Gbunch, pos, sub_captions, sub_labels): + subraw = to_latex_raw( + G, + pos, + tikz_options, + default_node_options, + node_options, + node_label, + default_edge_options, + edge_options, + edge_label, + edge_label_options, + ) + cap = f" \\caption{{{subcap}}}" if subcap else "" + lbl = f"\\label{{{sublbl}}}" if sublbl else "" + raw += sbf.format(size=size, content=subraw, caption=cap, label=lbl) + raw += "\n" + + # put raw latex code into a figure environment and optionally into a document + raw = raw[:-1] + cap = f"\n \\caption{{{caption}}}" if caption else "" + lbl = f"\\label{{{latex_label}}}" if latex_label else "" + fig = figure_wrapper.format(content=raw, caption=cap, label=lbl) + if as_document: + return document_wrapper.format(content=fig) + return fig + + +@nx.utils.open_file(1, mode="w") +def write_latex(Gbunch, path, **options): + """Write the latex code to draw the graph(s) onto `path`. + + This convenience function creates the latex drawing code as a string + and writes that to a file ready to be compiled when `as_document` is True + or ready to be ``import`` ed or ``include`` ed into your main LaTeX document. + + The `path` argument can be a string filename or a file handle to write to. + + Parameters + ---------- + Gbunch : NetworkX graph or iterable of NetworkX graphs + If Gbunch is a graph, it is drawn in a figure environment. + If Gbunch is an iterable of graphs, each is drawn in a subfigure + environment within a single figure environment. + path : string or file + Filename or file handle to write to. + Filenames ending in .gz or .bz2 will be compressed. + options : dict + By default, TikZ is used with options: (others are ignored):: + + pos : string or dict or list + The name of the node attribute on `G` that holds the position of each node. + Positions can be sequences of length 2 with numbers for (x,y) coordinates. + They can also be strings to denote positions in TikZ style, such as (x, y) + or (angle:radius). + If a dict, it should be keyed by node to a position. + If an empty dict, a circular layout is computed by TikZ. + If you are drawing many graphs in subfigures, use a list of position dicts. + tikz_options : string + The tikzpicture options description defining the options for the picture. + Often large scale options like `[scale=2]`. + default_node_options : string + The draw options for a path of nodes. Individual node options override these. + node_options : string or dict + The name of the node attribute on `G` that holds the options for each node. + Or a dict keyed by node to a string holding the options for that node. + node_label : string or dict + The name of the node attribute on `G` that holds the node label (text) + displayed for each node. If the attribute is "" or not present, the node + itself is drawn as a string. LaTeX processing such as ``"$A_1$"`` is allowed. + Or a dict keyed by node to a string holding the label for that node. + default_edge_options : string + The options for the scope drawing all edges. The default is "[-]" for + undirected graphs and "[->]" for directed graphs. + edge_options : string or dict + The name of the edge attribute on `G` that holds the options for each edge. + If the edge is a self-loop and ``"loop" not in edge_options`` the option + "loop," is added to the options for the self-loop edge. Hence you can + use "[loop above]" explicitly, but the default is "[loop]". + Or a dict keyed by edge to a string holding the options for that edge. + edge_label : string or dict + The name of the edge attribute on `G` that holds the edge label (text) + displayed for each edge. If the attribute is "" or not present, no edge + label is drawn. + Or a dict keyed by edge to a string holding the label for that edge. + edge_label_options : string or dict + The name of the edge attribute on `G` that holds the label options for + each edge. For example, "[sloped,above,blue]". The default is no options. + Or a dict keyed by edge to a string holding the label options for that edge. + caption : string + The caption string for the figure environment + latex_label : string + The latex label used for the figure for easy referral from the main text + sub_captions : list of strings + The sub_caption string for each subfigure in the figure + sub_latex_labels : list of strings + The latex label for each subfigure in the figure + n_rows : int + The number of rows of subfigures to arrange for multiple graphs + as_document : bool + Whether to wrap the latex code in a document environment for compiling + document_wrapper : formatted text string with variable ``content``. + This text is called to evaluate the content embedded in a document + environment with a preamble setting up the TikZ syntax. + figure_wrapper : formatted text string + This text is evaluated with variables ``content``, ``caption`` and ``label``. + It wraps the content and if a caption is provided, adds the latex code for + that caption, and if a label is provided, adds the latex code for a label. + subfigure_wrapper : formatted text string + This text evaluate variables ``size``, ``content``, ``caption`` and ``label``. + It wraps the content and if a caption is provided, adds the latex code for + that caption, and if a label is provided, adds the latex code for a label. + The size is the vertical size of each row of subfigures as a fraction. + + See Also + ======== + to_latex + """ + path.write(to_latex(Gbunch, **options)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pydot.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pydot.py new file mode 100644 index 0000000000000000000000000000000000000000..0fe5ceec97c5c3c85e5e64e05b4c02eb83978c3a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pydot.py @@ -0,0 +1,361 @@ +""" +***** +Pydot +***** + +Import and export NetworkX graphs in Graphviz dot format using pydot. + +Either this module or nx_agraph can be used to interface with graphviz. + +Examples +-------- +>>> G = nx.complete_graph(5) +>>> PG = nx.nx_pydot.to_pydot(G) +>>> H = nx.nx_pydot.from_pydot(PG) + +See Also +-------- + - pydot: https://github.com/erocarrera/pydot + - Graphviz: https://www.graphviz.org + - DOT Language: http://www.graphviz.org/doc/info/lang.html +""" + +from locale import getpreferredencoding + +import networkx as nx +from networkx.utils import open_file + +__all__ = [ + "write_dot", + "read_dot", + "graphviz_layout", + "pydot_layout", + "to_pydot", + "from_pydot", +] + + +@open_file(1, mode="w") +def write_dot(G, path): + """Write NetworkX graph G to Graphviz dot format on path. + + Parameters + ---------- + G : NetworkX graph + + path : string or file + Filename or file handle for data output. + Filenames ending in .gz or .bz2 will be compressed. + """ + P = to_pydot(G) + path.write(P.to_string()) + return + + +@open_file(0, mode="r") +@nx._dispatchable(name="pydot_read_dot", graphs=None, returns_graph=True) +def read_dot(path): + """Returns a NetworkX :class:`MultiGraph` or :class:`MultiDiGraph` from the + dot file with the passed path. + + If this file contains multiple graphs, only the first such graph is + returned. All graphs _except_ the first are silently ignored. + + Parameters + ---------- + path : str or file + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + G : MultiGraph or MultiDiGraph + A :class:`MultiGraph` or :class:`MultiDiGraph`. + + Notes + ----- + Use `G = nx.Graph(nx.nx_pydot.read_dot(path))` to return a :class:`Graph` instead of a + :class:`MultiGraph`. + """ + import pydot + + data = path.read() + + # List of one or more "pydot.Dot" instances deserialized from this file. + P_list = pydot.graph_from_dot_data(data) + + # Convert only the first such instance into a NetworkX graph. + return from_pydot(P_list[0]) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def from_pydot(P): + """Returns a NetworkX graph from a Pydot graph. + + Parameters + ---------- + P : Pydot graph + A graph created with Pydot + + Returns + ------- + G : NetworkX multigraph + A MultiGraph or MultiDiGraph. + + Examples + -------- + >>> K5 = nx.complete_graph(5) + >>> A = nx.nx_pydot.to_pydot(K5) + >>> G = nx.nx_pydot.from_pydot(A) # return MultiGraph + + # make a Graph instead of MultiGraph + >>> G = nx.Graph(nx.nx_pydot.from_pydot(A)) + + """ + # NOTE: Pydot v3 expects a dummy argument whereas Pydot v4 doesn't + # Remove the try-except when Pydot v4 becomes the minimum supported version + try: + strict = P.get_strict() + except TypeError: + strict = P.get_strict(None) # pydot bug: get_strict() shouldn't take argument + multiedges = not strict + + if P.get_type() == "graph": # undirected + if multiedges: + N = nx.MultiGraph() + else: + N = nx.Graph() + else: + if multiedges: + N = nx.MultiDiGraph() + else: + N = nx.DiGraph() + + # assign defaults + name = P.get_name().strip('"') + if name != "": + N.name = name + + # add nodes, attributes to N.node_attr + for p in P.get_node_list(): + n = p.get_name().strip('"') + if n in ("node", "graph", "edge"): + continue + N.add_node(n, **p.get_attributes()) + + # add edges + for e in P.get_edge_list(): + u = e.get_source() + v = e.get_destination() + attr = e.get_attributes() + s = [] + d = [] + + if isinstance(u, str): + s.append(u.strip('"')) + else: + for unodes in u["nodes"]: + s.append(unodes.strip('"')) + + if isinstance(v, str): + d.append(v.strip('"')) + else: + for vnodes in v["nodes"]: + d.append(vnodes.strip('"')) + + for source_node in s: + for destination_node in d: + N.add_edge(source_node, destination_node, **attr) + + # add default attributes for graph, nodes, edges + pattr = P.get_attributes() + if pattr: + N.graph["graph"] = pattr + try: + N.graph["node"] = P.get_node_defaults()[0] + except (IndexError, TypeError): + pass # N.graph['node']={} + try: + N.graph["edge"] = P.get_edge_defaults()[0] + except (IndexError, TypeError): + pass # N.graph['edge']={} + return N + + +def to_pydot(N): + """Returns a pydot graph from a NetworkX graph N. + + Parameters + ---------- + N : NetworkX graph + A graph created with NetworkX + + Examples + -------- + >>> K5 = nx.complete_graph(5) + >>> P = nx.nx_pydot.to_pydot(K5) + + Notes + ----- + + """ + import pydot + + # set Graphviz graph type + if N.is_directed(): + graph_type = "digraph" + else: + graph_type = "graph" + strict = nx.number_of_selfloops(N) == 0 and not N.is_multigraph() + + name = N.name + graph_defaults = N.graph.get("graph", {}) + if name == "": + P = pydot.Dot("", graph_type=graph_type, strict=strict, **graph_defaults) + else: + P = pydot.Dot( + f'"{name}"', graph_type=graph_type, strict=strict, **graph_defaults + ) + try: + P.set_node_defaults(**N.graph["node"]) + except KeyError: + pass + try: + P.set_edge_defaults(**N.graph["edge"]) + except KeyError: + pass + + for n, nodedata in N.nodes(data=True): + str_nodedata = {str(k): str(v) for k, v in nodedata.items()} + n = str(n) + p = pydot.Node(n, **str_nodedata) + P.add_node(p) + + if N.is_multigraph(): + for u, v, key, edgedata in N.edges(data=True, keys=True): + str_edgedata = {str(k): str(v) for k, v in edgedata.items() if k != "key"} + u, v = str(u), str(v) + edge = pydot.Edge(u, v, key=str(key), **str_edgedata) + P.add_edge(edge) + + else: + for u, v, edgedata in N.edges(data=True): + str_edgedata = {str(k): str(v) for k, v in edgedata.items()} + u, v = str(u), str(v) + edge = pydot.Edge(u, v, **str_edgedata) + P.add_edge(edge) + return P + + +def graphviz_layout(G, prog="neato", root=None): + """Create node positions using Pydot and Graphviz. + + Returns a dictionary of positions keyed by node. + + Parameters + ---------- + G : NetworkX Graph + The graph for which the layout is computed. + prog : string (default: 'neato') + The name of the GraphViz program to use for layout. + Options depend on GraphViz version but may include: + 'dot', 'twopi', 'fdp', 'sfdp', 'circo' + root : Node from G or None (default: None) + The node of G from which to start some layout algorithms. + + Returns + ------- + Dictionary of (x, y) positions keyed by node. + + Examples + -------- + >>> G = nx.complete_graph(4) + >>> pos = nx.nx_pydot.graphviz_layout(G) + >>> pos = nx.nx_pydot.graphviz_layout(G, prog="dot") + + Notes + ----- + This is a wrapper for pydot_layout. + """ + return pydot_layout(G=G, prog=prog, root=root) + + +def pydot_layout(G, prog="neato", root=None): + """Create node positions using :mod:`pydot` and Graphviz. + + Parameters + ---------- + G : Graph + NetworkX graph to be laid out. + prog : string (default: 'neato') + Name of the GraphViz command to use for layout. + Options depend on GraphViz version but may include: + 'dot', 'twopi', 'fdp', 'sfdp', 'circo' + root : Node from G or None (default: None) + The node of G from which to start some layout algorithms. + + Returns + ------- + dict + Dictionary of positions keyed by node. + + Examples + -------- + >>> G = nx.complete_graph(4) + >>> pos = nx.nx_pydot.pydot_layout(G) + >>> pos = nx.nx_pydot.pydot_layout(G, prog="dot") + + Notes + ----- + If you use complex node objects, they may have the same string + representation and GraphViz could treat them as the same node. + The layout may assign both nodes a single location. See Issue #1568 + If this occurs in your case, consider relabeling the nodes just + for the layout computation using something similar to:: + + H = nx.convert_node_labels_to_integers(G, label_attribute="node_label") + H_layout = nx.nx_pydot.pydot_layout(H, prog="dot") + G_layout = {H.nodes[n]["node_label"]: p for n, p in H_layout.items()} + + """ + import pydot + + P = to_pydot(G) + if root is not None: + P.set("root", str(root)) + + # List of low-level bytes comprising a string in the dot language converted + # from the passed graph with the passed external GraphViz command. + D_bytes = P.create_dot(prog=prog) + + # Unique string decoded from these bytes with the preferred locale encoding + D = str(D_bytes, encoding=getpreferredencoding()) + + if D == "": # no data returned + print(f"Graphviz layout with {prog} failed") + print() + print("To debug what happened try:") + print("P = nx.nx_pydot.to_pydot(G)") + print('P.write_dot("file.dot")') + print(f"And then run {prog} on file.dot") + return + + # List of one or more "pydot.Dot" instances deserialized from this string. + Q_list = pydot.graph_from_dot_data(D) + assert len(Q_list) == 1 + + # The first and only such instance, as guaranteed by the above assertion. + Q = Q_list[0] + + node_pos = {} + for n in G.nodes(): + str_n = str(n) + node = Q.get_node(pydot.quote_id_if_necessary(str_n)) + + if isinstance(node, list): + node = node[0] + pos = node.get_pos()[1:-1] # strip leading and trailing double quotes + if pos is not None: + xx, yy = pos.split(",") + node_pos[n] = (float(xx), float(yy)) + return node_pos diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pylab.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pylab.py new file mode 100644 index 0000000000000000000000000000000000000000..143b0a7590811054dd92a1757c64e4bdeb10f320 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/nx_pylab.py @@ -0,0 +1,2978 @@ +""" +********** +Matplotlib +********** + +Draw networks with matplotlib. + +Examples +-------- +>>> G = nx.complete_graph(5) +>>> nx.draw(G) + +See Also +-------- + - :doc:`matplotlib ` + - :func:`matplotlib.pyplot.scatter` + - :obj:`matplotlib.patches.FancyArrowPatch` +""" + +import collections +import itertools +import math +from numbers import Number + +import networkx as nx + +__all__ = [ + "display", + "apply_matplotlib_colors", + "draw", + "draw_networkx", + "draw_networkx_nodes", + "draw_networkx_edges", + "draw_networkx_labels", + "draw_networkx_edge_labels", + "draw_bipartite", + "draw_circular", + "draw_kamada_kawai", + "draw_random", + "draw_spectral", + "draw_spring", + "draw_planar", + "draw_shell", + "draw_forceatlas2", +] + + +def apply_matplotlib_colors( + G, src_attr, dest_attr, map, vmin=None, vmax=None, nodes=True +): + """ + Apply colors from a matplotlib colormap to a graph. + + Reads values from the `src_attr` and use a matplotlib colormap + to produce a color. Write the color to `dest_attr`. + + Parameters + ---------- + G : nx.Graph + The graph to read and compute colors for. + + src_attr : str or other attribute name + The name of the attribute to read from the graph. + + dest_attr : str or other attribute name + The name of the attribute to write to on the graph. + + map : matplotlib.colormap + The matplotlib colormap to use. + + vmin : float, default None + The minimum value for scaling the colormap. If `None`, find the + minimum value of `src_attr`. + + vmax : float, default None + The maximum value for scaling the colormap. If `None`, find the + maximum value of `src_attr`. + + nodes : bool, default True + Whether the attribute names are edge attributes or node attributes. + """ + import matplotlib as mpl + + if nodes: + type_iter = G.nodes() + elif G.is_multigraph(): + type_iter = G.edges(keys=True) + else: + type_iter = G.edges() + + if vmin is None or vmax is None: + vals = [type_iter[a][src_attr] for a in type_iter] + if vmin is None: + vmin = min(vals) + if vmax is None: + vmax = max(vals) + + mapper = mpl.cm.ScalarMappable(cmap=map) + mapper.set_clim(vmin, vmax) + + def do_map(x): + # Cast numpy scalars to float + return tuple(float(x) for x in mapper.to_rgba(x)) + + if nodes: + nx.set_node_attributes( + G, {n: do_map(G.nodes[n][src_attr]) for n in G.nodes()}, dest_attr + ) + else: + nx.set_edge_attributes( + G, {e: do_map(G.edges[e][src_attr]) for e in type_iter}, dest_attr + ) + + +class CurvedArrowTextBase: + def __init__( + self, + arrow, + *args, + label_pos=0.5, + labels_horizontal=False, + ax=None, + **kwargs, + ): + # Bind to FancyArrowPatch + self.arrow = arrow + # how far along the text should be on the curve, + # 0 is at start, 1 is at end etc. + self.label_pos = label_pos + self.labels_horizontal = labels_horizontal + if ax is None: + ax = plt.gca() + self.ax = ax + self.x, self.y, self.angle = self._update_text_pos_angle(arrow) + + # Create text object + super().__init__(self.x, self.y, *args, rotation=self.angle, **kwargs) + # Bind to axis + self.ax.add_artist(self) + + def _get_arrow_path_disp(self, arrow): + """ + This is part of FancyArrowPatch._get_path_in_displaycoord + It omits the second part of the method where path is converted + to polygon based on width + The transform is taken from ax, not the object, as the object + has not been added yet, and doesn't have transform + """ + dpi_cor = arrow._dpi_cor + trans_data = self.ax.transData + if arrow._posA_posB is None: + raise ValueError( + "Can only draw labels for fancy arrows with " + "posA and posB inputs, not custom path" + ) + posA = arrow._convert_xy_units(arrow._posA_posB[0]) + posB = arrow._convert_xy_units(arrow._posA_posB[1]) + (posA, posB) = trans_data.transform((posA, posB)) + _path = arrow.get_connectionstyle()( + posA, + posB, + patchA=arrow.patchA, + patchB=arrow.patchB, + shrinkA=arrow.shrinkA * dpi_cor, + shrinkB=arrow.shrinkB * dpi_cor, + ) + # Return is in display coordinates + return _path + + def _update_text_pos_angle(self, arrow): + # Fractional label position + # Text position at a proportion t along the line in display coords + # default is 0.5 so text appears at the halfway point + import matplotlib as mpl + import numpy as np + + t = self.label_pos + tt = 1 - t + path_disp = self._get_arrow_path_disp(arrow) + conn = arrow.get_connectionstyle() + # 1. Calculate x and y + points = path_disp.vertices + if is_curve := isinstance( + conn, + mpl.patches.ConnectionStyle.Angle3 | mpl.patches.ConnectionStyle.Arc3, + ): + # Arc3 or Angle3 type Connection Styles - Bezier curve + (x1, y1), (cx, cy), (x2, y2) = points + x = tt**2 * x1 + 2 * t * tt * cx + t**2 * x2 + y = tt**2 * y1 + 2 * t * tt * cy + t**2 * y2 + else: + if not isinstance( + conn, + mpl.patches.ConnectionStyle.Angle + | mpl.patches.ConnectionStyle.Arc + | mpl.patches.ConnectionStyle.Bar, + ): + msg = f"invalid connection style: {type(conn)}" + raise TypeError(msg) + # A. Collect lines + codes = path_disp.codes + lines = [ + points[i - 1 : i + 1] + for i in range(1, len(points)) + if codes[i] == mpl.path.Path.LINETO + ] + # B. If more than one line, find the right one and position in it + if (nlines := len(lines)) != 1: + dists = [math.dist(*line) for line in lines] + dist_tot = sum(dists) + cdist = 0 + last_cut = 0 + i_last = nlines - 1 + for i, dist in enumerate(dists): + cdist += dist + cut = cdist / dist_tot + if i == i_last or t < cut: + t = (t - last_cut) / (dist / dist_tot) + tt = 1 - t + lines = [lines[i]] + break + last_cut = cut + [[(cx1, cy1), (cx2, cy2)]] = lines + x = cx1 * tt + cx2 * t + y = cy1 * tt + cy2 * t + + # 2. Calculate Angle + if self.labels_horizontal: + # Horizontal text labels + angle = 0 + else: + # Labels parallel to curve + if is_curve: + change_x = 2 * tt * (cx - x1) + 2 * t * (x2 - cx) + change_y = 2 * tt * (cy - y1) + 2 * t * (y2 - cy) + else: + change_x = (cx2 - cx1) / 2 + change_y = (cy2 - cy1) / 2 + angle = np.arctan2(change_y, change_x) / (2 * np.pi) * 360 + # Text is "right way up" + if angle > 90: + angle -= 180 + elif angle < -90: + angle += 180 + (x, y) = self.ax.transData.inverted().transform((x, y)) + return x, y, angle + + def draw(self, renderer): + # recalculate the text position and angle + self.x, self.y, self.angle = self._update_text_pos_angle(self.arrow) + self.set_position((self.x, self.y)) + self.set_rotation(self.angle) + # redraw text + super().draw(renderer) + + +def display( + G, + canvas=None, + **kwargs, +): + """Draw the graph G. + + Draw the graph as a collection of nodes connected by edges. + The exact details of what the graph looks like are controlled by the below + attributes. All nodes and nodes at the end of visible edges must have a + position set, but nearly all other node and edge attributes are options and + nodes or edges missing the attribute will use the default listed below. A more + complete description of each parameter is given below this summary. + + .. list-table:: Default Visualization Attributes + :widths: 25 25 50 + :header-rows: 1 + + * - Parameter + - Default Attribute + - Default Value + * - node_pos + - `"pos"` + - If there is not position, a layout will be calculated with `nx.spring_layout`. + * - node_visible + - `"visible"` + - True + * - node_color + - `"color"` + - #1f78b4 + * - node_size + - `"size"` + - 300 + * - node_label + - `"label"` + - Dict describing the node label. Defaults create a black text with + the node name as the label. The dict respects these keys and defaults: + + * size : 12 + * color : black + * family : sans serif + * weight : normal + * alpha : 1.0 + * h_align : center + * v_align : center + * bbox : Dict describing a `matplotlib.patches.FancyBboxPatch`. + Default is None. + + * - node_shape + - `"shape"` + - "o" + * - node_alpha + - `"alpha"` + - 1.0 + * - node_border_width + - `"border_width"` + - 1.0 + * - node_border_color + - `"border_color"` + - Matching node_color + * - edge_visible + - `"visible"` + - True + * - edge_width + - `"width"` + - 1.0 + * - edge_color + - `"color"` + - Black (#000000) + * - edge_label + - `"label"` + - Dict describing the edge label. Defaults create black text with a + white bounding box. The dictionary respects these keys and defaults: + + * size : 12 + * color : black + * family : sans serif + * weight : normal + * alpha : 1.0 + * bbox : Dict describing a `matplotlib.patches.FancyBboxPatch`. + Default {"boxstyle": "round", "ec": (1.0, 1.0, 1.0), "fc": (1.0, 1.0, 1.0)} + * h_align : "center" + * v_align : "center" + * pos : 0.5 + * rotate : True + + * - edge_style + - `"style"` + - "-" + * - edge_alpha + - `"alpha"` + - 1.0 + * - edge_arrowstyle + - `"arrowstyle"` + - ``"-|>"`` if `G` is directed else ``"-"`` + * - edge_arrowsize + - `"arrowsize"` + - 10 if `G` is directed else 0 + * - edge_curvature + - `"curvature"` + - arc3 + * - edge_source_margin + - `"source_margin"` + - 0 + * - edge_target_margin + - `"target_margin"` + - 0 + + Parameters + ---------- + G : graph + A networkx graph + + canvas : Matplotlib Axes object, optional + Draw the graph in specified Matplotlib axes + + node_pos : string or function, default "pos" + A string naming the node attribute storing the position of nodes as a tuple. + Or a function to be called with input `G` which returns the layout as a dict keyed + by node to position tuple like the NetworkX layout functions. + If no nodes in the graph has the attribute, a spring layout is calculated. + + node_visible : string or bool, default visible + A string naming the node attribute which stores if a node should be drawn. + If `True`, all nodes will be visible while if `False` no nodes will be visible. + If incomplete, nodes missing this attribute will be shown by default. + + node_color : string, default "color" + A string naming the node attribute which stores the color of each node. + Visible nodes without this attribute will use '#1f78b4' as a default. + + node_size : string or number, default "size" + A string naming the node attribute which stores the size of each node. + Visible nodes without this attribute will use a default size of 300. + + node_label : string or bool, default "label" + A string naming the node attribute which stores the label of each node. + The attribute value can be a string, False (no label for that node), + True (the node is the label) or a dict keyed by node to the label. + + If a dict is specified, these keys are read to further control the label: + + * label : The text of the label; default: name of the node + * size : Font size of the label; default: 12 + * color : Font color of the label; default: black + * family : Font family of the label; default: "sans-serif" + * weight : Font weight of the label; default: "normal" + * alpha : Alpha value of the label; default: 1.0 + * h_align : The horizontal alignment of the label. + one of "left", "center", "right"; default: "center" + * v_align : The vertical alignment of the label. + one of "top", "center", "bottom"; default: "center" + * bbox : A dict of parameters for `matplotlib.patches.FancyBboxPatch`. + + Visible nodes without this attribute will be treated as if the value was True. + + node_shape : string, default "shape" + A string naming the node attribute which stores the label of each node. + The values of this attribute are expected to be one of the matplotlib shapes, + one of 'so^>v"`` for directed graphs. + + See `matplotlib.patches.ArrowStyle` for more options + + edge_arrowsize : string or int, default "arrowsize" + A string naming the edge attribute which stores the size of the arrowhead for each + edge. Visible edges without this attribute will use a default value of 10. + + edge_curvature : string, default "curvature" + A string naming the edge attribute storing the curvature and connection style + of each edge. Visible edges without this attribute will use "arc3" as a default + value, resulting an a straight line between the two nodes. Curvature can be given + as 'arc3,rad=0.2' to specify both the style and radius of curvature. + + Please see `matplotlib.patches.ConnectionStyle` and + `matplotlib.patches.FancyArrowPatch` for more information. + + edge_source_margin : string or int, default "source_margin" + A string naming the edge attribute which stores the minimum margin (gap) between + the source node and the start of the edge. Visible edges without this attribute + will use a default value of 0. + + edge_target_margin : string or int, default "target_margin" + A string naming the edge attribute which stores the minimumm margin (gap) between + the target node and the end of the edge. Visible edges without this attribute + will use a default value of 0. + + hide_ticks : bool, default True + Weather to remove the ticks from the axes of the matplotlib object. + + Raises + ------ + NetworkXError + If a node or edge is missing a required parameter such as `pos` or + if `display` receives an argument not listed above. + + ValueError + If a node or edge has an invalid color format, i.e. not a color string, + rgb tuple or rgba tuple. + + Returns + ------- + The input graph. This is potentially useful for dispatching visualization + functions. + """ + from collections import Counter + + import matplotlib as mpl + import matplotlib.pyplot as plt + import numpy as np + + defaults = { + "node_pos": None, + "node_visible": True, + "node_color": "#1f78b4", + "node_size": 300, + "node_label": { + "size": 12, + "color": "#000000", + "family": "sans-serif", + "weight": "normal", + "alpha": 1.0, + "h_align": "center", + "v_align": "center", + "bbox": None, + }, + "node_shape": "o", + "node_alpha": 1.0, + "node_border_width": 1.0, + "node_border_color": "face", + "edge_visible": True, + "edge_width": 1.0, + "edge_color": "#000000", + "edge_label": { + "size": 12, + "color": "#000000", + "family": "sans-serif", + "weight": "normal", + "alpha": 1.0, + "bbox": {"boxstyle": "round", "ec": (1.0, 1.0, 1.0), "fc": (1.0, 1.0, 1.0)}, + "h_align": "center", + "v_align": "center", + "pos": 0.5, + "rotate": True, + }, + "edge_style": "-", + "edge_alpha": 1.0, + "edge_arrowstyle": "-|>" if G.is_directed() else "-", + "edge_arrowsize": 10 if G.is_directed() else 0, + "edge_curvature": "arc3", + "edge_source_margin": 0, + "edge_target_margin": 0, + "hide_ticks": True, + } + + # Check arguments + for kwarg in kwargs: + if kwarg not in defaults: + raise nx.NetworkXError( + f"Unrecognized visualization keyword argument: {kwarg}" + ) + + if canvas is None: + canvas = plt.gca() + + if kwargs.get("hide_ticks", defaults["hide_ticks"]): + canvas.tick_params( + axis="both", + which="both", + bottom=False, + left=False, + labelbottom=False, + labelleft=False, + ) + + ### Helper methods and classes + + def node_property_sequence(seq, attr): + """Return a list of attribute values for `seq`, using a default if needed""" + + # All node attribute parameters start with "node_" + param_name = f"node_{attr}" + default = defaults[param_name] + attr = kwargs.get(param_name, attr) + + if default is None: + # raise instead of using non-existant default value + for n in seq: + if attr not in node_subgraph.nodes[n]: + raise nx.NetworkXError(f"Attribute '{attr}' missing for node {n}") + + # If `attr` is not a graph attr and was explicitly passed as an argument + # it must be a user-default value. Allow attr=None to tell draw to skip + # attributes which are on the graph + if ( + attr is not None + and nx.get_node_attributes(node_subgraph, attr) == {} + and any(attr == v for k, v in kwargs.items() if "node" in k) + ): + return [attr for _ in seq] + + return [node_subgraph.nodes[n].get(attr, default) for n in seq] + + def compute_colors(color, alpha): + if isinstance(color, str): + rgba = mpl.colors.colorConverter.to_rgba(color) + # Using a non-default alpha value overrides any alpha value in the color + if alpha != defaults["node_alpha"]: + return (rgba[0], rgba[1], rgba[2], alpha) + return rgba + + if isinstance(color, tuple) and len(color) == 3: + return (color[0], color[1], color[2], alpha) + + if isinstance(color, tuple) and len(color) == 4: + return color + + raise ValueError(f"Invalid format for color: {color}") + + # Find which edges can be plotted as a line collection + # + # Non-default values for these attributes require fancy arrow patches: + # - any arrow style (including the default -|> for directed graphs) + # - arrow size (by extension of style) + # - connection style + # - min_source_margin + # - min_target_margin + + def collection_compatible(e): + return ( + get_edge_attr(e, "arrowstyle") == "-" + and get_edge_attr(e, "curvature") == "arc3" + and get_edge_attr(e, "source_margin") == 0 + and get_edge_attr(e, "target_margin") == 0 + # Self-loops will use fancy arrow patches + and e[0] != e[1] + ) + + def edge_property_sequence(seq, attr): + """Return a list of attribute values for `seq`, using a default if needed""" + + param_name = f"edge_{attr}" + default = defaults[param_name] + attr = kwargs.get(param_name, attr) + + if default is None: + # raise instead of using non-existant default value + for e in seq: + if attr not in edge_subgraph.edges[e]: + raise nx.NetworkXError(f"Attribute '{attr}' missing for edge {e}") + + if ( + attr is not None + and nx.get_edge_attributes(edge_subgraph, attr) == {} + and any(attr == v for k, v in kwargs.items() if "edge" in k) + ): + return [attr for _ in seq] + + return [edge_subgraph.edges[e].get(attr, default) for e in seq] + + def get_edge_attr(e, attr): + """Return the final edge attribute value, using default if not None""" + + param_name = f"edge_{attr}" + default = defaults[param_name] + attr = kwargs.get(param_name, attr) + + if default is None and attr not in edge_subgraph.edges[e]: + raise nx.NetworkXError(f"Attribute '{attr}' missing from edge {e}") + + if ( + attr is not None + and nx.get_edge_attributes(edge_subgraph, attr) == {} + and attr in kwargs.values() + ): + return attr + + return edge_subgraph.edges[e].get(attr, default) + + def get_node_attr(n, attr, use_edge_subgraph=True): + """Return the final node attribute value, using default if not None""" + subgraph = edge_subgraph if use_edge_subgraph else node_subgraph + + param_name = f"node_{attr}" + default = defaults[param_name] + attr = kwargs.get(param_name, attr) + + if default is None and attr not in subgraph.nodes[n]: + raise nx.NetworkXError(f"Attribute '{attr}' missing from node {n}") + + if ( + attr is not None + and nx.get_node_attributes(subgraph, attr) == {} + and attr in kwargs.values() + ): + return attr + + return subgraph.nodes[n].get(attr, default) + + # Taken from ConnectionStyleFactory + def self_loop(edge_index, node_size): + def self_loop_connection(posA, posB, *args, **kwargs): + if not np.all(posA == posB): + raise nx.NetworkXError( + "`self_loop` connection style method" + "is only to be used for self-loops" + ) + # this is called with _screen space_ values + # so convert back to data space + data_loc = canvas.transData.inverted().transform(posA) + # Scale self loop based on the size of the base node + # Size of nodes are given in points ** 2 and each point is 1/72 of an inch + v_shift = np.sqrt(node_size) / 72 + h_shift = v_shift * 0.5 + # put the top of the loop first so arrow is not hidden by node + path = np.asarray( + [ + # 1 + [0, v_shift], + # 4 4 4 + [h_shift, v_shift], + [h_shift, 0], + [0, 0], + # 4 4 4 + [-h_shift, 0], + [-h_shift, v_shift], + [0, v_shift], + ] + ) + # Rotate self loop 90 deg. if more than 1 + # This will allow for maximum of 4 visible self loops + if edge_index % 4: + x, y = path.T + for _ in range(edge_index % 4): + x, y = y, -x + path = np.array([x, y]).T + return mpl.path.Path( + canvas.transData.transform(data_loc + path), [1, 4, 4, 4, 4, 4, 4] + ) + + return self_loop_connection + + def to_marker_edge(size, marker): + if marker in "s^>v 0: + node_shape = kwargs.get("node_shape", "shape") + for shape in Counter( + nx.get_node_attributes( + node_subgraph, node_shape, defaults["node_shape"] + ).values() + ): + # Filter position just on this shape. + nodes_with_shape = [ + n + for n, s in node_subgraph.nodes(data=node_shape) + if s == shape or (s is None and shape == defaults["node_shape"]) + ] + # There are two property sequences to create before hand. + # 1. position, since it is used for x and y parameters to scatter + # 2. edgecolor, since the spaeical 'face' parameter value can only be + # be passed in as the sole string, not part of a list of strings. + position = np.asarray(node_property_sequence(nodes_with_shape, "pos")) + color = np.asarray( + [ + compute_colors(c, a) + for c, a in zip( + node_property_sequence(nodes_with_shape, "color"), + node_property_sequence(nodes_with_shape, "alpha"), + ) + ] + ) + border_color = np.asarray( + [ + ( + c + if ( + c := get_node_attr( + n, + "border_color", + False, + ) + ) + != "face" + else color[i] + ) + for i, n in enumerate(nodes_with_shape) + ] + ) + canvas.scatter( + position[:, 0], + position[:, 1], + s=node_property_sequence(nodes_with_shape, "size"), + c=color, + marker=shape, + linewidths=node_property_sequence(nodes_with_shape, "border_width"), + edgecolors=border_color, + zorder=2, + ) + + ### Draw node labels + node_label = kwargs.get("node_label", "label") + # Plot labels if node_label is not None and not False + if node_label is not None and node_label is not False: + default_dict = {} + if isinstance(node_label, dict): + default_dict = node_label + node_label = None + + for n, lbl in node_subgraph.nodes(data=node_label): + if lbl is False: + continue + + # We work with label dicts down here... + if not isinstance(lbl, dict): + lbl = {"label": lbl if lbl is not None else n} + + lbl_text = lbl.get("label", n) + if not isinstance(lbl_text, str): + lbl_text = str(lbl_text) + + lbl.update(default_dict) + x, y = node_subgraph.nodes[n][pos] + canvas.text( + x, + y, + lbl_text, + size=lbl.get("size", defaults["node_label"]["size"]), + color=lbl.get("color", defaults["node_label"]["color"]), + family=lbl.get("family", defaults["node_label"]["family"]), + weight=lbl.get("weight", defaults["node_label"]["weight"]), + horizontalalignment=lbl.get( + "h_align", defaults["node_label"]["h_align"] + ), + verticalalignment=lbl.get("v_align", defaults["node_label"]["v_align"]), + transform=canvas.transData, + bbox=lbl.get("bbox", defaults["node_label"]["bbox"]), + ) + + ### Draw edges + + edge_visible = kwargs.get("edge_visible", "visible") + if isinstance(edge_visible, bool): + if edge_visible: + visible_edges = G.edges() + else: + visible_edges = [] + else: + visible_edges = [ + e for e, v in nx.get_edge_attributes(G, edge_visible, True).items() if v + ] + + edge_subgraph = G.edge_subgraph(visible_edges) + nx.set_node_attributes( + edge_subgraph, nx.get_node_attributes(node_subgraph, pos), name=pos + ) + + collection_edges = ( + [e for e in edge_subgraph.edges(keys=True) if collection_compatible(e)] + if edge_subgraph.is_multigraph() + else [e for e in edge_subgraph.edges() if collection_compatible(e)] + ) + non_collection_edges = ( + [e for e in edge_subgraph.edges(keys=True) if not collection_compatible(e)] + if edge_subgraph.is_multigraph() + else [e for e in edge_subgraph.edges() if not collection_compatible(e)] + ) + edge_position = np.asarray( + [ + ( + get_node_attr(u, "pos", use_edge_subgraph=True), + get_node_attr(v, "pos", use_edge_subgraph=True), + ) + for u, v, *_ in collection_edges + ] + ) + + # Only plot a line collection if needed + if len(collection_edges) > 0: + edge_collection = mpl.collections.LineCollection( + edge_position, + colors=edge_property_sequence(collection_edges, "color"), + linewidths=edge_property_sequence(collection_edges, "width"), + linestyle=edge_property_sequence(collection_edges, "style"), + alpha=edge_property_sequence(collection_edges, "alpha"), + antialiaseds=(1,), + zorder=1, + ) + canvas.add_collection(edge_collection) + + fancy_arrows = {} + if len(non_collection_edges) > 0: + for e in non_collection_edges: + # Cache results for use in edge labels + fancy_arrows[e] = build_fancy_arrow(e) + canvas.add_patch(fancy_arrows[e]) + + ### Draw edge labels + edge_label = kwargs.get("edge_label", "label") + default_dict = {} + if isinstance(edge_label, dict): + default_dict = edge_label + # Restore the default label attribute key of 'label' + edge_label = "label" + + # Handle multigraphs + edge_label_data = ( + edge_subgraph.edges(data=edge_label, keys=True) + if edge_subgraph.is_multigraph() + else edge_subgraph.edges(data=edge_label) + ) + if edge_label is not None and edge_label is not False: + for *e, lbl in edge_label_data: + e = tuple(e) + # I'm not sure how I want to handle None here... For now it means no label + if lbl is False or lbl is None: + continue + + if not isinstance(lbl, dict): + lbl = {"label": lbl} + + lbl.update(default_dict) + lbl_text = lbl.get("label") + if not isinstance(lbl_text, str): + lbl_text = str(lbl_text) + + # In the old code, every non-self-loop is placed via a fancy arrow patch + # Only compute a new fancy arrow if needed by caching the results from + # edge placement. + try: + arrow = fancy_arrows[e] + except KeyError: + arrow = build_fancy_arrow(e) + + if e[0] == e[1]: + # Taken directly from draw_networkx_edge_labels + connectionstyle_obj = arrow.get_connectionstyle() + posA = canvas.transData.transform(edge_subgraph.nodes[e[0]][pos]) + path_disp = connectionstyle_obj(posA, posA) + path_data = canvas.transData.inverted().transform_path(path_disp) + x, y = path_data.vertices[0] + canvas.text( + x, + y, + lbl_text, + size=lbl.get("size", defaults["edge_label"]["size"]), + color=lbl.get("color", defaults["edge_label"]["color"]), + family=lbl.get("family", defaults["edge_label"]["family"]), + weight=lbl.get("weight", defaults["edge_label"]["weight"]), + alpha=lbl.get("alpha", defaults["edge_label"]["alpha"]), + horizontalalignment=lbl.get( + "h_align", defaults["edge_label"]["h_align"] + ), + verticalalignment=lbl.get( + "v_align", defaults["edge_label"]["v_align"] + ), + rotation=0, + transform=canvas.transData, + bbox=lbl.get("bbox", defaults["edge_label"]["bbox"]), + zorder=1, + ) + continue + + CurvedArrowText( + arrow, + lbl_text, + size=lbl.get("size", defaults["edge_label"]["size"]), + color=lbl.get("color", defaults["edge_label"]["color"]), + family=lbl.get("family", defaults["edge_label"]["family"]), + weight=lbl.get("weight", defaults["edge_label"]["weight"]), + alpha=lbl.get("alpha", defaults["edge_label"]["alpha"]), + bbox=lbl.get("bbox", defaults["edge_label"]["bbox"]), + horizontalalignment=lbl.get( + "h_align", defaults["edge_label"]["h_align"] + ), + verticalalignment=lbl.get("v_align", defaults["edge_label"]["v_align"]), + label_pos=lbl.get("pos", defaults["edge_label"]["pos"]), + labels_horizontal=lbl.get("rotate", defaults["edge_label"]["rotate"]), + transform=canvas.transData, + zorder=1, + ax=canvas, + ) + + # If we had to add an attribute, remove it here + if pos == default_display_pos_attr: + nx.remove_node_attributes(G, default_display_pos_attr) + + return G + + +def draw(G, pos=None, ax=None, **kwds): + """Draw the graph G with Matplotlib. + + Draw the graph as a simple representation with no node + labels or edge labels and using the full Matplotlib figure area + and no axis labels by default. See draw_networkx() for more + full-featured drawing that allows title, axis labels etc. + + Parameters + ---------- + G : graph + A networkx graph + + pos : dictionary, optional + A dictionary with nodes as keys and positions as values. + If not specified a spring layout positioning will be computed. + See :py:mod:`networkx.drawing.layout` for functions that + compute node positions. + + ax : Matplotlib Axes object, optional + Draw the graph in specified Matplotlib axes. + + kwds : optional keywords + See networkx.draw_networkx() for a description of optional keywords. + + Examples + -------- + >>> G = nx.dodecahedral_graph() + >>> nx.draw(G) + >>> nx.draw(G, pos=nx.spring_layout(G)) # use spring layout + + See Also + -------- + draw_networkx + draw_networkx_nodes + draw_networkx_edges + draw_networkx_labels + draw_networkx_edge_labels + + Notes + ----- + This function has the same name as pylab.draw and pyplot.draw + so beware when using `from networkx import *` + + since you might overwrite the pylab.draw function. + + With pyplot use + + >>> import matplotlib.pyplot as plt + >>> G = nx.dodecahedral_graph() + >>> nx.draw(G) # networkx draw() + >>> plt.draw() # pyplot draw() + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + """ + + import matplotlib.pyplot as plt + + if ax is None: + cf = plt.gcf() + else: + cf = ax.get_figure() + cf.set_facecolor("w") + if ax is None: + if cf.axes: + ax = cf.gca() + else: + ax = cf.add_axes((0, 0, 1, 1)) + + if "with_labels" not in kwds: + kwds["with_labels"] = "labels" in kwds + + draw_networkx(G, pos=pos, ax=ax, **kwds) + ax.set_axis_off() + plt.draw_if_interactive() + return + + +def draw_networkx(G, pos=None, arrows=None, with_labels=True, **kwds): + r"""Draw the graph G using Matplotlib. + + Draw the graph with Matplotlib with options for node positions, + labeling, titles, and many other drawing features. + See draw() for simple drawing without labels or axes. + + Parameters + ---------- + G : graph + A networkx graph + + pos : dictionary, optional + A dictionary with nodes as keys and positions as values. + If not specified a spring layout positioning will be computed. + See :py:mod:`networkx.drawing.layout` for functions that + compute node positions. + + arrows : bool or None, optional (default=None) + If `None`, directed graphs draw arrowheads with + `~matplotlib.patches.FancyArrowPatch`, while undirected graphs draw edges + via `~matplotlib.collections.LineCollection` for speed. + If `True`, draw arrowheads with FancyArrowPatches (bendable and stylish). + If `False`, draw edges using LineCollection (linear and fast). + For directed graphs, if True draw arrowheads. + Note: Arrows will be the same color as edges. + + arrowstyle : str (default='-\|>' for directed graphs) + For directed graphs, choose the style of the arrowsheads. + For undirected graphs default to '-' + + See `matplotlib.patches.ArrowStyle` for more options. + + arrowsize : int or list (default=10) + For directed graphs, choose the size of the arrow head's length and + width. A list of values can be passed in to assign a different size for arrow head's length and width. + See `matplotlib.patches.FancyArrowPatch` for attribute `mutation_scale` + for more info. + + with_labels : bool (default=True) + Set to True to draw labels on the nodes. + + ax : Matplotlib Axes object, optional + Draw the graph in the specified Matplotlib axes. + + nodelist : list (default=list(G)) + Draw only specified nodes + + edgelist : list (default=list(G.edges())) + Draw only specified edges + + node_size : scalar or array (default=300) + Size of nodes. If an array is specified it must be the + same length as nodelist. + + node_color : color or array of colors (default='#1f78b4') + Node color. Can be a single color or a sequence of colors with the same + length as nodelist. Color can be string or rgb (or rgba) tuple of + floats from 0-1. If numeric values are specified they will be + mapped to colors using the cmap and vmin,vmax parameters. See + matplotlib.scatter for more details. + + node_shape : string (default='o') + The shape of the node. Specification is as matplotlib.scatter + marker, one of 'so^>v>> G = nx.dodecahedral_graph() + >>> nx.draw(G) + >>> nx.draw(G, pos=nx.spring_layout(G)) # use spring layout + + >>> import matplotlib.pyplot as plt + >>> limits = plt.axis("off") # turn off axis + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + + See Also + -------- + draw + draw_networkx_nodes + draw_networkx_edges + draw_networkx_labels + draw_networkx_edge_labels + """ + from inspect import signature + + import matplotlib.pyplot as plt + + # Get all valid keywords by inspecting the signatures of draw_networkx_nodes, + # draw_networkx_edges, draw_networkx_labels + + valid_node_kwds = signature(draw_networkx_nodes).parameters.keys() + valid_edge_kwds = signature(draw_networkx_edges).parameters.keys() + valid_label_kwds = signature(draw_networkx_labels).parameters.keys() + + # Create a set with all valid keywords across the three functions and + # remove the arguments of this function (draw_networkx) + valid_kwds = (valid_node_kwds | valid_edge_kwds | valid_label_kwds) - { + "G", + "pos", + "arrows", + "with_labels", + } + + if any(k not in valid_kwds for k in kwds): + invalid_args = ", ".join([k for k in kwds if k not in valid_kwds]) + raise ValueError(f"Received invalid argument(s): {invalid_args}") + + node_kwds = {k: v for k, v in kwds.items() if k in valid_node_kwds} + edge_kwds = {k: v for k, v in kwds.items() if k in valid_edge_kwds} + label_kwds = {k: v for k, v in kwds.items() if k in valid_label_kwds} + + if pos is None: + pos = nx.drawing.spring_layout(G) # default to spring layout + + draw_networkx_nodes(G, pos, **node_kwds) + draw_networkx_edges(G, pos, arrows=arrows, **edge_kwds) + if with_labels: + draw_networkx_labels(G, pos, **label_kwds) + plt.draw_if_interactive() + + +def draw_networkx_nodes( + G, + pos, + nodelist=None, + node_size=300, + node_color="#1f78b4", + node_shape="o", + alpha=None, + cmap=None, + vmin=None, + vmax=None, + ax=None, + linewidths=None, + edgecolors=None, + label=None, + margins=None, + hide_ticks=True, +): + """Draw the nodes of the graph G. + + This draws only the nodes of the graph G. + + Parameters + ---------- + G : graph + A networkx graph + + pos : dictionary + A dictionary with nodes as keys and positions as values. + Positions should be sequences of length 2. + + ax : Matplotlib Axes object, optional + Draw the graph in the specified Matplotlib axes. + + nodelist : list (default list(G)) + Draw only specified nodes + + node_size : scalar or array (default=300) + Size of nodes. If an array it must be the same length as nodelist. + + node_color : color or array of colors (default='#1f78b4') + Node color. Can be a single color or a sequence of colors with the same + length as nodelist. Color can be string or rgb (or rgba) tuple of + floats from 0-1. If numeric values are specified they will be + mapped to colors using the cmap and vmin,vmax parameters. See + matplotlib.scatter for more details. + + node_shape : string (default='o') + The shape of the node. Specification is as matplotlib.scatter + marker, one of 'so^>v>> G = nx.dodecahedral_graph() + >>> nodes = nx.draw_networkx_nodes(G, pos=nx.spring_layout(G)) + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + + See Also + -------- + draw + draw_networkx + draw_networkx_edges + draw_networkx_labels + draw_networkx_edge_labels + """ + from collections.abc import Iterable + + import matplotlib as mpl + import matplotlib.collections # call as mpl.collections + import matplotlib.pyplot as plt + import numpy as np + + if ax is None: + ax = plt.gca() + + if nodelist is None: + nodelist = list(G) + + if len(nodelist) == 0: # empty nodelist, no drawing + return mpl.collections.PathCollection(None) + + try: + xy = np.asarray([pos[v] for v in nodelist]) + except KeyError as err: + raise nx.NetworkXError(f"Node {err} has no position.") from err + + if isinstance(alpha, Iterable): + node_color = apply_alpha(node_color, alpha, nodelist, cmap, vmin, vmax) + alpha = None + + if not isinstance(node_shape, np.ndarray) and not isinstance(node_shape, list): + node_shape = np.array([node_shape for _ in range(len(nodelist))]) + elif isinstance(node_shape, list): + node_shape = np.asarray(node_shape) + + for shape in np.unique(node_shape): + node_collection = ax.scatter( + xy[node_shape == shape, 0], + xy[node_shape == shape, 1], + s=node_size, + c=node_color, + marker=shape, + cmap=cmap, + vmin=vmin, + vmax=vmax, + alpha=alpha, + linewidths=linewidths, + edgecolors=edgecolors, + label=label, + ) + if hide_ticks: + ax.tick_params( + axis="both", + which="both", + bottom=False, + left=False, + labelbottom=False, + labelleft=False, + ) + + if margins is not None: + if isinstance(margins, Iterable): + ax.margins(*margins) + else: + ax.margins(margins) + + node_collection.set_zorder(2) + return node_collection + + +class FancyArrowFactory: + """Draw arrows with `matplotlib.patches.FancyarrowPatch`""" + + class ConnectionStyleFactory: + def __init__(self, connectionstyles, selfloop_height, ax=None): + import matplotlib as mpl + import matplotlib.path # call as mpl.path + import numpy as np + + self.ax = ax + self.mpl = mpl + self.np = np + self.base_connection_styles = [ + mpl.patches.ConnectionStyle(cs) for cs in connectionstyles + ] + self.n = len(self.base_connection_styles) + self.selfloop_height = selfloop_height + + def curved(self, edge_index): + return self.base_connection_styles[edge_index % self.n] + + def self_loop(self, edge_index): + def self_loop_connection(posA, posB, *args, **kwargs): + if not self.np.all(posA == posB): + raise nx.NetworkXError( + "`self_loop` connection style method" + "is only to be used for self-loops" + ) + # this is called with _screen space_ values + # so convert back to data space + data_loc = self.ax.transData.inverted().transform(posA) + v_shift = 0.1 * self.selfloop_height + h_shift = v_shift * 0.5 + # put the top of the loop first so arrow is not hidden by node + path = self.np.asarray( + [ + # 1 + [0, v_shift], + # 4 4 4 + [h_shift, v_shift], + [h_shift, 0], + [0, 0], + # 4 4 4 + [-h_shift, 0], + [-h_shift, v_shift], + [0, v_shift], + ] + ) + # Rotate self loop 90 deg. if more than 1 + # This will allow for maximum of 4 visible self loops + if edge_index % 4: + x, y = path.T + for _ in range(edge_index % 4): + x, y = y, -x + path = self.np.array([x, y]).T + return self.mpl.path.Path( + self.ax.transData.transform(data_loc + path), [1, 4, 4, 4, 4, 4, 4] + ) + + return self_loop_connection + + def __init__( + self, + edge_pos, + edgelist, + nodelist, + edge_indices, + node_size, + selfloop_height, + connectionstyle="arc3", + node_shape="o", + arrowstyle="-", + arrowsize=10, + edge_color="k", + alpha=None, + linewidth=1.0, + style="solid", + min_source_margin=0, + min_target_margin=0, + ax=None, + ): + import matplotlib as mpl + import matplotlib.patches # call as mpl.patches + import matplotlib.pyplot as plt + import numpy as np + + if isinstance(connectionstyle, str): + connectionstyle = [connectionstyle] + elif np.iterable(connectionstyle): + connectionstyle = list(connectionstyle) + else: + msg = "ConnectionStyleFactory arg `connectionstyle` must be str or iterable" + raise nx.NetworkXError(msg) + self.ax = ax + self.mpl = mpl + self.np = np + self.edge_pos = edge_pos + self.edgelist = edgelist + self.nodelist = nodelist + self.node_shape = node_shape + self.min_source_margin = min_source_margin + self.min_target_margin = min_target_margin + self.edge_indices = edge_indices + self.node_size = node_size + self.connectionstyle_factory = self.ConnectionStyleFactory( + connectionstyle, selfloop_height, ax + ) + self.arrowstyle = arrowstyle + self.arrowsize = arrowsize + self.arrow_colors = mpl.colors.colorConverter.to_rgba_array(edge_color, alpha) + self.linewidth = linewidth + self.style = style + if isinstance(arrowsize, list) and len(arrowsize) != len(edge_pos): + raise ValueError("arrowsize should have the same length as edgelist") + + def __call__(self, i): + (x1, y1), (x2, y2) = self.edge_pos[i] + shrink_source = 0 # space from source to tail + shrink_target = 0 # space from head to target + if ( + self.np.iterable(self.min_source_margin) + and not isinstance(self.min_source_margin, str) + and not isinstance(self.min_source_margin, tuple) + ): + min_source_margin = self.min_source_margin[i] + else: + min_source_margin = self.min_source_margin + + if ( + self.np.iterable(self.min_target_margin) + and not isinstance(self.min_target_margin, str) + and not isinstance(self.min_target_margin, tuple) + ): + min_target_margin = self.min_target_margin[i] + else: + min_target_margin = self.min_target_margin + + if self.np.iterable(self.node_size): # many node sizes + source, target = self.edgelist[i][:2] + source_node_size = self.node_size[self.nodelist.index(source)] + target_node_size = self.node_size[self.nodelist.index(target)] + shrink_source = self.to_marker_edge(source_node_size, self.node_shape) + shrink_target = self.to_marker_edge(target_node_size, self.node_shape) + else: + shrink_source = self.to_marker_edge(self.node_size, self.node_shape) + shrink_target = shrink_source + shrink_source = max(shrink_source, min_source_margin) + shrink_target = max(shrink_target, min_target_margin) + + # scale factor of arrow head + if isinstance(self.arrowsize, list): + mutation_scale = self.arrowsize[i] + else: + mutation_scale = self.arrowsize + + if len(self.arrow_colors) > i: + arrow_color = self.arrow_colors[i] + elif len(self.arrow_colors) == 1: + arrow_color = self.arrow_colors[0] + else: # Cycle through colors + arrow_color = self.arrow_colors[i % len(self.arrow_colors)] + + if self.np.iterable(self.linewidth): + if len(self.linewidth) > i: + linewidth = self.linewidth[i] + else: + linewidth = self.linewidth[i % len(self.linewidth)] + else: + linewidth = self.linewidth + + if ( + self.np.iterable(self.style) + and not isinstance(self.style, str) + and not isinstance(self.style, tuple) + ): + if len(self.style) > i: + linestyle = self.style[i] + else: # Cycle through styles + linestyle = self.style[i % len(self.style)] + else: + linestyle = self.style + + if x1 == x2 and y1 == y2: + connectionstyle = self.connectionstyle_factory.self_loop( + self.edge_indices[i] + ) + else: + connectionstyle = self.connectionstyle_factory.curved(self.edge_indices[i]) + + if ( + self.np.iterable(self.arrowstyle) + and not isinstance(self.arrowstyle, str) + and not isinstance(self.arrowstyle, tuple) + ): + arrowstyle = self.arrowstyle[i] + else: + arrowstyle = self.arrowstyle + + return self.mpl.patches.FancyArrowPatch( + (x1, y1), + (x2, y2), + arrowstyle=arrowstyle, + shrinkA=shrink_source, + shrinkB=shrink_target, + mutation_scale=mutation_scale, + color=arrow_color, + linewidth=linewidth, + connectionstyle=connectionstyle, + linestyle=linestyle, + zorder=1, # arrows go behind nodes + ) + + def to_marker_edge(self, marker_size, marker): + if marker in "s^>v', + For undirected graphs default to '-'. + + See `matplotlib.patches.ArrowStyle` for more options. + + arrowsize : int or list of ints(default=10) + For directed graphs, choose the size of the arrow head's length and + width. See `matplotlib.patches.FancyArrowPatch` for attribute + `mutation_scale` for more info. + + connectionstyle : string or iterable of strings (default="arc3") + Pass the connectionstyle parameter to create curved arc of rounding + radius rad. For example, connectionstyle='arc3,rad=0.2'. + See `matplotlib.patches.ConnectionStyle` and + `matplotlib.patches.FancyArrowPatch` for more info. + If Iterable, index indicates i'th edge key of MultiGraph + + node_size : scalar or array (default=300) + Size of nodes. Though the nodes are not drawn with this function, the + node size is used in determining edge positioning. + + nodelist : list, optional (default=G.nodes()) + This provides the node order for the `node_size` array (if it is an array). + + node_shape : string (default='o') + The marker used for nodes, used in determining edge positioning. + Specification is as a `matplotlib.markers` marker, e.g. one of 'so^>v>> G = nx.dodecahedral_graph() + >>> edges = nx.draw_networkx_edges(G, pos=nx.spring_layout(G)) + + >>> G = nx.DiGraph() + >>> G.add_edges_from([(1, 2), (1, 3), (2, 3)]) + >>> arcs = nx.draw_networkx_edges(G, pos=nx.spring_layout(G)) + >>> alphas = [0.3, 0.4, 0.5] + >>> for i, arc in enumerate(arcs): # change alpha values of arcs + ... arc.set_alpha(alphas[i]) + + The FancyArrowPatches corresponding to self-loops are not always + returned, but can always be accessed via the ``patches`` attribute of the + `matplotlib.Axes` object. + + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> G = nx.Graph([(0, 1), (0, 0)]) # Self-loop at node 0 + >>> edge_collection = nx.draw_networkx_edges(G, pos=nx.circular_layout(G), ax=ax) + >>> self_loop_fap = ax.patches[0] + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + + See Also + -------- + draw + draw_networkx + draw_networkx_nodes + draw_networkx_labels + draw_networkx_edge_labels + + """ + import warnings + + import matplotlib as mpl + import matplotlib.collections # call as mpl.collections + import matplotlib.colors # call as mpl.colors + import matplotlib.pyplot as plt + import numpy as np + + # The default behavior is to use LineCollection to draw edges for + # undirected graphs (for performance reasons) and use FancyArrowPatches + # for directed graphs. + # The `arrows` keyword can be used to override the default behavior + if arrows is None: + use_linecollection = not (G.is_directed() or G.is_multigraph()) + else: + if not isinstance(arrows, bool): + raise TypeError("Argument `arrows` must be of type bool or None") + use_linecollection = not arrows + + if isinstance(connectionstyle, str): + connectionstyle = [connectionstyle] + elif np.iterable(connectionstyle): + connectionstyle = list(connectionstyle) + else: + msg = "draw_networkx_edges arg `connectionstyle` must be str or iterable" + raise nx.NetworkXError(msg) + + # Some kwargs only apply to FancyArrowPatches. Warn users when they use + # non-default values for these kwargs when LineCollection is being used + # instead of silently ignoring the specified option + if use_linecollection: + msg = ( + "\n\nThe {0} keyword argument is not applicable when drawing edges\n" + "with LineCollection.\n\n" + "To make this warning go away, either specify `arrows=True` to\n" + "force FancyArrowPatches or use the default values.\n" + "Note that using FancyArrowPatches may be slow for large graphs.\n" + ) + if arrowstyle is not None: + warnings.warn(msg.format("arrowstyle"), category=UserWarning, stacklevel=2) + if arrowsize != 10: + warnings.warn(msg.format("arrowsize"), category=UserWarning, stacklevel=2) + if min_source_margin != 0: + warnings.warn( + msg.format("min_source_margin"), category=UserWarning, stacklevel=2 + ) + if min_target_margin != 0: + warnings.warn( + msg.format("min_target_margin"), category=UserWarning, stacklevel=2 + ) + if any(cs != "arc3" for cs in connectionstyle): + warnings.warn( + msg.format("connectionstyle"), category=UserWarning, stacklevel=2 + ) + + # NOTE: Arrowstyle modification must occur after the warnings section + if arrowstyle is None: + arrowstyle = "-|>" if G.is_directed() else "-" + + if ax is None: + ax = plt.gca() + + if edgelist is None: + edgelist = list(G.edges) # (u, v, k) for multigraph (u, v) otherwise + + if len(edgelist): + if G.is_multigraph(): + key_count = collections.defaultdict(lambda: itertools.count(0)) + edge_indices = [next(key_count[tuple(e[:2])]) for e in edgelist] + else: + edge_indices = [0] * len(edgelist) + else: # no edges! + return [] + + if nodelist is None: + nodelist = list(G.nodes()) + + # FancyArrowPatch handles color=None different from LineCollection + if edge_color is None: + edge_color = "k" + + # set edge positions + edge_pos = np.asarray([(pos[e[0]], pos[e[1]]) for e in edgelist]) + + # Check if edge_color is an array of floats and map to edge_cmap. + # This is the only case handled differently from matplotlib + if ( + np.iterable(edge_color) + and (len(edge_color) == len(edge_pos)) + and np.all([isinstance(c, Number) for c in edge_color]) + ): + if edge_cmap is not None: + assert isinstance(edge_cmap, mpl.colors.Colormap) + else: + edge_cmap = plt.get_cmap() + if edge_vmin is None: + edge_vmin = min(edge_color) + if edge_vmax is None: + edge_vmax = max(edge_color) + color_normal = mpl.colors.Normalize(vmin=edge_vmin, vmax=edge_vmax) + edge_color = [edge_cmap(color_normal(e)) for e in edge_color] + + # compute initial view + minx = np.amin(np.ravel(edge_pos[:, :, 0])) + maxx = np.amax(np.ravel(edge_pos[:, :, 0])) + miny = np.amin(np.ravel(edge_pos[:, :, 1])) + maxy = np.amax(np.ravel(edge_pos[:, :, 1])) + w = maxx - minx + h = maxy - miny + + # Self-loops are scaled by view extent, except in cases the extent + # is 0, e.g. for a single node. In this case, fall back to scaling + # by the maximum node size + selfloop_height = h if h != 0 else 0.005 * np.array(node_size).max() + fancy_arrow_factory = FancyArrowFactory( + edge_pos, + edgelist, + nodelist, + edge_indices, + node_size, + selfloop_height, + connectionstyle, + node_shape, + arrowstyle, + arrowsize, + edge_color, + alpha, + width, + style, + min_source_margin, + min_target_margin, + ax=ax, + ) + + # Draw the edges + if use_linecollection: + edge_collection = mpl.collections.LineCollection( + edge_pos, + colors=edge_color, + linewidths=width, + antialiaseds=(1,), + linestyle=style, + alpha=alpha, + ) + edge_collection.set_cmap(edge_cmap) + edge_collection.set_clim(edge_vmin, edge_vmax) + edge_collection.set_zorder(1) # edges go behind nodes + edge_collection.set_label(label) + ax.add_collection(edge_collection) + edge_viz_obj = edge_collection + + # Make sure selfloop edges are also drawn + # --------------------------------------- + selfloops_to_draw = [loop for loop in nx.selfloop_edges(G) if loop in edgelist] + if selfloops_to_draw: + edgelist_tuple = list(map(tuple, edgelist)) + arrow_collection = [] + for loop in selfloops_to_draw: + i = edgelist_tuple.index(loop) + arrow = fancy_arrow_factory(i) + arrow_collection.append(arrow) + ax.add_patch(arrow) + else: + edge_viz_obj = [] + for i in range(len(edgelist)): + arrow = fancy_arrow_factory(i) + ax.add_patch(arrow) + edge_viz_obj.append(arrow) + + # update view after drawing + padx, pady = 0.05 * w, 0.05 * h + corners = (minx - padx, miny - pady), (maxx + padx, maxy + pady) + ax.update_datalim(corners) + ax.autoscale_view() + + if hide_ticks: + ax.tick_params( + axis="both", + which="both", + bottom=False, + left=False, + labelbottom=False, + labelleft=False, + ) + + return edge_viz_obj + + +def draw_networkx_labels( + G, + pos, + labels=None, + font_size=12, + font_color="k", + font_family="sans-serif", + font_weight="normal", + alpha=None, + bbox=None, + horizontalalignment="center", + verticalalignment="center", + ax=None, + clip_on=True, + hide_ticks=True, +): + """Draw node labels on the graph G. + + Parameters + ---------- + G : graph + A networkx graph + + pos : dictionary + A dictionary with nodes as keys and positions as values. + Positions should be sequences of length 2. + + labels : dictionary (default={n: n for n in G}) + Node labels in a dictionary of text labels keyed by node. + Node-keys in labels should appear as keys in `pos`. + If needed use: `{n:lab for n,lab in labels.items() if n in pos}` + + font_size : int or dictionary of nodes to ints (default=12) + Font size for text labels. + + font_color : color or dictionary of nodes to colors (default='k' black) + Font color string. Color can be string or rgb (or rgba) tuple of + floats from 0-1. + + font_weight : string or dictionary of nodes to strings (default='normal') + Font weight. + + font_family : string or dictionary of nodes to strings (default='sans-serif') + Font family. + + alpha : float or None or dictionary of nodes to floats (default=None) + The text transparency. + + bbox : Matplotlib bbox, (default is Matplotlib's ax.text default) + Specify text box properties (e.g. shape, color etc.) for node labels. + + horizontalalignment : string or array of strings (default='center') + Horizontal alignment {'center', 'right', 'left'}. If an array is + specified it must be the same length as `nodelist`. + + verticalalignment : string (default='center') + Vertical alignment {'center', 'top', 'bottom', 'baseline', 'center_baseline'}. + If an array is specified it must be the same length as `nodelist`. + + ax : Matplotlib Axes object, optional + Draw the graph in the specified Matplotlib axes. + + clip_on : bool (default=True) + Turn on clipping of node labels at axis boundaries + + hide_ticks : bool, optional + Hide ticks of axes. When `True` (the default), ticks and ticklabels + are removed from the axes. To set ticks and tick labels to the pyplot default, + use ``hide_ticks=False``. + + Returns + ------- + dict + `dict` of labels keyed on the nodes + + Examples + -------- + >>> G = nx.dodecahedral_graph() + >>> labels = nx.draw_networkx_labels(G, pos=nx.spring_layout(G)) + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + + See Also + -------- + draw + draw_networkx + draw_networkx_nodes + draw_networkx_edges + draw_networkx_edge_labels + """ + import matplotlib.pyplot as plt + + if ax is None: + ax = plt.gca() + + if labels is None: + labels = {n: n for n in G.nodes()} + + individual_params = set() + + def check_individual_params(p_value, p_name): + if isinstance(p_value, dict): + if len(p_value) != len(labels): + raise ValueError(f"{p_name} must have the same length as labels.") + individual_params.add(p_name) + + def get_param_value(node, p_value, p_name): + if p_name in individual_params: + return p_value[node] + return p_value + + check_individual_params(font_size, "font_size") + check_individual_params(font_color, "font_color") + check_individual_params(font_weight, "font_weight") + check_individual_params(font_family, "font_family") + check_individual_params(alpha, "alpha") + + text_items = {} # there is no text collection so we'll fake one + for n, label in labels.items(): + (x, y) = pos[n] + if not isinstance(label, str): + label = str(label) # this makes "1" and 1 labeled the same + t = ax.text( + x, + y, + label, + size=get_param_value(n, font_size, "font_size"), + color=get_param_value(n, font_color, "font_color"), + family=get_param_value(n, font_family, "font_family"), + weight=get_param_value(n, font_weight, "font_weight"), + alpha=get_param_value(n, alpha, "alpha"), + horizontalalignment=horizontalalignment, + verticalalignment=verticalalignment, + transform=ax.transData, + bbox=bbox, + clip_on=clip_on, + ) + text_items[n] = t + + if hide_ticks: + ax.tick_params( + axis="both", + which="both", + bottom=False, + left=False, + labelbottom=False, + labelleft=False, + ) + + return text_items + + +def draw_networkx_edge_labels( + G, + pos, + edge_labels=None, + label_pos=0.5, + font_size=10, + font_color="k", + font_family="sans-serif", + font_weight="normal", + alpha=None, + bbox=None, + horizontalalignment="center", + verticalalignment="center", + ax=None, + rotate=True, + clip_on=True, + node_size=300, + nodelist=None, + connectionstyle="arc3", + hide_ticks=True, +): + """Draw edge labels. + + Parameters + ---------- + G : graph + A networkx graph + + pos : dictionary + A dictionary with nodes as keys and positions as values. + Positions should be sequences of length 2. + + edge_labels : dictionary (default=None) + Edge labels in a dictionary of labels keyed by edge two-tuple. + Only labels for the keys in the dictionary are drawn. + + label_pos : float (default=0.5) + Position of edge label along edge (0=head, 0.5=center, 1=tail) + + font_size : int (default=10) + Font size for text labels + + font_color : color (default='k' black) + Font color string. Color can be string or rgb (or rgba) tuple of + floats from 0-1. + + font_weight : string (default='normal') + Font weight + + font_family : string (default='sans-serif') + Font family + + alpha : float or None (default=None) + The text transparency + + bbox : Matplotlib bbox, optional + Specify text box properties (e.g. shape, color etc.) for edge labels. + Default is {boxstyle='round', ec=(1.0, 1.0, 1.0), fc=(1.0, 1.0, 1.0)}. + + horizontalalignment : string (default='center') + Horizontal alignment {'center', 'right', 'left'} + + verticalalignment : string (default='center') + Vertical alignment {'center', 'top', 'bottom', 'baseline', 'center_baseline'} + + ax : Matplotlib Axes object, optional + Draw the graph in the specified Matplotlib axes. + + rotate : bool (default=True) + Rotate edge labels to lie parallel to edges + + clip_on : bool (default=True) + Turn on clipping of edge labels at axis boundaries + + node_size : scalar or array (default=300) + Size of nodes. If an array it must be the same length as nodelist. + + nodelist : list, optional (default=G.nodes()) + This provides the node order for the `node_size` array (if it is an array). + + connectionstyle : string or iterable of strings (default="arc3") + Pass the connectionstyle parameter to create curved arc of rounding + radius rad. For example, connectionstyle='arc3,rad=0.2'. + See `matplotlib.patches.ConnectionStyle` and + `matplotlib.patches.FancyArrowPatch` for more info. + If Iterable, index indicates i'th edge key of MultiGraph + + hide_ticks : bool, optional + Hide ticks of axes. When `True` (the default), ticks and ticklabels + are removed from the axes. To set ticks and tick labels to the pyplot default, + use ``hide_ticks=False``. + + Returns + ------- + dict + `dict` of labels keyed by edge + + Examples + -------- + >>> G = nx.dodecahedral_graph() + >>> edge_labels = nx.draw_networkx_edge_labels(G, pos=nx.spring_layout(G)) + + Also see the NetworkX drawing examples at + https://networkx.org/documentation/latest/auto_examples/index.html + + See Also + -------- + draw + draw_networkx + draw_networkx_nodes + draw_networkx_edges + draw_networkx_labels + """ + import matplotlib as mpl + import matplotlib.pyplot as plt + import numpy as np + + class CurvedArrowText(CurvedArrowTextBase, mpl.text.Text): + pass + + # use default box of white with white border + if bbox is None: + bbox = {"boxstyle": "round", "ec": (1.0, 1.0, 1.0), "fc": (1.0, 1.0, 1.0)} + + if isinstance(connectionstyle, str): + connectionstyle = [connectionstyle] + elif np.iterable(connectionstyle): + connectionstyle = list(connectionstyle) + else: + raise nx.NetworkXError( + "draw_networkx_edges arg `connectionstyle` must be" + "string or iterable of strings" + ) + + if ax is None: + ax = plt.gca() + + if edge_labels is None: + kwds = {"keys": True} if G.is_multigraph() else {} + edge_labels = {tuple(edge): d for *edge, d in G.edges(data=True, **kwds)} + # NOTHING TO PLOT + if not edge_labels: + return {} + edgelist, labels = zip(*edge_labels.items()) + + if nodelist is None: + nodelist = list(G.nodes()) + + # set edge positions + edge_pos = np.asarray([(pos[e[0]], pos[e[1]]) for e in edgelist]) + + if G.is_multigraph(): + key_count = collections.defaultdict(lambda: itertools.count(0)) + edge_indices = [next(key_count[tuple(e[:2])]) for e in edgelist] + else: + edge_indices = [0] * len(edgelist) + + # Used to determine self loop mid-point + # Note, that this will not be accurate, + # if not drawing edge_labels for all edges drawn + h = 0 + if edge_labels: + miny = np.amin(np.ravel(edge_pos[:, :, 1])) + maxy = np.amax(np.ravel(edge_pos[:, :, 1])) + h = maxy - miny + selfloop_height = h if h != 0 else 0.005 * np.array(node_size).max() + fancy_arrow_factory = FancyArrowFactory( + edge_pos, + edgelist, + nodelist, + edge_indices, + node_size, + selfloop_height, + connectionstyle, + ax=ax, + ) + + individual_params = {} + + def check_individual_params(p_value, p_name): + # TODO should this be list or array (as in a numpy array)? + if isinstance(p_value, list): + if len(p_value) != len(edgelist): + raise ValueError(f"{p_name} must have the same length as edgelist.") + individual_params[p_name] = p_value.iter() + + # Don't need to pass in an edge because these are lists, not dicts + def get_param_value(p_value, p_name): + if p_name in individual_params: + return next(individual_params[p_name]) + return p_value + + check_individual_params(font_size, "font_size") + check_individual_params(font_color, "font_color") + check_individual_params(font_weight, "font_weight") + check_individual_params(alpha, "alpha") + check_individual_params(horizontalalignment, "horizontalalignment") + check_individual_params(verticalalignment, "verticalalignment") + check_individual_params(rotate, "rotate") + check_individual_params(label_pos, "label_pos") + + text_items = {} + for i, (edge, label) in enumerate(zip(edgelist, labels)): + if not isinstance(label, str): + label = str(label) # this makes "1" and 1 labeled the same + + n1, n2 = edge[:2] + arrow = fancy_arrow_factory(i) + if n1 == n2: + connectionstyle_obj = arrow.get_connectionstyle() + posA = ax.transData.transform(pos[n1]) + path_disp = connectionstyle_obj(posA, posA) + path_data = ax.transData.inverted().transform_path(path_disp) + x, y = path_data.vertices[0] + text_items[edge] = ax.text( + x, + y, + label, + size=get_param_value(font_size, "font_size"), + color=get_param_value(font_color, "font_color"), + family=get_param_value(font_family, "font_family"), + weight=get_param_value(font_weight, "font_weight"), + alpha=get_param_value(alpha, "alpha"), + horizontalalignment=get_param_value( + horizontalalignment, "horizontalalignment" + ), + verticalalignment=get_param_value( + verticalalignment, "verticalalignment" + ), + rotation=0, + transform=ax.transData, + bbox=bbox, + zorder=1, + clip_on=clip_on, + ) + else: + text_items[edge] = CurvedArrowText( + arrow, + label, + size=get_param_value(font_size, "font_size"), + color=get_param_value(font_color, "font_color"), + family=get_param_value(font_family, "font_family"), + weight=get_param_value(font_weight, "font_weight"), + alpha=get_param_value(alpha, "alpha"), + horizontalalignment=get_param_value( + horizontalalignment, "horizontalalignment" + ), + verticalalignment=get_param_value( + verticalalignment, "verticalalignment" + ), + transform=ax.transData, + bbox=bbox, + zorder=1, + clip_on=clip_on, + label_pos=get_param_value(label_pos, "label_pos"), + labels_horizontal=not get_param_value(rotate, "rotate"), + ax=ax, + ) + + if hide_ticks: + ax.tick_params( + axis="both", + which="both", + bottom=False, + left=False, + labelbottom=False, + labelleft=False, + ) + + return text_items + + +def draw_bipartite(G, **kwargs): + """Draw the graph `G` with a bipartite layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.bipartite_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Raises + ------ + NetworkXError : + If `G` is not bipartite. + + Notes + ----- + The layout is computed each time this function is called. For + repeated drawing it is much more efficient to call + `~networkx.drawing.layout.bipartite_layout` directly and reuse the result:: + + >>> G = nx.complete_bipartite_graph(3, 3) + >>> pos = nx.bipartite_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.complete_bipartite_graph(2, 5) + >>> nx.draw_bipartite(G) + + See Also + -------- + :func:`~networkx.drawing.layout.bipartite_layout` + """ + draw(G, pos=nx.bipartite_layout(G), **kwargs) + + +def draw_circular(G, **kwargs): + """Draw the graph `G` with a circular layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.circular_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + The layout is computed each time this function is called. For + repeated drawing it is much more efficient to call + `~networkx.drawing.layout.circular_layout` directly and reuse the result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.circular_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.draw_circular(G) + + See Also + -------- + :func:`~networkx.drawing.layout.circular_layout` + """ + draw(G, pos=nx.circular_layout(G), **kwargs) + + +def draw_kamada_kawai(G, **kwargs): + """Draw the graph `G` with a Kamada-Kawai force-directed layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.kamada_kawai_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.kamada_kawai_layout` directly and reuse the + result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.kamada_kawai_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.draw_kamada_kawai(G) + + See Also + -------- + :func:`~networkx.drawing.layout.kamada_kawai_layout` + """ + draw(G, pos=nx.kamada_kawai_layout(G), **kwargs) + + +def draw_random(G, **kwargs): + """Draw the graph `G` with a random layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.random_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.random_layout` directly and reuse the result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.random_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.lollipop_graph(4, 3) + >>> nx.draw_random(G) + + See Also + -------- + :func:`~networkx.drawing.layout.random_layout` + """ + draw(G, pos=nx.random_layout(G), **kwargs) + + +def draw_spectral(G, **kwargs): + """Draw the graph `G` with a spectral 2D layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.spectral_layout(G), **kwargs) + + For more information about how node positions are determined, see + `~networkx.drawing.layout.spectral_layout`. + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.spectral_layout` directly and reuse the result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.spectral_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.draw_spectral(G) + + See Also + -------- + :func:`~networkx.drawing.layout.spectral_layout` + """ + draw(G, pos=nx.spectral_layout(G), **kwargs) + + +def draw_spring(G, **kwargs): + """Draw the graph `G` with a spring layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.spring_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + `~networkx.drawing.layout.spring_layout` is also the default layout for + `draw`, so this function is equivalent to `draw`. + + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.spring_layout` directly and reuse the result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.spring_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(20) + >>> nx.draw_spring(G) + + See Also + -------- + draw + :func:`~networkx.drawing.layout.spring_layout` + """ + draw(G, pos=nx.spring_layout(G), **kwargs) + + +def draw_shell(G, nlist=None, **kwargs): + """Draw networkx graph `G` with shell layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.shell_layout(G, nlist=nlist), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + nlist : list of list of nodes, optional + A list containing lists of nodes representing the shells. + Default is `None`, meaning all nodes are in a single shell. + See `~networkx.drawing.layout.shell_layout` for details. + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Notes + ----- + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.shell_layout` directly and reuse the result:: + + >>> G = nx.complete_graph(5) + >>> pos = nx.shell_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(4) + >>> shells = [[0], [1, 2, 3]] + >>> nx.draw_shell(G, nlist=shells) + + See Also + -------- + :func:`~networkx.drawing.layout.shell_layout` + """ + draw(G, pos=nx.shell_layout(G, nlist=nlist), **kwargs) + + +def draw_planar(G, **kwargs): + """Draw a planar networkx graph `G` with planar layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.planar_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A planar networkx graph + + kwargs : optional keywords + See `draw_networkx` for a description of optional keywords. + + Raises + ------ + NetworkXException + When `G` is not planar + + Notes + ----- + The layout is computed each time this function is called. + For repeated drawing it is much more efficient to call + `~networkx.drawing.layout.planar_layout` directly and reuse the result:: + + >>> G = nx.path_graph(5) + >>> pos = nx.planar_layout(G) + >>> nx.draw(G, pos=pos) # Draw the original graph + >>> # Draw a subgraph, reusing the same node positions + >>> nx.draw(G.subgraph([0, 1, 2]), pos=pos, node_color="red") + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.draw_planar(G) + + See Also + -------- + :func:`~networkx.drawing.layout.planar_layout` + """ + draw(G, pos=nx.planar_layout(G), **kwargs) + + +def draw_forceatlas2(G, **kwargs): + """Draw a networkx graph with forceatlas2 layout. + + This is a convenience function equivalent to:: + + nx.draw(G, pos=nx.forceatlas2_layout(G), **kwargs) + + Parameters + ---------- + G : graph + A networkx graph + + kwargs : optional keywords + See networkx.draw_networkx() for a description of optional keywords, + with the exception of the pos parameter which is not used by this + function. + """ + draw(G, pos=nx.forceatlas2_layout(G), **kwargs) + + +def apply_alpha(colors, alpha, elem_list, cmap=None, vmin=None, vmax=None): + """Apply an alpha (or list of alphas) to the colors provided. + + Parameters + ---------- + + colors : color string or array of floats (default='r') + Color of element. Can be a single color format string, + or a sequence of colors with the same length as nodelist. + If numeric values are specified they will be mapped to + colors using the cmap and vmin,vmax parameters. See + matplotlib.scatter for more details. + + alpha : float or array of floats + Alpha values for elements. This can be a single alpha value, in + which case it will be applied to all the elements of color. Otherwise, + if it is an array, the elements of alpha will be applied to the colors + in order (cycling through alpha multiple times if necessary). + + elem_list : array of networkx objects + The list of elements which are being colored. These could be nodes, + edges or labels. + + cmap : matplotlib colormap + Color map for use if colors is a list of floats corresponding to points + on a color mapping. + + vmin, vmax : float + Minimum and maximum values for normalizing colors if a colormap is used + + Returns + ------- + + rgba_colors : numpy ndarray + Array containing RGBA format values for each of the node colours. + + """ + from itertools import cycle, islice + + import matplotlib as mpl + import matplotlib.cm # call as mpl.cm + import matplotlib.colors # call as mpl.colors + import numpy as np + + # If we have been provided with a list of numbers as long as elem_list, + # apply the color mapping. + if len(colors) == len(elem_list) and isinstance(colors[0], Number): + mapper = mpl.cm.ScalarMappable(cmap=cmap) + mapper.set_clim(vmin, vmax) + rgba_colors = mapper.to_rgba(colors) + # Otherwise, convert colors to matplotlib's RGB using the colorConverter + # object. These are converted to numpy ndarrays to be consistent with the + # to_rgba method of ScalarMappable. + else: + try: + rgba_colors = np.array([mpl.colors.colorConverter.to_rgba(colors)]) + except ValueError: + rgba_colors = np.array( + [mpl.colors.colorConverter.to_rgba(color) for color in colors] + ) + # Set the final column of the rgba_colors to have the relevant alpha values + try: + # If alpha is longer than the number of colors, resize to the number of + # elements. Also, if rgba_colors.size (the number of elements of + # rgba_colors) is the same as the number of elements, resize the array, + # to avoid it being interpreted as a colormap by scatter() + if len(alpha) > len(rgba_colors) or rgba_colors.size == len(elem_list): + rgba_colors = np.resize(rgba_colors, (len(elem_list), 4)) + rgba_colors[1:, 0] = rgba_colors[0, 0] + rgba_colors[1:, 1] = rgba_colors[0, 1] + rgba_colors[1:, 2] = rgba_colors[0, 2] + rgba_colors[:, 3] = list(islice(cycle(alpha), len(rgba_colors))) + except TypeError: + rgba_colors[:, -1] = alpha + return rgba_colors diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d791c84fe6ec893708fc217e631b5b5f927bdb3c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_agraph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_agraph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c37e7776440eb531e037bedb16f6ceefd034de8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_agraph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_image_comparison_pylab_mpl.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_image_comparison_pylab_mpl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f8e35dda1cea6a520606323a8a9c5076e1a94fe Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_image_comparison_pylab_mpl.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_latex.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_latex.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0be3cc943979556063428709725e2f766878dfc9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_latex.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_layout.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_layout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95dcc76c16a59c766d97c3185ce0d4b4b18ff6bd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_layout.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pydot.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pydot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78e5d0f23fa8f618f2d1d0261f462be9b78795e5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pydot.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pylab.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pylab.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf07a5b1cc06e86889fd6ea336bd1ff226e6f818 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/__pycache__/test_pylab.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_empty_graph.png b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_empty_graph.png new file mode 100644 index 0000000000000000000000000000000000000000..5d2f4e7bf4671ba1958f3db1fe5b9111a0a1078d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_empty_graph.png differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_house_with_colors.png b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_house_with_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..08862d7ee56317f4e09e45991a15f320cb06b4ea Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_house_with_colors.png differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_labels_and_colors.png b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_labels_and_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..6d842bf1aa52197b41f22483fe4e2d189b5f4d66 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_labels_and_colors.png differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_shortest_path.png b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_shortest_path.png new file mode 100644 index 0000000000000000000000000000000000000000..bcce0ff95fd6fc298ddf59759584f740b2cfd024 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_display_shortest_path.png differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_house_with_colors.png b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_house_with_colors.png new file mode 100644 index 0000000000000000000000000000000000000000..31f4962eb651bc274bd11ced28300785475e2685 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/baseline/test_house_with_colors.png differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_agraph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_agraph.py new file mode 100644 index 0000000000000000000000000000000000000000..3aab7d440f92777c4726cb39bc2c0324beab44ab --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_agraph.py @@ -0,0 +1,237 @@ +"""Unit tests for PyGraphviz interface.""" + +import warnings + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, graphs_equal, nodes_equal + +pygraphviz = pytest.importorskip("pygraphviz") + + +class TestAGraph: + def build_graph(self, G): + edges = [("A", "B"), ("A", "C"), ("A", "C"), ("B", "C"), ("A", "D")] + G.add_edges_from(edges) + G.add_node("E") + G.graph["metal"] = "bronze" + return G + + def assert_equal(self, G1, G2): + assert nodes_equal(G1.nodes(), G2.nodes()) + assert edges_equal(G1.edges(), G2.edges(), directed=G1.is_directed()) + assert G1.graph["metal"] == G2.graph["metal"] + + @pytest.mark.parametrize( + "G", (nx.Graph(), nx.DiGraph(), nx.MultiGraph(), nx.MultiDiGraph()) + ) + def test_agraph_roundtripping(self, G, tmp_path): + G = self.build_graph(G) + A = nx.nx_agraph.to_agraph(G) + H = nx.nx_agraph.from_agraph(A) + self.assert_equal(G, H) + + fname = tmp_path / "test.dot" + nx.drawing.nx_agraph.write_dot(H, fname) + Hin = nx.nx_agraph.read_dot(fname) + self.assert_equal(H, Hin) + + fname = tmp_path / "fh_test.dot" + with open(fname, "w") as fh: + nx.drawing.nx_agraph.write_dot(H, fh) + + with open(fname) as fh: + Hin = nx.nx_agraph.read_dot(fh) + self.assert_equal(H, Hin) + + def test_from_agraph_name(self): + G = nx.Graph(name="test") + A = nx.nx_agraph.to_agraph(G) + H = nx.nx_agraph.from_agraph(A) + assert G.name == "test" + + @pytest.mark.parametrize( + "graph_class", (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph) + ) + def test_from_agraph_create_using(self, graph_class): + G = nx.path_graph(3) + A = nx.nx_agraph.to_agraph(G) + H = nx.nx_agraph.from_agraph(A, create_using=graph_class) + assert isinstance(H, graph_class) + + def test_from_agraph_named_edges(self): + # Create an AGraph from an existing (non-multi) Graph + G = nx.Graph() + G.add_nodes_from([0, 1]) + A = nx.nx_agraph.to_agraph(G) + # Add edge (+ name, given by key) to the AGraph + A.add_edge(0, 1, key="foo") + # Verify a.name roundtrips out to 'key' in from_agraph + H = nx.nx_agraph.from_agraph(A) + assert isinstance(H, nx.Graph) + assert ("0", "1", {"key": "foo"}) in H.edges(data=True) + + def test_to_agraph_with_nodedata(self): + G = nx.Graph() + G.add_node(1, color="red") + A = nx.nx_agraph.to_agraph(G) + assert dict(A.nodes()[0].attr) == {"color": "red"} + + @pytest.mark.parametrize("graph_class", (nx.Graph, nx.MultiGraph)) + def test_to_agraph_with_edgedata(self, graph_class): + G = graph_class() + G.add_nodes_from([0, 1]) + G.add_edge(0, 1, color="yellow") + A = nx.nx_agraph.to_agraph(G) + assert dict(A.edges()[0].attr) == {"color": "yellow"} + + def test_view_pygraphviz_path(self, tmp_path): + G = nx.complete_graph(3) + input_path = str(tmp_path / "graph.png") + out_path, A = nx.nx_agraph.view_pygraphviz(G, path=input_path, show=False) + assert out_path == input_path + # Ensure file is not empty + with open(input_path, "rb") as fh: + data = fh.read() + assert len(data) > 0 + + def test_view_pygraphviz_file_suffix(self, tmp_path): + G = nx.complete_graph(3) + path, A = nx.nx_agraph.view_pygraphviz(G, suffix=1, show=False) + assert path[-6:] == "_1.png" + + def test_view_pygraphviz(self): + G = nx.Graph() # "An empty graph cannot be drawn." + pytest.raises(nx.NetworkXException, nx.nx_agraph.view_pygraphviz, G) + G = nx.barbell_graph(4, 6) + nx.nx_agraph.view_pygraphviz(G, show=False) + + def test_view_pygraphviz_edgelabel(self): + G = nx.Graph() + G.add_edge(1, 2, weight=7) + G.add_edge(2, 3, weight=8) + path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="weight", show=False) + for edge in A.edges(): + assert edge.attr["weight"] in ("7", "8") + + def test_view_pygraphviz_callable_edgelabel(self): + G = nx.complete_graph(3) + + def foo_label(data): + return "foo" + + path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel=foo_label, show=False) + for edge in A.edges(): + assert edge.attr["label"] == "foo" + + def test_view_pygraphviz_multigraph_edgelabels(self): + G = nx.MultiGraph() + G.add_edge(0, 1, key=0, name="left_fork") + G.add_edge(0, 1, key=1, name="right_fork") + path, A = nx.nx_agraph.view_pygraphviz(G, edgelabel="name", show=False) + edges = A.edges() + assert len(edges) == 2 + for edge in edges: + assert edge.attr["label"].strip() in ("left_fork", "right_fork") + + def test_graph_with_reserved_keywords(self): + # test attribute/keyword clash case for #1582 + # node: n + # edges: u,v + G = nx.Graph() + G = self.build_graph(G) + G.nodes["E"]["n"] = "keyword" + G.edges[("A", "B")]["u"] = "keyword" + G.edges[("A", "B")]["v"] = "keyword" + A = nx.nx_agraph.to_agraph(G) + + def test_view_pygraphviz_no_added_attrs_to_input(self): + G = nx.complete_graph(2) + path, A = nx.nx_agraph.view_pygraphviz(G, show=False) + assert G.graph == {} + + @pytest.mark.xfail(reason="known bug in clean_attrs") + def test_view_pygraphviz_leaves_input_graph_unmodified(self): + G = nx.complete_graph(2) + # Add entries to graph dict that to_agraph handles specially + G.graph["node"] = {"width": "0.80"} + G.graph["edge"] = {"fontsize": "14"} + path, A = nx.nx_agraph.view_pygraphviz(G, show=False) + assert G.graph == {"node": {"width": "0.80"}, "edge": {"fontsize": "14"}} + + def test_graph_with_AGraph_attrs(self): + G = nx.complete_graph(2) + # Add entries to graph dict that to_agraph handles specially + G.graph["node"] = {"width": "0.80"} + G.graph["edge"] = {"fontsize": "14"} + path, A = nx.nx_agraph.view_pygraphviz(G, show=False) + # Ensure user-specified values are not lost + assert dict(A.node_attr)["width"] == "0.80" + assert dict(A.edge_attr)["fontsize"] == "14" + + def test_round_trip_empty_graph(self): + G = nx.Graph() + A = nx.nx_agraph.to_agraph(G) + H = nx.nx_agraph.from_agraph(A) + assert graphs_equal(G, H) + AA = nx.nx_agraph.to_agraph(H) + HH = nx.nx_agraph.from_agraph(AA) + assert graphs_equal(H, HH) + assert graphs_equal(G, HH) + + @pytest.mark.xfail(reason="integer->string node conversion in round trip") + def test_round_trip_integer_nodes(self): + G = nx.complete_graph(3) + A = nx.nx_agraph.to_agraph(G) + H = nx.nx_agraph.from_agraph(A) + assert graphs_equal(G, H) + + def test_graphviz_alias(self): + G = self.build_graph(nx.Graph()) + pos_graphviz = nx.nx_agraph.graphviz_layout(G) + pos_pygraphviz = nx.nx_agraph.pygraphviz_layout(G) + assert pos_graphviz == pos_pygraphviz + + @pytest.mark.parametrize("root", range(5)) + def test_pygraphviz_layout_root(self, root): + # NOTE: test depends on layout prog being deterministic + G = nx.complete_graph(5) + A = nx.nx_agraph.to_agraph(G) + # Get layout with root arg is not None + pygv_layout = nx.nx_agraph.pygraphviz_layout(G, prog="circo", root=root) + # Equivalent layout directly on AGraph + A.layout(args=f"-Groot={root}", prog="circo") + # Parse AGraph layout + a1_pos = tuple(float(v) for v in dict(A.get_node("1").attr)["pos"].split(",")) + assert pygv_layout[1] == a1_pos + + def test_2d_layout(self): + G = nx.Graph() + G = self.build_graph(G) + G.graph["dimen"] = 2 + pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato") + pos = list(pos.values()) + assert len(pos) == 5 + assert len(pos[0]) == 2 + + def test_3d_layout(self): + G = nx.Graph() + G = self.build_graph(G) + G.graph["dimen"] = 3 + pos = nx.nx_agraph.pygraphviz_layout(G, prog="neato") + pos = list(pos.values()) + assert len(pos) == 5 + assert len(pos[0]) == 3 + + def test_no_warnings_raised(self): + # Test that no warnings are raised when Networkx graph + # is converted to Pygraphviz graph and 'pos' + # attribute is given + G = nx.Graph() + G.add_node(0, pos=(0, 0)) + G.add_node(1, pos=(1, 1)) + A = nx.nx_agraph.to_agraph(G) + with warnings.catch_warnings(record=True) as record: + A.layout() + assert len(record) == 0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_image_comparison_pylab_mpl.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_image_comparison_pylab_mpl.py new file mode 100644 index 0000000000000000000000000000000000000000..2d874f4af1e5fa49e5936fe9940c5c1cefacf708 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_image_comparison_pylab_mpl.py @@ -0,0 +1,229 @@ +"""Unit tests for explicit image comparison with pytest-mpl.""" + +import pytest + +import networkx as nx + +pytest.importorskip("pytest_mpl") + +mpl = pytest.importorskip("matplotlib") +mpl.use("PS") +plt = pytest.importorskip("matplotlib.pyplot") +plt.rcParams["text.usetex"] = False +np = pytest.importorskip("numpy") + + +@pytest.mark.mpl_image_compare +def test_display_house_with_colors(): + """ + Originally, I wanted to use the exact samge image as test_house_with_colors. + But I can't seem to find the correct value for the margins to get the figures + to line up perfectly. To the human eye, these visualizations are basically the + same. + """ + G = nx.house_graph() + fig, ax = plt.subplots() + nx.set_node_attributes( + G, {0: (0, 0), 1: (1, 0), 2: (0, 1), 3: (1, 1), 4: (0.5, 2.0)}, "pos" + ) + nx.set_node_attributes( + G, + { + n: { + "size": 3000 if n != 4 else 2000, + "color": "tab:blue" if n != 4 else "tab:orange", + } + for n in G.nodes() + }, + ) + nx.display( + G, + node_pos="pos", + edge_alpha=0.5, + edge_width=6, + node_label=None, + node_border_color="k", + ) + ax.margins(0.17) + plt.tight_layout() + plt.axis("off") + return fig + + +@pytest.mark.mpl_image_compare +def test_display_labels_and_colors(): + """See 'Labels and Colors' gallery example""" + fig, ax = plt.subplots() + G = nx.cubical_graph() + pos = nx.spring_layout(G, seed=3113794652) # positions for all nodes + nx.set_node_attributes(G, pos, "pos") # Will not be needed after PR 7571 + labels = iter( + [ + r"$a$", + r"$b$", + r"$c$", + r"$d$", + r"$\alpha$", + r"$\beta$", + r"$\gamma$", + r"$\delta$", + ] + ) + nx.set_node_attributes( + G, + { + n: { + "size": 800, + "alpha": 0.9, + "color": "tab:red" if n < 4 else "tab:blue", + "label": {"label": next(labels), "size": 22, "color": "whitesmoke"}, + } + for n in G.nodes() + }, + ) + + nx.display(G, node_pos="pos", edge_color="tab:grey") + + # The tricky bit is the highlighted colors for the edges + edgelist = [(0, 1), (1, 2), (2, 3), (0, 3)] + nx.set_edge_attributes( + G, + { + (u, v): { + "width": 8, + "alpha": 0.5, + "color": "tab:red", + "visible": (u, v) in edgelist, + } + for u, v in G.edges() + }, + ) + nx.display(G, node_pos="pos", node_visible=False) + edgelist = [(4, 5), (5, 6), (6, 7), (4, 7)] + nx.set_edge_attributes( + G, + { + (u, v): { + "color": "tab:blue", + "visible": (u, v) in edgelist, + } + for u, v in G.edges() + }, + ) + nx.display(G, node_pos="pos", node_visible=False) + + plt.tight_layout() + plt.axis("off") + return fig + + +@pytest.mark.mpl_image_compare +def test_display_complex(): + import itertools as it + + fig, ax = plt.subplots() + G = nx.MultiDiGraph() + nodes = "ABC" + prod = list(it.product(nodes, repeat=2)) * 4 + G = nx.MultiDiGraph() + for i, (u, v) in enumerate(prod): + G.add_edge(u, v, w=round(i / 3, 2)) + nx.set_node_attributes(G, nx.spring_layout(G, seed=3113794652), "pos") + csi = it.cycle([f"arc3,rad={r}" for r in it.accumulate([0.15] * 4)]) + nx.set_edge_attributes(G, {e: next(csi) for e in G.edges(keys=True)}, "curvature") + nx.set_edge_attributes( + G, + { + tuple(e): {"label": w, "bbox": {"alpha": 0}} + for *e, w in G.edges(keys=True, data="w") + }, + "label", + ) + nx.apply_matplotlib_colors(G, "w", "color", mpl.colormaps["inferno"], nodes=False) + nx.display(G, canvas=ax, node_pos="pos") + + plt.tight_layout() + plt.axis("off") + return fig + + +@pytest.mark.mpl_image_compare +def test_display_shortest_path(): + fig, ax = plt.subplots() + G = nx.Graph() + G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H"]) + G.add_edge("A", "B", weight=4) + G.add_edge("A", "H", weight=8) + G.add_edge("B", "C", weight=8) + G.add_edge("B", "H", weight=11) + G.add_edge("C", "D", weight=7) + G.add_edge("C", "F", weight=4) + G.add_edge("C", "I", weight=2) + G.add_edge("D", "E", weight=9) + G.add_edge("D", "F", weight=14) + G.add_edge("E", "F", weight=10) + G.add_edge("F", "G", weight=2) + G.add_edge("G", "H", weight=1) + G.add_edge("G", "I", weight=6) + G.add_edge("H", "I", weight=7) + + # Find the shortest path from node A to node E + path = nx.shortest_path(G, "A", "E", weight="weight") + + # Create a list of edges in the shortest path + path_edges = list(zip(path, path[1:])) + nx.set_node_attributes(G, nx.spring_layout(G, seed=37), "pos") + nx.set_edge_attributes( + G, + { + (u, v): { + "color": ( + "red" + if (u, v) in path_edges or tuple(reversed((u, v))) in path_edges + else "black" + ), + "label": {"label": d["weight"], "rotate": False}, + } + for u, v, d in G.edges(data=True) + }, + ) + nx.display(G, canvas=ax) + plt.tight_layout() + plt.axis("off") + return fig + + +@pytest.mark.mpl_image_compare +def test_display_empty_graph(): + G = nx.empty_graph() + fig, ax = plt.subplots() + nx.display(G, canvas=ax) + plt.tight_layout() + plt.axis("off") + return fig + + +@pytest.mark.mpl_image_compare +def test_house_with_colors(): + G = nx.house_graph() + # explicitly set positions + fig, ax = plt.subplots() + pos = {0: (0, 0), 1: (1, 0), 2: (0, 1), 3: (1, 1), 4: (0.5, 2.0)} + + # Plot nodes with different properties for the "wall" and "roof" nodes + nx.draw_networkx_nodes( + G, + pos, + node_size=3000, + nodelist=[0, 1, 2, 3], + node_color="tab:blue", + ) + nx.draw_networkx_nodes( + G, pos, node_size=2000, nodelist=[4], node_color="tab:orange" + ) + nx.draw_networkx_edges(G, pos, alpha=0.5, width=6) + # Customize axes + ax.margins(0.11) + plt.tight_layout() + plt.axis("off") + return fig diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_latex.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_latex.py new file mode 100644 index 0000000000000000000000000000000000000000..6ec9b07ab5888d039e719c672be4526597ba8a94 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_latex.py @@ -0,0 +1,285 @@ +import pytest + +import networkx as nx + + +def test_tikz_attributes(): + G = nx.path_graph(4, create_using=nx.DiGraph) + pos = {n: (n, n) for n in G} + + G.add_edge(0, 0) + G.edges[(0, 0)]["label"] = "Loop" + G.edges[(0, 0)]["label_options"] = "midway" + + G.nodes[0]["style"] = "blue" + G.nodes[1]["style"] = "line width=3,draw" + G.nodes[2]["style"] = "circle,draw,blue!50" + G.nodes[3]["label"] = "Stop" + G.edges[(0, 1)]["label"] = "1st Step" + G.edges[(0, 1)]["label_options"] = "near end" + G.edges[(2, 3)]["label"] = "3rd Step" + G.edges[(2, 3)]["label_options"] = "near start" + G.edges[(2, 3)]["style"] = "bend left,green" + G.edges[(1, 2)]["label"] = "2nd" + G.edges[(1, 2)]["label_options"] = "pos=0.5" + G.edges[(1, 2)]["style"] = ">->,bend right,line width=3,green!90" + + output_tex = nx.to_latex( + G, + pos=pos, + as_document=False, + tikz_options="[scale=3]", + node_options="style", + edge_options="style", + node_label="label", + edge_label="label", + edge_label_options="label_options", + ) + expected_tex = r"""\begin{figure} + \begin{tikzpicture}[scale=3] + \draw + (0, 0) node[blue] (0){0} + (1, 1) node[line width=3,draw] (1){1} + (2, 2) node[circle,draw,blue!50] (2){2} + (3, 3) node (3){Stop}; + \begin{scope}[->] + \draw (0) to node[near end] {1st Step} (1); + \draw[loop,] (0) to node[midway] {Loop} (0); + \draw[>->,bend right,line width=3,green!90] (1) to node[pos=0.5] {2nd} (2); + \draw[bend left,green] (2) to node[near start] {3rd Step} (3); + \end{scope} + \end{tikzpicture} +\end{figure}""" + + # First, check for consistency line-by-line - if this fails, the mismatched + # line will be shown explicitly in the failure summary + for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")): + assert expected == actual + + assert output_tex == expected_tex + + +def test_basic_multiple_graphs(): + H1 = nx.path_graph(4) + H2 = nx.complete_graph(4) + H3 = nx.path_graph(8) + H4 = nx.complete_graph(8) + captions = [ + "Path on 4 nodes", + "Complete graph on 4 nodes", + "Path on 8 nodes", + "Complete graph on 8 nodes", + ] + labels = ["fig2a", "fig2b", "fig2c", "fig2d"] + latex_code = nx.to_latex( + [H1, H2, H3, H4], + n_rows=2, + sub_captions=captions, + sub_labels=labels, + ) + assert "begin{document}" in latex_code + assert "begin{figure}" in latex_code + assert latex_code.count("begin{subfigure}") == 4 + assert latex_code.count("tikzpicture") == 8 + assert latex_code.count("[-]") == 4 + + +def test_basic_tikz(): + expected_tex = r"""\documentclass{report} +\usepackage{tikz} +\usepackage{subcaption} + +\begin{document} +\begin{figure} + \begin{subfigure}{0.5\textwidth} + \begin{tikzpicture}[scale=2] + \draw[gray!90] + (0.749, 0.702) node[red!90] (0){0} + (1.0, -0.014) node[red!90] (1){1} + (-0.777, -0.705) node (2){2} + (-0.984, 0.042) node (3){3} + (-0.028, 0.375) node[cyan!90] (4){4} + (-0.412, 0.888) node (5){5} + (0.448, -0.856) node (6){6} + (0.003, -0.431) node[cyan!90] (7){7}; + \begin{scope}[->,gray!90] + \draw (0) to (4); + \draw (0) to (5); + \draw (0) to (6); + \draw (0) to (7); + \draw (1) to (4); + \draw (1) to (5); + \draw (1) to (6); + \draw (1) to (7); + \draw (2) to (4); + \draw (2) to (5); + \draw (2) to (6); + \draw (2) to (7); + \draw (3) to (4); + \draw (3) to (5); + \draw (3) to (6); + \draw (3) to (7); + \end{scope} + \end{tikzpicture} + \caption{My tikz number 1 of 2}\label{tikz_1_2} + \end{subfigure} + \begin{subfigure}{0.5\textwidth} + \begin{tikzpicture}[scale=2] + \draw[gray!90] + (0.749, 0.702) node[green!90] (0){0} + (1.0, -0.014) node[green!90] (1){1} + (-0.777, -0.705) node (2){2} + (-0.984, 0.042) node (3){3} + (-0.028, 0.375) node[purple!90] (4){4} + (-0.412, 0.888) node (5){5} + (0.448, -0.856) node (6){6} + (0.003, -0.431) node[purple!90] (7){7}; + \begin{scope}[->,gray!90] + \draw (0) to (4); + \draw (0) to (5); + \draw (0) to (6); + \draw (0) to (7); + \draw (1) to (4); + \draw (1) to (5); + \draw (1) to (6); + \draw (1) to (7); + \draw (2) to (4); + \draw (2) to (5); + \draw (2) to (6); + \draw (2) to (7); + \draw (3) to (4); + \draw (3) to (5); + \draw (3) to (6); + \draw (3) to (7); + \end{scope} + \end{tikzpicture} + \caption{My tikz number 2 of 2}\label{tikz_2_2} + \end{subfigure} + \caption{A graph generated with python and latex.} +\end{figure} +\end{document}""" + + edges = [ + (0, 4), + (0, 5), + (0, 6), + (0, 7), + (1, 4), + (1, 5), + (1, 6), + (1, 7), + (2, 4), + (2, 5), + (2, 6), + (2, 7), + (3, 4), + (3, 5), + (3, 6), + (3, 7), + ] + G = nx.DiGraph() + G.add_nodes_from(range(8)) + G.add_edges_from(edges) + pos = { + 0: (0.7490296171687696, 0.702353520257394), + 1: (1.0, -0.014221357723796535), + 2: (-0.7765783344161441, -0.7054170966808919), + 3: (-0.9842690223417624, 0.04177547602465483), + 4: (-0.02768523817180917, 0.3745724439551441), + 5: (-0.41154855146767433, 0.8880106515525136), + 6: (0.44780153389148264, -0.8561492709269164), + 7: (0.0032499953371383505, -0.43092436645809945), + } + + rc_node_color = {0: "red!90", 1: "red!90", 4: "cyan!90", 7: "cyan!90"} + gp_node_color = {0: "green!90", 1: "green!90", 4: "purple!90", 7: "purple!90"} + + H = G.copy() + nx.set_node_attributes(G, rc_node_color, "color") + nx.set_node_attributes(H, gp_node_color, "color") + + sub_captions = ["My tikz number 1 of 2", "My tikz number 2 of 2"] + sub_labels = ["tikz_1_2", "tikz_2_2"] + + output_tex = nx.to_latex( + [G, H], + [pos, pos], + tikz_options="[scale=2]", + default_node_options="gray!90", + default_edge_options="gray!90", + node_options="color", + sub_captions=sub_captions, + sub_labels=sub_labels, + caption="A graph generated with python and latex.", + n_rows=2, + as_document=True, + ) + + # First, check for consistency line-by-line - if this fails, the mismatched + # line will be shown explicitly in the failure summary + for expected, actual in zip(expected_tex.split("\n"), output_tex.split("\n")): + assert expected == actual + # Double-check for document-level consistency + assert output_tex == expected_tex + + +def test_exception_pos_single_graph(to_latex=nx.to_latex): + # smoke test that pos can be a string + G = nx.path_graph(4) + to_latex(G, pos="pos") + + # must include all nodes + pos = {0: (1, 2), 1: (0, 1), 2: (2, 1)} + with pytest.raises(nx.NetworkXError): + to_latex(G, pos) + + # must have 2 values + pos[3] = (1, 2, 3) + with pytest.raises(nx.NetworkXError): + to_latex(G, pos) + pos[3] = 2 + with pytest.raises(nx.NetworkXError): + to_latex(G, pos) + + # check that passes with 2 values + pos[3] = (3, 2) + to_latex(G, pos) + + +def test_exception_multiple_graphs(to_latex=nx.to_latex): + G = nx.path_graph(3) + pos_bad = {0: (1, 2), 1: (0, 1)} + pos_OK = {0: (1, 2), 1: (0, 1), 2: (2, 1)} + fourG = [G, G, G, G] + fourpos = [pos_OK, pos_OK, pos_OK, pos_OK] + + # input single dict to use for all graphs + to_latex(fourG, pos_OK) + with pytest.raises(nx.NetworkXError): + to_latex(fourG, pos_bad) + + # input list of dicts to use for all graphs + to_latex(fourG, fourpos) + with pytest.raises(nx.NetworkXError): + to_latex(fourG, [pos_bad, pos_bad, pos_bad, pos_bad]) + + # every pos dict must include all nodes + with pytest.raises(nx.NetworkXError): + to_latex(fourG, [pos_OK, pos_OK, pos_bad, pos_OK]) + + # test sub_captions and sub_labels (len must match Gbunch) + with pytest.raises(nx.NetworkXError): + to_latex(fourG, fourpos, sub_captions=["hi", "hi"]) + + with pytest.raises(nx.NetworkXError): + to_latex(fourG, fourpos, sub_labels=["hi", "hi"]) + + # all pass + to_latex(fourG, fourpos, sub_captions=["hi"] * 4, sub_labels=["lbl"] * 4) + + +def test_exception_multigraph(): + G = nx.path_graph(4, create_using=nx.MultiGraph) + G.add_edge(1, 2) + with pytest.raises(nx.NetworkXNotImplemented): + nx.to_latex(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_layout.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_layout.py new file mode 100644 index 0000000000000000000000000000000000000000..34d71efa6050d7ea45df1a5ac3227244a361f84f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_layout.py @@ -0,0 +1,631 @@ +"""Unit tests for layout functions.""" + +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +class TestLayout: + @classmethod + def setup_class(cls): + cls.Gi = nx.grid_2d_graph(5, 5) + cls.Gs = nx.Graph() + nx.add_path(cls.Gs, "abcdef") + cls.bigG = nx.grid_2d_graph(25, 25) # > 500 nodes for sparse + + def test_spring_fixed_without_pos(self): + G = nx.path_graph(4) + # No pos dict at all + with pytest.raises(ValueError, match="nodes are fixed without positions"): + nx.spring_layout(G, fixed=[0]) + + pos = {0: (1, 1), 2: (0, 0)} + # Node 1 not in pos dict + with pytest.raises(ValueError, match="nodes are fixed without positions"): + nx.spring_layout(G, fixed=[0, 1], pos=pos) + + # All fixed nodes in pos dict + out = nx.spring_layout(G, fixed=[0, 2], pos=pos) # No ValueError + assert all(np.array_equal(out[n], pos[n]) for n in (0, 2)) + + def test_spring_init_pos(self): + # Tests GH #2448 + import math + + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (2, 0), (2, 3)]) + + init_pos = {0: (0.0, 0.0)} + fixed_pos = [0] + pos = nx.fruchterman_reingold_layout(G, pos=init_pos, fixed=fixed_pos) + has_nan = any(math.isnan(c) for coords in pos.values() for c in coords) + assert not has_nan, "values should not be nan" + + def test_smoke_empty_graph(self): + G = [] + nx.random_layout(G) + nx.circular_layout(G) + nx.planar_layout(G) + nx.spring_layout(G) + nx.fruchterman_reingold_layout(G) + nx.spectral_layout(G) + nx.shell_layout(G) + nx.bipartite_layout(G, G) + nx.spiral_layout(G) + nx.multipartite_layout(G) + nx.kamada_kawai_layout(G) + + def test_smoke_int(self): + G = self.Gi + nx.random_layout(G) + nx.circular_layout(G) + nx.planar_layout(G) + nx.spring_layout(G) + nx.forceatlas2_layout(G) + nx.fruchterman_reingold_layout(G) + nx.fruchterman_reingold_layout(self.bigG) + nx.spectral_layout(G) + nx.spectral_layout(G.to_directed()) + nx.spectral_layout(self.bigG) + nx.spectral_layout(self.bigG.to_directed()) + nx.shell_layout(G) + nx.spiral_layout(G) + nx.kamada_kawai_layout(G) + nx.kamada_kawai_layout(G, dim=1) + nx.kamada_kawai_layout(G, dim=3) + nx.arf_layout(G) + + def test_smoke_string(self): + G = self.Gs + nx.random_layout(G) + nx.circular_layout(G) + nx.planar_layout(G) + nx.spring_layout(G) + nx.forceatlas2_layout(G) + nx.fruchterman_reingold_layout(G) + nx.spectral_layout(G) + nx.shell_layout(G) + nx.spiral_layout(G) + nx.kamada_kawai_layout(G) + nx.kamada_kawai_layout(G, dim=1) + nx.kamada_kawai_layout(G, dim=3) + nx.arf_layout(G) + + def check_scale_and_center(self, pos, scale, center): + center = np.array(center) + low = center - scale + hi = center + scale + vpos = np.array(list(pos.values())) + length = vpos.max(0) - vpos.min(0) + assert (length <= 2 * scale).all() + assert (vpos >= low).all() + assert (vpos <= hi).all() + + def test_scale_and_center_arg(self): + sc = self.check_scale_and_center + c = (4, 5) + G = nx.complete_graph(9) + G.add_node(9) + sc(nx.random_layout(G, center=c), scale=0.5, center=(4.5, 5.5)) + # rest can have 2*scale length: [-scale, scale] + sc(nx.spring_layout(G, scale=2, center=c), scale=2, center=c) + sc(nx.spectral_layout(G, scale=2, center=c), scale=2, center=c) + sc(nx.circular_layout(G, scale=2, center=c), scale=2, center=c) + sc(nx.shell_layout(G, scale=2, center=c), scale=2, center=c) + sc(nx.spiral_layout(G, scale=2, center=c), scale=2, center=c) + sc(nx.kamada_kawai_layout(G, scale=2, center=c), scale=2, center=c) + + c = (2, 3, 5) + sc(nx.kamada_kawai_layout(G, dim=3, scale=2, center=c), scale=2, center=c) + + def test_planar_layout_non_planar_input(self): + G = nx.complete_graph(9) + pytest.raises(nx.NetworkXException, nx.planar_layout, G) + + def test_smoke_planar_layout_embedding_input(self): + embedding = nx.PlanarEmbedding() + embedding.set_data({0: [1, 2], 1: [0, 2], 2: [0, 1]}) + nx.planar_layout(embedding) + + def test_default_scale_and_center(self): + sc = self.check_scale_and_center + c = (0, 0) + G = nx.complete_graph(9) + G.add_node(9) + sc(nx.random_layout(G), scale=0.5, center=(0.5, 0.5)) + sc(nx.spring_layout(G), scale=1, center=c) + sc(nx.spectral_layout(G), scale=1, center=c) + sc(nx.circular_layout(G), scale=1, center=c) + sc(nx.shell_layout(G), scale=1, center=c) + sc(nx.spiral_layout(G), scale=1, center=c) + sc(nx.kamada_kawai_layout(G), scale=1, center=c) + + c = (0, 0, 0) + sc(nx.kamada_kawai_layout(G, dim=3), scale=1, center=c) + + def test_circular_planar_and_shell_dim_error(self): + G = nx.path_graph(4) + pytest.raises(ValueError, nx.circular_layout, G, dim=1) + pytest.raises(ValueError, nx.shell_layout, G, dim=1) + pytest.raises(ValueError, nx.shell_layout, G, dim=3) + pytest.raises(ValueError, nx.planar_layout, G, dim=1) + pytest.raises(ValueError, nx.planar_layout, G, dim=3) + + def test_adjacency_interface_numpy(self): + A = nx.to_numpy_array(self.Gs) + pos = nx.drawing.layout._fruchterman_reingold(A) + assert pos.shape == (6, 2) + pos = nx.drawing.layout._fruchterman_reingold(A, dim=3) + assert pos.shape == (6, 3) + pos = nx.drawing.layout._sparse_fruchterman_reingold(A) + assert pos.shape == (6, 2) + + def test_adjacency_interface_scipy(self): + A = nx.to_scipy_sparse_array(self.Gs, dtype="d") + pos = nx.drawing.layout._sparse_fruchterman_reingold(A) + assert pos.shape == (6, 2) + pos = nx.drawing.layout._sparse_spectral(A) + assert pos.shape == (6, 2) + pos = nx.drawing.layout._sparse_fruchterman_reingold(A, dim=3) + assert pos.shape == (6, 3) + + def test_single_nodes(self): + G = nx.path_graph(1) + vpos = nx.shell_layout(G) + assert not vpos[0].any() + G = nx.path_graph(4) + vpos = nx.shell_layout(G, [[0], [1, 2], [3]]) + assert not vpos[0].any() + assert vpos[3].any() # ensure node 3 not at origin (#3188) + assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753) + vpos = nx.shell_layout(G, [[0], [1, 2], [3]], rotate=0) + assert np.linalg.norm(vpos[3]) <= 1 # ensure node 3 fits (#3753) + + def test_smoke_initial_pos_forceatlas2(self): + pos = nx.circular_layout(self.Gi) + npos = nx.forceatlas2_layout(self.Gi, pos=pos) + + def test_smoke_initial_pos_fruchterman_reingold(self): + pos = nx.circular_layout(self.Gi) + npos = nx.fruchterman_reingold_layout(self.Gi, pos=pos) + + def test_smoke_initial_pos_arf(self): + pos = nx.circular_layout(self.Gi) + npos = nx.arf_layout(self.Gi, pos=pos) + + def test_fixed_node_fruchterman_reingold(self): + # Dense version (numpy based) + pos = nx.circular_layout(self.Gi) + npos = nx.spring_layout(self.Gi, pos=pos, fixed=[(0, 0)]) + assert tuple(pos[(0, 0)]) == tuple(npos[(0, 0)]) + # Sparse version (scipy based) + pos = nx.circular_layout(self.bigG) + npos = nx.spring_layout(self.bigG, pos=pos, fixed=[(0, 0)]) + for axis in range(2): + assert pos[(0, 0)][axis] == pytest.approx(npos[(0, 0)][axis], abs=1e-7) + + def test_center_parameter(self): + G = nx.path_graph(1) + nx.random_layout(G, center=(1, 1)) + vpos = nx.circular_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.planar_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.spring_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.fruchterman_reingold_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.spectral_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.shell_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + vpos = nx.spiral_layout(G, center=(1, 1)) + assert tuple(vpos[0]) == (1, 1) + + def test_center_wrong_dimensions(self): + G = nx.path_graph(1) + assert id(nx.spring_layout) == id(nx.fruchterman_reingold_layout) + pytest.raises(ValueError, nx.random_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.circular_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.planar_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.spring_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.spring_layout, G, dim=3, center=(1, 1)) + pytest.raises(ValueError, nx.spectral_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.spectral_layout, G, dim=3, center=(1, 1)) + pytest.raises(ValueError, nx.shell_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.spiral_layout, G, center=(1, 1, 1)) + pytest.raises(ValueError, nx.kamada_kawai_layout, G, center=(1, 1, 1)) + + def test_empty_graph(self): + G = nx.empty_graph() + vpos = nx.random_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.circular_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.planar_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.bipartite_layout(G, G) + assert vpos == {} + vpos = nx.spring_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.fruchterman_reingold_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.spectral_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.shell_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.spiral_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.multipartite_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.kamada_kawai_layout(G, center=(1, 1)) + assert vpos == {} + vpos = nx.forceatlas2_layout(G) + assert vpos == {} + vpos = nx.arf_layout(G) + assert vpos == {} + + def test_bipartite_layout(self): + G = nx.complete_bipartite_graph(3, 5) + top, bottom = nx.bipartite.sets(G) + + vpos = nx.bipartite_layout(G, top) + assert len(vpos) == len(G) + + top_x = vpos[list(top)[0]][0] + bottom_x = vpos[list(bottom)[0]][0] + for node in top: + assert vpos[node][0] == top_x + for node in bottom: + assert vpos[node][0] == bottom_x + + vpos = nx.bipartite_layout( + G, top, align="horizontal", center=(2, 2), scale=2, aspect_ratio=1 + ) + assert len(vpos) == len(G) + + top_y = vpos[list(top)[0]][1] + bottom_y = vpos[list(bottom)[0]][1] + for node in top: + assert vpos[node][1] == top_y + for node in bottom: + assert vpos[node][1] == bottom_y + + pytest.raises(ValueError, nx.bipartite_layout, G, top, align="foo") + + def test_multipartite_layout(self): + sizes = (0, 5, 7, 2, 8) + G = nx.complete_multipartite_graph(*sizes) + + vpos = nx.multipartite_layout(G) + assert len(vpos) == len(G) + + start = 0 + for n in sizes: + end = start + n + assert all(vpos[start][0] == vpos[i][0] for i in range(start + 1, end)) + start += n + + vpos = nx.multipartite_layout(G, align="horizontal", scale=2, center=(2, 2)) + assert len(vpos) == len(G) + + start = 0 + for n in sizes: + end = start + n + assert all(vpos[start][1] == vpos[i][1] for i in range(start + 1, end)) + start += n + + pytest.raises(ValueError, nx.multipartite_layout, G, align="foo") + + def test_kamada_kawai_costfn_1d(self): + costfn = nx.drawing.layout._kamada_kawai_costfn + + pos = np.array([4.0, 7.0]) + invdist = 1 / np.array([[0.1, 2.0], [2.0, 0.3]]) + + cost, grad = costfn(pos, np, invdist, meanweight=0, dim=1) + + assert cost == pytest.approx(((3 / 2.0 - 1) ** 2), abs=1e-7) + assert grad[0] == pytest.approx((-0.5), abs=1e-7) + assert grad[1] == pytest.approx(0.5, abs=1e-7) + + def check_kamada_kawai_costfn(self, pos, invdist, meanwt, dim): + costfn = nx.drawing.layout._kamada_kawai_costfn + + cost, grad = costfn(pos.ravel(), np, invdist, meanweight=meanwt, dim=dim) + + expected_cost = 0.5 * meanwt * np.sum(np.sum(pos, axis=0) ** 2) + for i in range(pos.shape[0]): + for j in range(i + 1, pos.shape[0]): + diff = np.linalg.norm(pos[i] - pos[j]) + expected_cost += (diff * invdist[i][j] - 1.0) ** 2 + + assert cost == pytest.approx(expected_cost, abs=1e-7) + + dx = 1e-4 + for nd in range(pos.shape[0]): + for dm in range(pos.shape[1]): + idx = nd * pos.shape[1] + dm + ps = pos.flatten() + + ps[idx] += dx + cplus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0] + + ps[idx] -= 2 * dx + cminus = costfn(ps, np, invdist, meanweight=meanwt, dim=pos.shape[1])[0] + + assert grad[idx] == pytest.approx((cplus - cminus) / (2 * dx), abs=1e-5) + + def test_kamada_kawai_costfn(self): + invdist = 1 / np.array([[0.1, 2.1, 1.7], [2.1, 0.2, 0.6], [1.7, 0.6, 0.3]]) + meanwt = 0.3 + + # 2d + pos = np.array([[1.3, -3.2], [2.7, -0.3], [5.1, 2.5]]) + + self.check_kamada_kawai_costfn(pos, invdist, meanwt, 2) + + # 3d + pos = np.array([[0.9, 8.6, -8.7], [-10, -0.5, -7.1], [9.1, -8.1, 1.6]]) + + self.check_kamada_kawai_costfn(pos, invdist, meanwt, 3) + + def test_spiral_layout(self): + G = self.Gs + + # a lower value of resolution should result in a more compact layout + # intuitively, the total distance from the start and end nodes + # via each node in between (transiting through each) will be less, + # assuming rescaling does not occur on the computed node positions + pos_standard = np.array(list(nx.spiral_layout(G, resolution=0.35).values())) + pos_tighter = np.array(list(nx.spiral_layout(G, resolution=0.34).values())) + distances = np.linalg.norm(pos_standard[:-1] - pos_standard[1:], axis=1) + distances_tighter = np.linalg.norm(pos_tighter[:-1] - pos_tighter[1:], axis=1) + assert sum(distances) > sum(distances_tighter) + + # return near-equidistant points after the first value if set to true + pos_equidistant = np.array(list(nx.spiral_layout(G, equidistant=True).values())) + distances_equidistant = np.linalg.norm( + pos_equidistant[:-1] - pos_equidistant[1:], axis=1 + ) + assert np.allclose( + distances_equidistant[1:], distances_equidistant[-1], atol=0.01 + ) + + def test_spiral_layout_equidistant(self): + G = nx.path_graph(10) + nx.spiral_layout(G, equidistant=True, store_pos_as="pos") + pos = nx.get_node_attributes(G, "pos") + # Extract individual node positions as an array + p = np.array(list(pos.values())) + # Elementwise-distance between node positions + dist = np.linalg.norm(p[1:] - p[:-1], axis=1) + assert np.allclose(np.diff(dist), 0, atol=1e-3) + + def test_forceatlas2_layout_partial_input_test(self): + # check whether partial pos input still returns a full proper position + G = self.Gs + node = nx.utils.arbitrary_element(G) + pos = nx.circular_layout(G) + del pos[node] + pos = nx.forceatlas2_layout(G, pos=pos) + assert len(pos) == len(G) + + def test_rescale_layout_dict(self): + G = nx.empty_graph() + vpos = nx.random_layout(G, center=(1, 1)) + assert nx.rescale_layout_dict(vpos) == {} + + G = nx.empty_graph(2) + vpos = {0: (0.0, 0.0), 1: (1.0, 1.0)} + s_vpos = nx.rescale_layout_dict(vpos) + assert np.linalg.norm([sum(x) for x in zip(*s_vpos.values())]) < 1e-6 + + G = nx.empty_graph(3) + vpos = {0: (0, 0), 1: (1, 1), 2: (0.5, 0.5)} + s_vpos = nx.rescale_layout_dict(vpos) + + expectation = { + 0: np.array((-1, -1)), + 1: np.array((1, 1)), + 2: np.array((0, 0)), + } + for k, v in expectation.items(): + assert (s_vpos[k] == v).all() + s_vpos = nx.rescale_layout_dict(vpos, scale=2) + expectation = { + 0: np.array((-2, -2)), + 1: np.array((2, 2)), + 2: np.array((0, 0)), + } + for k, v in expectation.items(): + assert (s_vpos[k] == v).all() + + def test_arf_layout_partial_input_test(self): + # Checks whether partial pos input still returns a proper position. + G = self.Gs + node = nx.utils.arbitrary_element(G) + pos = nx.circular_layout(G) + del pos[node] + pos = nx.arf_layout(G, pos=pos) + assert len(pos) == len(G) + + def test_arf_layout_negative_a_check(self): + """ + Checks input parameters correctly raises errors. For example, `a` should be larger than 1 + """ + G = self.Gs + pytest.raises(ValueError, nx.arf_layout, G=G, a=-1) + + def test_smoke_seed_input(self): + G = self.Gs + nx.random_layout(G, seed=42) + nx.spring_layout(G, seed=42) + nx.arf_layout(G, seed=42) + nx.forceatlas2_layout(G, seed=42) + + def test_node_at_center(self): + # see gh-7791 avoid divide by zero + G = nx.path_graph(3) + orig_pos = {i: [i - 1, 0.0] for i in range(3)} + new_pos = nx.forceatlas2_layout(G, pos=orig_pos) + + def test_initial_only_some_pos(self): + G = nx.path_graph(3) + orig_pos = {i: [i - 1, 0.0] for i in range(2)} + new_pos = nx.forceatlas2_layout(G, pos=orig_pos, seed=42) + + +def test_multipartite_layout_nonnumeric_partition_labels(): + """See gh-5123.""" + G = nx.Graph() + G.add_node(0, subset="s0") + G.add_node(1, subset="s0") + G.add_node(2, subset="s1") + G.add_node(3, subset="s1") + G.add_edges_from([(0, 2), (0, 3), (1, 2)]) + pos = nx.multipartite_layout(G) + assert len(pos) == len(G) + + +def test_multipartite_layout_layer_order(): + """Return the layers in sorted order if the layers of the multipartite + graph are sortable. See gh-5691""" + G = nx.Graph() + node_group = dict(zip(("a", "b", "c", "d", "e"), (2, 3, 1, 2, 4))) + for node, layer in node_group.items(): + G.add_node(node, subset=layer) + + # Horizontal alignment, therefore y-coord determines layers + pos = nx.multipartite_layout(G, align="horizontal") + + layers = nx.utils.groups(node_group) + pos_from_layers = nx.multipartite_layout(G, align="horizontal", subset_key=layers) + for (n1, p1), (n2, p2) in zip(pos.items(), pos_from_layers.items()): + assert n1 == n2 and (p1 == p2).all() + + # Nodes "a" and "d" are in the same layer + assert pos["a"][-1] == pos["d"][-1] + # positions should be sorted according to layer + assert pos["c"][-1] < pos["a"][-1] < pos["b"][-1] < pos["e"][-1] + + # Make sure that multipartite_layout still works when layers are not sortable + G.nodes["a"]["subset"] = "layer_0" # Can't sort mixed strs/ints + pos_nosort = nx.multipartite_layout(G) # smoke test: this should not raise + assert pos_nosort.keys() == pos.keys() + + +def _num_nodes_per_bfs_layer(pos): + """Helper function to extract the number of nodes in each layer of bfs_layout""" + x = np.array(list(pos.values()))[:, 0] # node positions in layered dimension + _, layer_count = np.unique(x, return_counts=True) + return layer_count + + +@pytest.mark.parametrize("n", range(2, 7)) +def test_bfs_layout_complete_graph(n): + """The complete graph should result in two layers: the starting node and + a second layer containing all neighbors.""" + G = nx.complete_graph(n) + nx.bfs_layout(G, start=0, store_pos_as="pos") + pos = nx.get_node_attributes(G, "pos") + assert np.array_equal(_num_nodes_per_bfs_layer(pos), [1, n - 1]) + + +def test_bfs_layout_barbell(): + G = nx.barbell_graph(5, 3) + # Start in one of the "bells" + pos = nx.bfs_layout(G, start=0) + # start, bell-1, [1] * len(bar)+1, bell-1 + expected_nodes_per_layer = [1, 4, 1, 1, 1, 1, 4] + assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer) + # Start in the other "bell" - expect same layer pattern + pos = nx.bfs_layout(G, start=12) + assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer) + # Starting in the center of the bar, expect layers to be symmetric + pos = nx.bfs_layout(G, start=6) + # Expected layers: {6 (start)}, {5, 7}, {4, 8}, {8 nodes from remainder of bells} + expected_nodes_per_layer = [1, 2, 2, 8] + assert np.array_equal(_num_nodes_per_bfs_layer(pos), expected_nodes_per_layer) + + +def test_bfs_layout_disconnected(): + G = nx.complete_graph(5) + G.add_edges_from([(10, 11), (11, 12)]) + with pytest.raises(nx.NetworkXError, match="bfs_layout didn't include all nodes"): + nx.bfs_layout(G, start=0) + + +def test_bipartite_layout_default_nodes_raises_non_bipartite_input(): + G = nx.complete_graph(5) + with pytest.raises(nx.NetworkXError, match="Graph is not bipartite"): + nx.bipartite_layout(G) + # No exception if nodes are explicitly specified + pos = nx.bipartite_layout(G, nodes=[2, 3]) + + +def test_bipartite_layout_default_nodes(): + G = nx.complete_bipartite_graph(3, 3) + pos = nx.bipartite_layout(G) # no nodes specified + # X coords of nodes should be the same within the bipartite sets + for nodeset in nx.bipartite.sets(G): + xs = [pos[k][0] for k in nodeset] + assert all(x == pytest.approx(xs[0]) for x in xs) + + +@pytest.mark.parametrize( + "layout", + [ + nx.random_layout, + nx.circular_layout, + nx.shell_layout, + nx.spring_layout, + nx.kamada_kawai_layout, + nx.spectral_layout, + nx.planar_layout, + nx.spiral_layout, + nx.forceatlas2_layout, + ], +) +def test_layouts_negative_dim(layout): + """Test all layouts that support dim kwarg handle invalid inputs.""" + G = nx.path_graph(4) + valid_err_msgs = "|".join( + [ + "negative dimensions.*not allowed", + "can only handle 2", + "cannot handle.*2", + ] + ) + with pytest.raises(ValueError, match=valid_err_msgs): + layout(G, dim=-1) + + +@pytest.mark.parametrize( + ("num_nodes", "expected_method"), [(100, "force"), (501, "energy")] +) +@pytest.mark.parametrize( + "extra_layout_kwargs", + [ + {}, # No extra kwargs + {"pos": {0: (0, 0)}, "fixed": [0]}, # Fixed node position + {"dim": 3}, # 3D layout + ], +) +def test_spring_layout_graph_size_heuristic( + num_nodes, expected_method, extra_layout_kwargs +): + """Expect 'force' layout for n < 500 and 'energy' for n >= 500""" + G = nx.cycle_graph(num_nodes) + # Seeded layout to compare explicit method to one determined by "auto" + seed = 163674319 + + # Compare explicit method to auto method + expected = nx.spring_layout( + G, method=expected_method, seed=seed, **extra_layout_kwargs + ) + actual = nx.spring_layout(G, method="auto", seed=seed, **extra_layout_kwargs) + assert np.allclose(list(expected.values()), list(actual.values()), atol=1e-5) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pydot.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pydot.py new file mode 100644 index 0000000000000000000000000000000000000000..acf93d77ec3e555207f8c02b5a9da00633382eed --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pydot.py @@ -0,0 +1,146 @@ +"""Unit tests for pydot drawing functions.""" + +from io import StringIO + +import pytest + +import networkx as nx +from networkx.utils import graphs_equal + +pydot = pytest.importorskip("pydot") + + +class TestPydot: + @pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) + @pytest.mark.parametrize("prog", ("neato", "dot")) + def test_pydot(self, G, prog, tmp_path): + """ + Validate :mod:`pydot`-based usage of the passed NetworkX graph with the + passed basename of an external GraphViz command (e.g., `dot`, `neato`). + """ + + # Set the name of this graph to... "G". Failing to do so will + # subsequently trip an assertion expecting this name. + G.graph["name"] = "G" + + # Add arbitrary nodes and edges to the passed empty graph. + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("A", "D")]) + G.add_node("E") + + # Validate layout of this graph with the passed GraphViz command. + graph_layout = nx.nx_pydot.pydot_layout(G, prog=prog) + assert isinstance(graph_layout, dict) + + # Convert this graph into a "pydot.Dot" instance. + P = nx.nx_pydot.to_pydot(G) + + # Convert this "pydot.Dot" instance back into a graph of the same type. + G2 = G.__class__(nx.nx_pydot.from_pydot(P)) + + # Validate the original and resulting graphs to be the same. + assert graphs_equal(G, G2) + + fname = tmp_path / "out.dot" + + # Serialize this "pydot.Dot" instance to a temporary file in dot format + P.write_raw(fname) + + # Deserialize a list of new "pydot.Dot" instances back from this file. + Pin_list = pydot.graph_from_dot_file(path=fname, encoding="utf-8") + + # Validate this file to contain only one graph. + assert len(Pin_list) == 1 + + # The single "pydot.Dot" instance deserialized from this file. + Pin = Pin_list[0] + + # Sorted list of all nodes in the original "pydot.Dot" instance. + n1 = sorted(p.get_name() for p in P.get_node_list()) + + # Sorted list of all nodes in the deserialized "pydot.Dot" instance. + n2 = sorted(p.get_name() for p in Pin.get_node_list()) + + # Validate these instances to contain the same nodes. + assert n1 == n2 + + # Sorted list of all edges in the original "pydot.Dot" instance. + e1 = sorted((e.get_source(), e.get_destination()) for e in P.get_edge_list()) + + # Sorted list of all edges in the original "pydot.Dot" instance. + e2 = sorted((e.get_source(), e.get_destination()) for e in Pin.get_edge_list()) + + # Validate these instances to contain the same edges. + assert e1 == e2 + + # Deserialize a new graph of the same type back from this file. + Hin = nx.nx_pydot.read_dot(fname) + Hin = G.__class__(Hin) + + # Validate the original and resulting graphs to be the same. + assert graphs_equal(G, Hin) + + def test_read_write(self): + G = nx.MultiGraph() + G.graph["name"] = "G" + G.add_edge("1", "2", key="0") # read assumes strings + fh = StringIO() + nx.nx_pydot.write_dot(G, fh) + fh.seek(0) + H = nx.nx_pydot.read_dot(fh) + assert graphs_equal(G, H) + + +def test_pydot_issue_7581(tmp_path): + """Validate that `nx_pydot.pydot_layout` handles nodes + with characters like "\n", " ". + + Those characters cause `pydot` to escape and quote them on output, + which caused #7581. + """ + G = nx.Graph() + G.add_edges_from([("A\nbig test", "B"), ("A\nbig test", "C"), ("B", "C")]) + + graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot") + assert isinstance(graph_layout, dict) + + # Convert the graph to pydot and back into a graph. There should be no difference. + P = nx.nx_pydot.to_pydot(G) + G2 = nx.Graph(nx.nx_pydot.from_pydot(P)) + assert graphs_equal(G, G2) + + +@pytest.mark.parametrize( + "graph_type", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph] +) +def test_hashable_pydot(graph_type): + # gh-5790 + G = graph_type() + G.add_edge("5", frozenset([1]), t='"Example:A"', l=False) + G.add_edge("1", 2, w=True, t=("node1",), l=frozenset(["node1"])) + G.add_edge("node", (3, 3), w="string") + + assert [ + {"t": '"Example:A"', "l": "False"}, + {"w": "True", "t": "('node1',)", "l": "frozenset({'node1'})"}, + {"w": "string"}, + ] == [ + attr + for _, _, attr in nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).edges.data() + ] + + assert {str(i) for i in G.nodes()} == set( + nx.nx_pydot.from_pydot(nx.nx_pydot.to_pydot(G)).nodes + ) + + +def test_pydot_numerical_name(): + G = nx.Graph() + G.add_edges_from([("A", "B"), (0, 1)]) + graph_layout = nx.nx_pydot.pydot_layout(G, prog="dot") + assert isinstance(graph_layout, dict) + assert "0" not in graph_layout + assert 0 in graph_layout + assert "1" not in graph_layout + assert 1 in graph_layout + assert "A" in graph_layout + assert "B" in graph_layout diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pylab.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pylab.py new file mode 100644 index 0000000000000000000000000000000000000000..d6cdd1455dfe3c408213c96f4e37728b57f0cd42 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/drawing/tests/test_pylab.py @@ -0,0 +1,1582 @@ +"""Unit tests for matplotlib drawing functions.""" + +import itertools +import os +import warnings + +import pytest + +import networkx as nx + +mpl = pytest.importorskip("matplotlib") +np = pytest.importorskip("numpy") +mpl.use("PS") +plt = pytest.importorskip("matplotlib.pyplot") +plt.rcParams["text.usetex"] = False + + +barbell = nx.barbell_graph(4, 6) + +defaults = { + "node_pos": None, + "node_visible": True, + "node_color": "#1f78b4", + "node_size": 300, + "node_label": { + "size": 12, + "color": "#000000", + "family": "sans-serif", + "weight": "normal", + "alpha": 1.0, + "background_color": None, + "background_alpha": None, + "h_align": "center", + "v_align": "center", + "bbox": None, + }, + "node_shape": "o", + "node_alpha": 1.0, + "node_border_width": 1.0, + "node_border_color": "face", + "edge_visible": True, + "edge_width": 1.0, + "edge_color": "#000000", + "edge_label": { + "size": 12, + "color": "#000000", + "family": "sans-serif", + "weight": "normal", + "alpha": 1.0, + "bbox": {"boxstyle": "round", "ec": (1.0, 1.0, 1.0), "fc": (1.0, 1.0, 1.0)}, + "h_align": "center", + "v_align": "center", + "pos": 0.5, + "rotate": True, + }, + "edge_style": "-", + "edge_alpha": 1.0, + # These are for undirected-graphs. Directed graphs shouls use "-|>" and 10, respectively + "edge_arrowstyle": "-", + "edge_arrowsize": 0, + "edge_curvature": "arc3", + "edge_source_margin": 0, + "edge_target_margin": 0, +} + + +@pytest.mark.parametrize( + ("param_name", "param_value", "expected"), + ( + ("node_color", None, defaults["node_color"]), + ("node_color", "#FF0000", "red"), + ("node_color", "color", "lime"), + ), +) +def test_display_arg_handling_node_color(param_name, param_value, expected): + G = nx.path_graph(4) + nx.set_node_attributes(G, "#00FF00", "color") + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas, **{param_name: param_value}) + assert mpl.colors.same_color(canvas.get_children()[0].get_edgecolors()[0], expected) + plt.close() + + +@pytest.mark.parametrize( + ("param_value", "expected"), + ( + (None, (1, 1, 1, 1)), # default value + (0.5, (0.5, 0.5, 0.5, 0.5)), + ("n_alpha", (1.0, 1 / 2, 1 / 3, 0.25)), + ), +) +def test_display_arg_handling_node_alpha(param_value, expected): + G = nx.path_graph(4) + nx.set_node_attributes(G, {n: 1 / (n + 1) for n in G.nodes()}, "n_alpha") + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas, node_alpha=param_value) + assert all( + canvas.get_children()[0].get_fc()[:, 3] == expected + ) # Extract just the alpha from the node colors + plt.close() + + +def test_display_node_position(): + G = nx.path_graph(4) + nx.set_node_attributes(G, {n: (n, n) for n in G.nodes()}, "pos") + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas, node_pos="pos") + assert np.all( + canvas.get_children()[0].get_offsets().data == [[0, 0], [1, 1], [2, 2], [3, 3]] + ) + plt.close() + + +def test_display_line_collection(): + G = nx.karate_club_graph() + nx.set_edge_attributes( + G, {(u, v): "-|>" if (u + v) % 2 else "-" for u, v in G.edges()}, "arrowstyle" + ) + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas, edge_arrowsize=10) + # There should only be one line collection in any given visualization + lc = [ + l + for l in canvas.get_children() + if isinstance(l, mpl.collections.LineCollection) + ][0] + assert len(lc.get_paths()) == sum([1 for u, v in G.edges() if (u + v) % 2]) + plt.close() + + +@pytest.mark.parametrize( + ("edge_color", "expected"), + ( + (None, "black"), + ("r", "red"), + ((1.0, 1.0, 0.0), "yellow"), + ((0, 1, 0, 1), "lime"), + ("color", "blue"), + ("#0000FF", "blue"), + ), +) +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_single_color(edge_color, expected, graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, "#0000FF", "color") + canvas = plt.figure().add_subplot(111) + nx.display(G, edge_color=edge_color, canvas=canvas) + if G.is_directed(): + colors = [ + f.get_fc() + for f in canvas.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + colors = [ + l + for l in canvas.collections + if isinstance(l, mpl.collections.LineCollection) + ][0].get_colors() + assert all(mpl.colors.same_color(c, expected) for c in colors) + plt.close() + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_multiple_colors(graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, {(0, 1): "#FF0000", (1, 2): (0, 0, 1)}, "color") + ax = plt.figure().add_subplot(111) + nx.display(G, canvas=ax) + expected = ["red", "blue"] + if G.is_directed(): + colors = [ + f.get_fc() + for f in ax.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + colors = [ + l for l in ax.collections if isinstance(l, mpl.collections.LineCollection) + ][0].get_colors() + assert mpl.colors.same_color(colors, expected) + plt.close() + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_position(graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_node_attributes(G, {n: (n, n) for n in G.nodes()}, "pos") + ax = plt.figure().add_subplot(111) + nx.display(G, canvas=ax) + if G.is_directed(): + end_points = [ + (f.get_path().vertices[0, :], f.get_path().vertices[-2, :]) + for f in ax.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + line_collection = [ + l for l in ax.collections if isinstance(l, mpl.collections.LineCollection) + ][0] + end_points = [ + (p.vertices[0, :], p.vertices[-1, :]) for p in line_collection.get_paths() + ] + expected = [((0, 0), (1, 1)), ((1, 1), (2, 2))] + # Use the threshold to account for slight shifts in FancyArrowPatch margins to + # avoid covering the arrow head with the node. + threshold = 0.05 + for a, e in zip(end_points, expected): + act_start, act_end = a + exp_start, exp_end = e + assert all(abs(act_start - exp_start) < (threshold, threshold)) and all( + abs(act_end - exp_end) < (threshold, threshold) + ) + plt.close() + + +def test_display_position_function(): + G = nx.karate_club_graph() + + def fixed_layout(G): + return nx.spring_layout(G, seed=314159) + + pos = fixed_layout(G) + ax = plt.figure().add_subplot(111) + nx.display(G, node_pos=fixed_layout, canvas=ax) + # rebuild the position dictionary from the canvas + act_pos = { + n: tuple(p) for n, p in zip(G.nodes(), ax.get_children()[0].get_offsets().data) + } + for n in G.nodes(): + assert all(pos[n] == act_pos[n]) + plt.close() + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_colormaps(graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, {(0, 1): 0, (1, 2): 1}, "weight") + cmap = mpl.colormaps["plasma"] + nx.apply_matplotlib_colors(G, "weight", "color", cmap, nodes=False) + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas) + mapper = mpl.cm.ScalarMappable(cmap=cmap) + mapper.set_clim(0, 1) + expected = [mapper.to_rgba(0), mapper.to_rgba(1)] + if G.is_directed(): + colors = [ + f.get_facecolor() + for f in canvas.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + colors = [ + l + for l in canvas.collections + if isinstance(l, mpl.collections.LineCollection) + ][0].get_colors() + assert mpl.colors.same_color(expected[0], G.edges[0, 1]["color"]) + assert mpl.colors.same_color(expected[1], G.edges[1, 2]["color"]) + assert mpl.colors.same_color(expected, colors) + plt.close() + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_node_colormaps(graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_node_attributes(G, {0: 0, 1: 0.5, 2: 1}, "weight") + cmap = mpl.colormaps["plasma"] + nx.apply_matplotlib_colors(G, "weight", "color", cmap) + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas) + mapper = mpl.cm.ScalarMappable(cmap=cmap) + mapper.set_clim(0, 1) + expected = [mapper.to_rgba(0), mapper.to_rgba(0.5), mapper.to_rgba(1)] + colors = [ + s for s in canvas.collections if isinstance(s, mpl.collections.PathCollection) + ][0].get_edgecolors() + assert mpl.colors.same_color(expected[0], G.nodes[0]["color"]) + assert mpl.colors.same_color(expected[1], G.nodes[1]["color"]) + assert mpl.colors.same_color(expected[2], G.nodes[2]["color"]) + assert mpl.colors.same_color(expected, colors) + plt.close() + + +@pytest.mark.parametrize( + ("param_value", "expected"), + ( + (None, [defaults["edge_width"], defaults["edge_width"]]), + (5, [5, 5]), + ("width", [5, 10]), + ), +) +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_width(param_value, expected, graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, {(0, 1): 5, (1, 2): 10}, "width") + canvas = plt.figure().add_subplot(111) + nx.display(G, edge_width=param_value, canvas=canvas) + if G.is_directed(): + widths = [ + f.get_linewidth() + for f in canvas.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + widths = list( + [ + l + for l in canvas.collections + if isinstance(l, mpl.collections.LineCollection) + ][0].get_linewidths() + ) + assert widths == expected + + +@pytest.mark.parametrize( + ("param_value", "expected"), + ( + (None, [defaults["edge_style"], defaults["edge_style"]]), + (":", [":", ":"]), + ("style", ["-", ":"]), + ), +) +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_display_edge_style(param_value, expected, graph_type): + G = nx.path_graph(3, create_using=graph_type) + nx.set_edge_attributes(G, {(0, 1): "-", (1, 2): ":"}, "style") + canvas = plt.figure().add_subplot(111) + nx.display(G, edge_style=param_value, canvas=canvas) + if G.is_directed(): + styles = [ + f.get_linestyle() + for f in canvas.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + else: + # Convert back from tuple description to character form + linestyles = {(0, None): "-", (0, (1, 1.65)): ":"} + styles = [ + linestyles[(s[0], tuple(s[1]) if s[1] is not None else None)] + for s in [ + l + for l in canvas.collections + if isinstance(l, mpl.collections.LineCollection) + ][0].get_linestyles() + ] + assert styles == expected + plt.close() + + +def test_display_node_labels(): + G = nx.path_graph(4) + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas, node_label={"size": 20}) + labels = [t for t in canvas.get_children() if isinstance(t, mpl.text.Text)] + for n, l in zip(G.nodes(), labels): + assert l.get_text() == str(n) + assert l.get_size() == 20.0 + plt.close() + + +def test_display_edge_labels(): + G = nx.path_graph(4) + canvas = plt.figure().add_subplot(111) + # While we can pass in dicts for edge label defaults without errors, + # this isn't helpful unless we want one label for all edges. + nx.set_edge_attributes(G, {(u, v): {"label": u + v} for u, v in G.edges()}) + nx.display(G, canvas=canvas, edge_label={"color": "r"}, node_label=None) + labels = [t for t in canvas.get_children() if isinstance(t, mpl.text.Text)] + print(labels) + for e, l in zip(G.edges(), labels): + assert l.get_text() == str(e[0] + e[1]) + assert l.get_color() == "r" + plt.close() + + +def test_display_multigraph_non_integer_keys(): + G = nx.MultiGraph() + G.add_nodes_from(["A", "B", "C", "D"]) + G.add_edges_from( + [ + ("A", "B", "0"), + ("A", "B", "1"), + ("B", "C", "-1"), + ("B", "C", "1"), + ("C", "D", "-1"), + ("C", "D", "0"), + ] + ) + nx.set_edge_attributes( + G, {e: f"arc3,rad={0.2 * int(e[2])}" for e in G.edges(keys=True)}, "curvature" + ) + canvas = plt.figure().add_subplot(111) + nx.display(G, canvas=canvas) + rads = [ + f.get_connectionstyle().rad + for f in canvas.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + assert rads == [0.0, 0.2, -0.2, 0.2, -0.2, 0.0] + plt.close() + + +def test_display_raises_for_bad_arg(): + G = nx.karate_club_graph() + with pytest.raises(nx.NetworkXError): + nx.display(G, bad_arg="bad_arg") + plt.close() + + +def test_display_arrow_size(): + G = nx.path_graph(4, create_using=nx.DiGraph) + nx.set_edge_attributes( + G, {(u, v): (u + v + 2) ** 2 for u, v in G.edges()}, "arrowsize" + ) + ax = plt.axes() + nx.display(G, canvas=ax) + assert [9, 25, 49] == [ + f.get_mutation_scale() + for f in ax.get_children() + if isinstance(f, mpl.patches.FancyArrowPatch) + ] + plt.close() + + +def test_display_mismatched_edge_position(): + """ + This test ensures that a error is raised for incomplete position data. + """ + G = nx.path_graph(5) + # Notice that there is no position for node 3 + nx.set_node_attributes(G, {0: (0, 0), 1: (1, 1), 2: (2, 2), 4: (4, 4)}, "pos") + # But that's not a problem since we don't want to show node 4, right? + nx.set_node_attributes(G, {n: n < 4 for n in G.nodes()}, "visible") + # However, if we try to visualize every edge (including 3 -> 4)... + # That's a problem since node 4 doesn't have a position + with pytest.raises(nx.NetworkXError): + nx.display(G) + + +# NOTE: parametrizing on marker to test both branches of internal +# to_marker_edge function +@pytest.mark.parametrize("node_shape", ("o", "s")) +def test_display_edge_margins(node_shape): + """ + Test that there is a wider gap between the node and the start of an + incident edge when min_source_margin is specified. + + This test checks that the use os min_{source/target}_margin edge + attributes result is shorter (more padding) between the edges and + source and target nodes. + + + As a crude visual example, let 's' and 't' represent source and target + nodes, respectively: + + Default: + s-----------------------------t + + With margins: + s ----------------------- t + + """ + ax = plt.figure().add_subplot(111) + G = nx.DiGraph([(0, 1)]) + nx.set_node_attributes(G, {0: (0, 0), 1: (1, 1)}, "pos") + # Get the default patches from the regular visualization + nx.display(G, canvas=ax, node_shape=node_shape) + default_arrow = [ + f for f in ax.get_children() if isinstance(f, mpl.patches.FancyArrowPatch) + ][0] + default_extent = default_arrow.get_extents().corners()[::2, 0] + # Now plot again with margins + ax = plt.figure().add_subplot(111) + nx.display( + G, + canvas=ax, + edge_source_margin=100, + edge_target_margin=100, + node_shape=node_shape, + ) + padded_arrow = [ + f for f in ax.get_children() if isinstance(f, mpl.patches.FancyArrowPatch) + ][0] + padded_extent = padded_arrow.get_extents().corners()[::2, 0] + + # With padding, the left-most extent of the edge should be further to the right + assert padded_extent[0] > default_extent[0] + # And the rightmost extent of the edge, further to the left + assert padded_extent[1] < default_extent[1] + plt.close() + + +@pytest.mark.parametrize("ticks", [False, True]) +def test_display_hide_ticks(ticks): + G = nx.path_graph(3) + nx.set_node_attributes(G, {n: (n, n) for n in G.nodes()}, "pos") + ax = plt.axes() + nx.display(G, hide_ticks=ticks) + for axis in [ax.xaxis, ax.yaxis]: + assert bool(axis.get_ticklabels()) != ticks + + plt.close() + + +def test_display_self_loop(): + ax = plt.axes() + G = nx.DiGraph() + G.add_node(0) + G.add_edge(0, 0) + nx.set_node_attributes(G, {0: (0, 0)}, "pos") + nx.display(G, canvas=ax) + arrow = [ + f for f in ax.get_children() if isinstance(f, mpl.patches.FancyArrowPatch) + ][0] + bbox = arrow.get_extents() + print(bbox.width) + print(bbox.height) + assert bbox.width > 0 and bbox.height > 0 + + plt.delaxes(ax) + plt.close() + + +def test_display_remove_pos_attr(): + """ + If the pos attribute isn't provided or is a function, display computes the layout + and adds it to the graph. We need to ensure that this new attribute is removed from + the returned graph. + """ + G = nx.karate_club_graph() + nx.display(G) + assert nx.get_node_attributes(G, "display's position attribute name") == {} + + +@pytest.fixture +def subplots(): + fig, ax = plt.subplots() + yield fig, ax + plt.delaxes(ax) + plt.close() + + +@pytest.mark.parametrize( + "function", + [ + nx.draw_circular, + nx.draw_kamada_kawai, + nx.draw_planar, + nx.draw_random, + nx.draw_spectral, + nx.draw_spring, + nx.draw_shell, + nx.draw_forceatlas2, + ], +) +def test_draw(function, subplots, tmp_path): + if function == nx.draw_kamada_kawai: + pytest.importorskip("scipy", reason="draw_kamada_kawai requires scipy") + fig, _ = subplots + options = {"node_color": "black", "node_size": 100, "width": 3} + function(barbell, **options) + fig.savefig(tmp_path / "test.ps") + + +def test_draw_shell_nlist(subplots, tmp_path): + fig, _ = subplots + nlist = [list(range(4)), list(range(4, 10)), list(range(10, 14))] + nx.draw_shell(barbell, nlist=nlist) + fig.savefig(tmp_path / "test.ps") + + +def test_draw_bipartite(subplots, tmp_path): + fig, _ = subplots + G = nx.complete_bipartite_graph(2, 5) + nx.draw_bipartite(G) + fig.savefig(tmp_path / "test.ps") + + +def test_edge_colormap(): + colors = range(barbell.number_of_edges()) + nx.draw_spring( + barbell, edge_color=colors, width=4, edge_cmap=plt.cm.Blues, with_labels=True + ) + # plt.show() + + +def test_draw_networkx_edge_labels(subplots, tmp_path): + fig, _ = subplots + edge = (0, 1) + G = nx.DiGraph([edge]) + pos = {n: (n, n) for n in G} + nx.draw(G, pos=pos) + nx.draw_networkx_edge_labels(G, pos, edge_labels={edge: "edge"}) + fig.savefig(tmp_path / "test.ps") + + +def test_arrows(): + nx.draw_spring(barbell.to_directed()) + # plt.show() + + +@pytest.mark.parametrize( + ("edge_color", "expected"), + ( + (None, "black"), # Default + ("r", "red"), # Non-default color string + (["r"], "red"), # Single non-default color in a list + ((1.0, 1.0, 0.0), "yellow"), # single color as rgb tuple + ([(1.0, 1.0, 0.0)], "yellow"), # single color as rgb tuple in list + ((0, 1, 0, 1), "lime"), # single color as rgba tuple + ([(0, 1, 0, 1)], "lime"), # single color as rgba tuple in list + ("#0000ff", "blue"), # single color hex code + (["#0000ff"], "blue"), # hex code in list + ), +) +@pytest.mark.parametrize("edgelist", (None, [(0, 1)])) +def test_single_edge_color_undirected(edge_color, expected, edgelist): + """Tests ways of specifying all edges have a single color for edges + drawn with a LineCollection""" + + G = nx.path_graph(3) + drawn_edges = nx.draw_networkx_edges( + G, pos=nx.random_layout(G), edgelist=edgelist, edge_color=edge_color + ) + assert mpl.colors.same_color(drawn_edges.get_color(), expected) + + +@pytest.mark.parametrize( + ("edge_color", "expected"), + ( + (None, "black"), # Default + ("r", "red"), # Non-default color string + (["r"], "red"), # Single non-default color in a list + ((1.0, 1.0, 0.0), "yellow"), # single color as rgb tuple + ([(1.0, 1.0, 0.0)], "yellow"), # single color as rgb tuple in list + ((0, 1, 0, 1), "lime"), # single color as rgba tuple + ([(0, 1, 0, 1)], "lime"), # single color as rgba tuple in list + ("#0000ff", "blue"), # single color hex code + (["#0000ff"], "blue"), # hex code in list + ), +) +@pytest.mark.parametrize("edgelist", (None, [(0, 1)])) +def test_single_edge_color_directed(edge_color, expected, edgelist): + """Tests ways of specifying all edges have a single color for edges drawn + with FancyArrowPatches""" + + G = nx.path_graph(3, create_using=nx.DiGraph) + drawn_edges = nx.draw_networkx_edges( + G, pos=nx.random_layout(G), edgelist=edgelist, edge_color=edge_color + ) + for fap in drawn_edges: + assert mpl.colors.same_color(fap.get_edgecolor(), expected) + + +def test_edge_color_tuple_interpretation(): + """If edge_color is a sequence with the same length as edgelist, then each + value in edge_color is mapped onto each edge via colormap.""" + G = nx.path_graph(6, create_using=nx.DiGraph) + pos = {n: (n, n) for n in range(len(G))} + + # num edges != 3 or 4 --> edge_color interpreted as rgb(a) + for ec in ((0, 0, 1), (0, 0, 1, 1)): + # More than 4 edges + drawn_edges = nx.draw_networkx_edges(G, pos, edge_color=ec) + for fap in drawn_edges: + assert mpl.colors.same_color(fap.get_edgecolor(), ec) + # Fewer than 3 edges + drawn_edges = nx.draw_networkx_edges( + G, pos, edgelist=[(0, 1), (1, 2)], edge_color=ec + ) + for fap in drawn_edges: + assert mpl.colors.same_color(fap.get_edgecolor(), ec) + + # num edges == 3, len(edge_color) == 4: interpreted as rgba + drawn_edges = nx.draw_networkx_edges( + G, pos, edgelist=[(0, 1), (1, 2), (2, 3)], edge_color=(0, 0, 1, 1) + ) + for fap in drawn_edges: + assert mpl.colors.same_color(fap.get_edgecolor(), "blue") + + # num edges == 4, len(edge_color) == 3: interpreted as rgb + drawn_edges = nx.draw_networkx_edges( + G, pos, edgelist=[(0, 1), (1, 2), (2, 3), (3, 4)], edge_color=(0, 0, 1) + ) + for fap in drawn_edges: + assert mpl.colors.same_color(fap.get_edgecolor(), "blue") + + # num edges == len(edge_color) == 3: interpreted with cmap, *not* as rgb + drawn_edges = nx.draw_networkx_edges( + G, pos, edgelist=[(0, 1), (1, 2), (2, 3)], edge_color=(0, 0, 1) + ) + assert mpl.colors.same_color( + drawn_edges[0].get_edgecolor(), drawn_edges[1].get_edgecolor() + ) + for fap in drawn_edges: + assert not mpl.colors.same_color(fap.get_edgecolor(), "blue") + + # num edges == len(edge_color) == 4: interpreted with cmap, *not* as rgba + drawn_edges = nx.draw_networkx_edges( + G, pos, edgelist=[(0, 1), (1, 2), (2, 3), (3, 4)], edge_color=(0, 0, 1, 1) + ) + assert mpl.colors.same_color( + drawn_edges[0].get_edgecolor(), drawn_edges[1].get_edgecolor() + ) + assert mpl.colors.same_color( + drawn_edges[2].get_edgecolor(), drawn_edges[3].get_edgecolor() + ) + for fap in drawn_edges: + assert not mpl.colors.same_color(fap.get_edgecolor(), "blue") + + +def test_fewer_edge_colors_than_num_edges_directed(): + """Test that the edge colors are cycled when there are fewer specified + colors than edges.""" + G = barbell.to_directed() + pos = nx.random_layout(barbell) + edgecolors = ("r", "g", "b") + drawn_edges = nx.draw_networkx_edges(G, pos, edge_color=edgecolors) + for fap, expected in zip(drawn_edges, itertools.cycle(edgecolors)): + assert mpl.colors.same_color(fap.get_edgecolor(), expected) + + +def test_more_edge_colors_than_num_edges_directed(): + """Test that extra edge colors are ignored when there are more specified + colors than edges.""" + G = nx.path_graph(4, create_using=nx.DiGraph) # 3 edges + pos = nx.random_layout(barbell) + edgecolors = ("r", "g", "b", "c") # 4 edge colors + drawn_edges = nx.draw_networkx_edges(G, pos, edge_color=edgecolors) + for fap, expected in zip(drawn_edges, edgecolors[:-1]): + assert mpl.colors.same_color(fap.get_edgecolor(), expected) + + +def test_edge_color_string_with_global_alpha_undirected(): + edge_collection = nx.draw_networkx_edges( + barbell, + pos=nx.random_layout(barbell), + edgelist=[(0, 1), (1, 2)], + edge_color="purple", + alpha=0.2, + ) + ec = edge_collection.get_color().squeeze() # as rgba tuple + assert len(edge_collection.get_paths()) == 2 + assert mpl.colors.same_color(ec[:-1], "purple") + assert ec[-1] == 0.2 + + +def test_edge_color_string_with_global_alpha_directed(): + drawn_edges = nx.draw_networkx_edges( + barbell.to_directed(), + pos=nx.random_layout(barbell), + edgelist=[(0, 1), (1, 2)], + edge_color="purple", + alpha=0.2, + ) + assert len(drawn_edges) == 2 + for fap in drawn_edges: + ec = fap.get_edgecolor() # As rgba tuple + assert mpl.colors.same_color(ec[:-1], "purple") + assert ec[-1] == 0.2 + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +def test_edge_width_default_value(graph_type): + """Test the default linewidth for edges drawn either via LineCollection or + FancyArrowPatches.""" + G = nx.path_graph(2, create_using=graph_type) + pos = {n: (n, n) for n in range(len(G))} + drawn_edges = nx.draw_networkx_edges(G, pos) + if isinstance(drawn_edges, list): # directed case: list of FancyArrowPatch + drawn_edges = drawn_edges[0] + assert drawn_edges.get_linewidth() == 1 + + +@pytest.mark.parametrize( + ("edgewidth", "expected"), + ( + (3, 3), # single-value, non-default + ([3], 3), # Single value as a list + ), +) +def test_edge_width_single_value_undirected(edgewidth, expected): + G = nx.path_graph(4) + pos = {n: (n, n) for n in range(len(G))} + drawn_edges = nx.draw_networkx_edges(G, pos, width=edgewidth) + assert len(drawn_edges.get_paths()) == 3 + assert drawn_edges.get_linewidth() == expected + + +@pytest.mark.parametrize( + ("edgewidth", "expected"), + ( + (3, 3), # single-value, non-default + ([3], 3), # Single value as a list + ), +) +def test_edge_width_single_value_directed(edgewidth, expected): + G = nx.path_graph(4, create_using=nx.DiGraph) + pos = {n: (n, n) for n in range(len(G))} + drawn_edges = nx.draw_networkx_edges(G, pos, width=edgewidth) + assert len(drawn_edges) == 3 + for fap in drawn_edges: + assert fap.get_linewidth() == expected + + +@pytest.mark.parametrize( + "edgelist", + ( + [(0, 1), (1, 2), (2, 3)], # one width specification per edge + None, # fewer widths than edges - widths cycle + [(0, 1), (1, 2)], # More widths than edges - unused widths ignored + ), +) +def test_edge_width_sequence(edgelist): + G = barbell.to_directed() + pos = nx.random_layout(G) + widths = (0.5, 2.0, 12.0) + drawn_edges = nx.draw_networkx_edges(G, pos, edgelist=edgelist, width=widths) + for fap, expected_width in zip(drawn_edges, itertools.cycle(widths)): + assert fap.get_linewidth() == expected_width + + +def test_edge_color_with_edge_vmin_vmax(): + """Test that edge_vmin and edge_vmax properly set the dynamic range of the + color map when num edges == len(edge_colors).""" + G = nx.path_graph(3, create_using=nx.DiGraph) + pos = nx.random_layout(G) + # Extract colors from the original (unscaled) colormap + drawn_edges = nx.draw_networkx_edges(G, pos, edge_color=[0, 1.0]) + orig_colors = [e.get_edgecolor() for e in drawn_edges] + # Colors from scaled colormap + drawn_edges = nx.draw_networkx_edges( + G, pos, edge_color=[0.2, 0.8], edge_vmin=0.2, edge_vmax=0.8 + ) + scaled_colors = [e.get_edgecolor() for e in drawn_edges] + assert mpl.colors.same_color(orig_colors, scaled_colors) + + +def test_directed_edges_linestyle_default(): + """Test default linestyle for edges drawn with FancyArrowPatches.""" + G = nx.path_graph(4, create_using=nx.DiGraph) # Graph with 3 edges + pos = {n: (n, n) for n in range(len(G))} + + # edge with default style + drawn_edges = nx.draw_networkx_edges(G, pos) + assert len(drawn_edges) == 3 + for fap in drawn_edges: + assert fap.get_linestyle() == "solid" + + +@pytest.mark.parametrize( + "style", + ( + "dashed", # edge with string style + "--", # edge with simplified string style + (1, (1, 1)), # edge with (offset, onoffseq) style + ), +) +def test_directed_edges_linestyle_single_value(style): + """Tests support for specifying linestyles with a single value to be applied to + all edges in ``draw_networkx_edges`` for FancyArrowPatch outputs + (e.g. directed edges).""" + + G = nx.path_graph(4, create_using=nx.DiGraph) # Graph with 3 edges + pos = {n: (n, n) for n in range(len(G))} + + drawn_edges = nx.draw_networkx_edges(G, pos, style=style) + assert len(drawn_edges) == 3 + for fap in drawn_edges: + assert fap.get_linestyle() == style + + +@pytest.mark.parametrize( + "style_seq", + ( + ["dashed"], # edge with string style in list + ["--"], # edge with simplified string style in list + [(1, (1, 1))], # edge with (offset, onoffseq) style in list + ["--", "-", ":"], # edges with styles for each edge + ["--", "-"], # edges with fewer styles than edges (styles cycle) + ["--", "-", ":", "-."], # edges with more styles than edges (extra unused) + ), +) +def test_directed_edges_linestyle_sequence(style_seq): + """Tests support for specifying linestyles with sequences in + ``draw_networkx_edges`` for FancyArrowPatch outputs (e.g. directed edges).""" + + G = nx.path_graph(4, create_using=nx.DiGraph) # Graph with 3 edges + pos = {n: (n, n) for n in range(len(G))} + + drawn_edges = nx.draw_networkx_edges(G, pos, style=style_seq) + assert len(drawn_edges) == 3 + for fap, style in zip(drawn_edges, itertools.cycle(style_seq)): + assert fap.get_linestyle() == style + + +def test_return_types(): + from matplotlib.collections import LineCollection, PathCollection + from matplotlib.patches import FancyArrowPatch + + G = nx.frucht_graph(create_using=nx.Graph) + dG = nx.frucht_graph(create_using=nx.DiGraph) + pos = nx.spring_layout(G, seed=42) + dpos = nx.spring_layout(dG, seed=42) + # nodes + nodes = nx.draw_networkx_nodes(G, pos) + assert isinstance(nodes, PathCollection) + # edges + edges = nx.draw_networkx_edges(dG, dpos, arrows=True) + assert isinstance(edges, list) + if len(edges) > 0: + assert isinstance(edges[0], FancyArrowPatch) + edges = nx.draw_networkx_edges(dG, dpos, arrows=False) + assert isinstance(edges, LineCollection) + edges = nx.draw_networkx_edges(G, dpos, arrows=None) + assert isinstance(edges, LineCollection) + edges = nx.draw_networkx_edges(dG, pos, arrows=None) + assert isinstance(edges, list) + if len(edges) > 0: + assert isinstance(edges[0], FancyArrowPatch) + + +def test_labels_and_colors(): + G = nx.cubical_graph() + pos = nx.spring_layout(G, seed=42) # positions for all nodes + # nodes + nx.draw_networkx_nodes( + G, pos, nodelist=[0, 1, 2, 3], node_color="r", node_size=500, alpha=0.75 + ) + nx.draw_networkx_nodes( + G, + pos, + nodelist=[4, 5, 6, 7], + node_color="b", + node_size=500, + alpha=[0.25, 0.5, 0.75, 1.0], + ) + # edges + nx.draw_networkx_edges(G, pos, width=1.0, alpha=0.5) + nx.draw_networkx_edges( + G, + pos, + edgelist=[(0, 1), (1, 2), (2, 3), (3, 0)], + width=8, + alpha=0.5, + edge_color="r", + ) + nx.draw_networkx_edges( + G, + pos, + edgelist=[(4, 5), (5, 6), (6, 7), (7, 4)], + width=8, + alpha=0.5, + edge_color="b", + ) + nx.draw_networkx_edges( + G, + pos, + edgelist=[(4, 5), (5, 6), (6, 7), (7, 4)], + arrows=True, + min_source_margin=0.5, + min_target_margin=0.75, + width=8, + edge_color="b", + ) + # some math labels + labels = {} + labels[0] = r"$a$" + labels[1] = r"$b$" + labels[2] = r"$c$" + labels[3] = r"$d$" + labels[4] = r"$\alpha$" + labels[5] = r"$\beta$" + labels[6] = r"$\gamma$" + labels[7] = r"$\delta$" + colors = {n: "k" if n % 2 == 0 else "r" for n in range(8)} + nx.draw_networkx_labels(G, pos, labels, font_size=16) + nx.draw_networkx_labels(G, pos, labels, font_size=16, font_color=colors) + nx.draw_networkx_edge_labels(G, pos, edge_labels=None, rotate=False) + nx.draw_networkx_edge_labels(G, pos, edge_labels={(4, 5): "4-5"}) + # plt.show() + + +def test_axes(subplots): + fig, ax = subplots + nx.draw(barbell, ax=ax) + nx.draw_networkx_edge_labels(barbell, nx.circular_layout(barbell), ax=ax) + + +def test_empty_graph(): + G = nx.Graph() + nx.draw(G) + + +def test_draw_empty_nodes_return_values(): + # See Issue #3833 + import matplotlib.collections # call as mpl.collections + + G = nx.Graph([(1, 2), (2, 3)]) + DG = nx.DiGraph([(1, 2), (2, 3)]) + pos = nx.circular_layout(G) + assert isinstance( + nx.draw_networkx_nodes(G, pos, nodelist=[]), mpl.collections.PathCollection + ) + assert isinstance( + nx.draw_networkx_nodes(DG, pos, nodelist=[]), mpl.collections.PathCollection + ) + + # drawing empty edges used to return an empty LineCollection or empty list. + # Now it is always an empty list (because edges are now lists of FancyArrows) + assert nx.draw_networkx_edges(G, pos, edgelist=[], arrows=True) == [] + assert nx.draw_networkx_edges(G, pos, edgelist=[], arrows=False) == [] + assert nx.draw_networkx_edges(DG, pos, edgelist=[], arrows=False) == [] + assert nx.draw_networkx_edges(DG, pos, edgelist=[], arrows=True) == [] + + +def test_multigraph_edgelist_tuples(): + # See Issue #3295 + G = nx.path_graph(3, create_using=nx.MultiDiGraph) + nx.draw_networkx(G, edgelist=[(0, 1, 0)]) + nx.draw_networkx(G, edgelist=[(0, 1, 0)], node_size=[10, 20, 0]) + + +def test_alpha_iter(): + pos = nx.random_layout(barbell) + fig = plt.figure() + # with fewer alpha elements than nodes + fig.add_subplot(131) # Each test in a new axis object + nx.draw_networkx_nodes(barbell, pos, alpha=[0.1, 0.2]) + # with equal alpha elements and nodes + num_nodes = len(barbell.nodes) + alpha = [x / num_nodes for x in range(num_nodes)] + colors = range(num_nodes) + fig.add_subplot(132) + nx.draw_networkx_nodes(barbell, pos, node_color=colors, alpha=alpha) + # with more alpha elements than nodes + alpha.append(1) + fig.add_subplot(133) + nx.draw_networkx_nodes(barbell, pos, alpha=alpha) + + +def test_multiple_node_shapes(subplots): + fig, ax = subplots + G = nx.path_graph(4) + nx.draw(G, node_shape=["o", "h", "s", "^"], ax=ax) + scatters = [ + s for s in ax.get_children() if isinstance(s, mpl.collections.PathCollection) + ] + assert len(scatters) == 4 + + +def test_individualized_font_attributes(subplots): + G = nx.karate_club_graph() + fig, ax = subplots + nx.draw( + G, + ax=ax, + font_color={n: "k" if n % 2 else "r" for n in G.nodes()}, + font_size={n: int(n / (34 / 15) + 5) for n in G.nodes()}, + ) + for n, t in zip( + G.nodes(), + [ + t + for t in ax.get_children() + if isinstance(t, mpl.text.Text) and len(t.get_text()) > 0 + ], + ): + expected = "black" if n % 2 else "red" + + assert mpl.colors.same_color(t.get_color(), expected) + assert int(n / (34 / 15) + 5) == t.get_size() + + +def test_individualized_edge_attributes(subplots): + G = nx.karate_club_graph() + fig, ax = subplots + arrowstyles = ["-|>" if (u + v) % 2 == 0 else "-[" for u, v in G.edges()] + arrowsizes = [10 * (u % 2 + v % 2) + 10 for u, v in G.edges()] + nx.draw(G, ax=ax, arrows=True, arrowstyle=arrowstyles, arrowsize=arrowsizes) + arrows = [ + f for f in ax.get_children() if isinstance(f, mpl.patches.FancyArrowPatch) + ] + for e, a in zip(G.edges(), arrows): + assert a.get_mutation_scale() == 10 * (e[0] % 2 + e[1] % 2) + 10 + expected = ( + mpl.patches.ArrowStyle.BracketB + if sum(e) % 2 + else mpl.patches.ArrowStyle.CurveFilledB + ) + assert isinstance(a.get_arrowstyle(), expected) + + +def test_error_invalid_kwds(): + with pytest.raises(ValueError, match="Received invalid argument"): + nx.draw(barbell, foo="bar") + + +def test_draw_networkx_arrowsize_incorrect_size(): + G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 3)]) + arrowsize = [1, 2, 3] + with pytest.raises( + ValueError, match="arrowsize should have the same length as edgelist" + ): + nx.draw(G, arrowsize=arrowsize) + + +@pytest.mark.parametrize("arrowsize", (30, [10, 20, 30])) +def test_draw_edges_arrowsize(arrowsize): + G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) + pos = {0: (0, 0), 1: (0, 1), 2: (1, 0)} + edges = nx.draw_networkx_edges(G, pos=pos, arrowsize=arrowsize) + + arrowsize = itertools.repeat(arrowsize) if isinstance(arrowsize, int) else arrowsize + + for fap, expected in zip(edges, arrowsize): + assert isinstance(fap, mpl.patches.FancyArrowPatch) + assert fap.get_mutation_scale() == expected + + +@pytest.mark.parametrize("arrowstyle", ("-|>", ["-|>", "-[", "<|-|>"])) +def test_draw_edges_arrowstyle(arrowstyle): + G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) + pos = {0: (0, 0), 1: (0, 1), 2: (1, 0)} + edges = nx.draw_networkx_edges(G, pos=pos, arrowstyle=arrowstyle) + + arrowstyle = ( + itertools.repeat(arrowstyle) if isinstance(arrowstyle, str) else arrowstyle + ) + + arrow_objects = { + "-|>": mpl.patches.ArrowStyle.CurveFilledB, + "-[": mpl.patches.ArrowStyle.BracketB, + "<|-|>": mpl.patches.ArrowStyle.CurveFilledAB, + } + + for fap, expected in zip(edges, arrowstyle): + assert isinstance(fap, mpl.patches.FancyArrowPatch) + assert isinstance(fap.get_arrowstyle(), arrow_objects[expected]) + + +def test_np_edgelist(): + # see issue #4129 + nx.draw_networkx(barbell, edgelist=np.array([(0, 2), (0, 3)])) + + +def test_draw_nodes_missing_node_from_position(): + G = nx.path_graph(3) + pos = {0: (0, 0), 1: (1, 1)} # No position for node 2 + with pytest.raises(nx.NetworkXError, match="has no position"): + nx.draw_networkx_nodes(G, pos) + + +def test_draw_networkx_nodes_node_shape_list_with_scalar_color(subplots): + """Ensure draw_networkx_nodes works when node_shape is a Python list. + + This covers the case where node_shape is a sequence (list) and node_color + is a single scalar color, which should be supported. + """ + fig, ax = subplots + + G = nx.empty_graph(5) + pos = {i: (i, i) for i in G} + + shapes = ["o", "^", "o", "^", "o"] + + nodes = nx.draw_networkx_nodes( + G, + pos, + node_color="red", # scalar color (supported) + node_shape=shapes, # list of shapes – this used to be buggy + ax=ax, + ) + # Should get a PathCollection with an element in it (same as with numpy arrays) + assert len(nodes.get_offsets()) > 0 + # NOTE: When node_shape is a sequence, draw_networkx_nodes internally calls + # ax.scatter multiple times and returns only the last PathCollection. + # Therefore, we do NOT assert a value for len(nodes.get_offsets()) here. + + +# NOTE: parametrizing on marker to test both branches of internal +# nx.draw_networkx_edges.to_marker_edge function +@pytest.mark.parametrize("node_shape", ("o", "s")) +def test_draw_edges_min_source_target_margins(node_shape, subplots): + """Test that there is a wider gap between the node and the start of an + incident edge when min_source_margin is specified. + + This test checks that the use of min_{source/target}_margin kwargs result + in shorter (more padding) between the edges and source and target nodes. + As a crude visual example, let 's' and 't' represent source and target + nodes, respectively: + + Default: + s-----------------------------t + + With margins: + s ----------------------- t + + """ + # Create a single axis object to get consistent pixel coords across + # multiple draws + fig, ax = subplots + G = nx.DiGraph([(0, 1)]) + pos = {0: (0, 0), 1: (1, 0)} # horizontal layout + # Get leftmost and rightmost points of the FancyArrowPatch object + # representing the edge between nodes 0 and 1 (in pixel coordinates) + default_patch = nx.draw_networkx_edges(G, pos, ax=ax, node_shape=node_shape)[0] + default_extent = default_patch.get_extents().corners()[::2, 0] + # Now, do the same but with "padding" for the source and target via the + # min_{source/target}_margin kwargs + padded_patch = nx.draw_networkx_edges( + G, + pos, + ax=ax, + node_shape=node_shape, + min_source_margin=100, + min_target_margin=100, + )[0] + padded_extent = padded_patch.get_extents().corners()[::2, 0] + + # With padding, the left-most extent of the edge should be further to the + # right + assert padded_extent[0] > default_extent[0] + # And the rightmost extent of the edge, further to the left + assert padded_extent[1] < default_extent[1] + + +# NOTE: parametrizing on marker to test both branches of internal +# nx.draw_networkx_edges.to_marker_edge function +@pytest.mark.parametrize("node_shape", ("o", "s")) +def test_draw_edges_min_source_target_margins_individual(node_shape, subplots): + """Test that there is a wider gap between the node and the start of an + incident edge when min_source_margin is specified. + + This test checks that the use of min_{source/target}_margin kwargs result + in shorter (more padding) between the edges and source and target nodes. + As a crude visual example, let 's' and 't' represent source and target + nodes, respectively: + + Default: + s-----------------------------t + + With margins: + s ----------------------- t + + """ + # Create a single axis object to get consistent pixel coords across + # multiple draws + fig, ax = subplots + G = nx.DiGraph([(0, 1), (1, 2)]) + pos = {0: (0, 0), 1: (1, 0), 2: (2, 0)} # horizontal layout + # Get leftmost and rightmost points of the FancyArrowPatch object + # representing the edge between nodes 0 and 1 (in pixel coordinates) + default_patch = nx.draw_networkx_edges(G, pos, ax=ax, node_shape=node_shape) + default_extent = [d.get_extents().corners()[::2, 0] for d in default_patch] + # Now, do the same but with "padding" for the source and target via the + # min_{source/target}_margin kwargs + padded_patch = nx.draw_networkx_edges( + G, + pos, + ax=ax, + node_shape=node_shape, + min_source_margin=[98, 102], + min_target_margin=[98, 102], + ) + padded_extent = [p.get_extents().corners()[::2, 0] for p in padded_patch] + for d, p in zip(default_extent, padded_extent): + # With padding, the left-most extent of the edge should be further to the + # right + assert p[0] > d[0] + # And the rightmost extent of the edge, further to the left + assert p[1] < d[1] + + +def test_nonzero_selfloop_with_single_node(subplots): + """Ensure that selfloop extent is non-zero when there is only one node.""" + # Create explicit axis object for test + fig, ax = subplots + # Graph with single node + self loop + G = nx.DiGraph() + G.add_node(0) + G.add_edge(0, 0) + # Draw + patch = nx.draw_networkx_edges(G, {0: (0, 0)})[0] + # The resulting patch must have non-zero extent + bbox = patch.get_extents() + assert bbox.width > 0 and bbox.height > 0 + + +def test_nonzero_selfloop_with_single_edge_in_edgelist(subplots): + """Ensure that selfloop extent is non-zero when only a single edge is + specified in the edgelist. + """ + # Create explicit axis object for test + fig, ax = subplots + # Graph with selfloop + G = nx.path_graph(2, create_using=nx.DiGraph) + G.add_edge(1, 1) + pos = {n: (n, n) for n in G.nodes} + # Draw only the selfloop edge via the `edgelist` kwarg + patch = nx.draw_networkx_edges(G, pos, edgelist=[(1, 1)])[0] + # The resulting patch must have non-zero extent + bbox = patch.get_extents() + assert bbox.width > 0 and bbox.height > 0 + + +def test_apply_alpha(): + """Test apply_alpha when there is a mismatch between the number of + supplied colors and elements. + """ + nodelist = [0, 1, 2] + colorlist = ["r", "g", "b"] + alpha = 0.5 + rgba_colors = nx.drawing.nx_pylab.apply_alpha(colorlist, alpha, nodelist) + assert all(rgba_colors[:, -1] == alpha) + + +def test_draw_edges_toggling_with_arrows_kwarg(): + """ + The `arrows` keyword argument is used as a 3-way switch to select which + type of object to use for drawing edges: + - ``arrows=None`` -> default (FancyArrowPatches for directed, else LineCollection) + - ``arrows=True`` -> FancyArrowPatches + - ``arrows=False`` -> LineCollection + """ + import matplotlib.collections + import matplotlib.patches + + UG = nx.path_graph(3) + DG = nx.path_graph(3, create_using=nx.DiGraph) + pos = {n: (n, n) for n in UG} + + # Use FancyArrowPatches when arrows=True, regardless of graph type + for G in (UG, DG): + edges = nx.draw_networkx_edges(G, pos, arrows=True) + assert len(edges) == len(G.edges) + assert isinstance(edges[0], mpl.patches.FancyArrowPatch) + + # Use LineCollection when arrows=False, regardless of graph type + for G in (UG, DG): + edges = nx.draw_networkx_edges(G, pos, arrows=False) + assert isinstance(edges, mpl.collections.LineCollection) + + # Default behavior when arrows=None: FAPs for directed, LC's for undirected + edges = nx.draw_networkx_edges(UG, pos) + assert isinstance(edges, mpl.collections.LineCollection) + edges = nx.draw_networkx_edges(DG, pos) + assert len(edges) == len(G.edges) + assert isinstance(edges[0], mpl.patches.FancyArrowPatch) + + +@pytest.mark.parametrize("drawing_func", (nx.draw, nx.draw_networkx)) +def test_draw_networkx_arrows_default_undirected(drawing_func, subplots): + import matplotlib.collections + + G = nx.path_graph(3) + fig, ax = subplots + drawing_func(G, ax=ax) + assert any(isinstance(c, mpl.collections.LineCollection) for c in ax.collections) + assert not ax.patches + + +@pytest.mark.parametrize("drawing_func", (nx.draw, nx.draw_networkx)) +def test_draw_networkx_arrows_default_directed(drawing_func, subplots): + import matplotlib.collections + + G = nx.path_graph(3, create_using=nx.DiGraph) + fig, ax = subplots + drawing_func(G, ax=ax) + assert not any( + isinstance(c, mpl.collections.LineCollection) for c in ax.collections + ) + assert ax.patches + + +def test_edgelist_kwarg_not_ignored(subplots): + # See gh-4994 + G = nx.path_graph(3) + G.add_edge(0, 0) + fig, ax = subplots + nx.draw(G, edgelist=[(0, 1), (1, 2)], ax=ax) # Exclude self-loop from edgelist + assert not ax.patches + + +@pytest.mark.parametrize( + ("G", "expected_n_edges"), + ([nx.DiGraph(), 2], [nx.MultiGraph(), 4], [nx.MultiDiGraph(), 4]), +) +def test_draw_networkx_edges_multiedge_connectionstyle(G, expected_n_edges): + """Draws edges correctly for 3 types of graphs and checks for valid length""" + for i, (u, v) in enumerate([(0, 1), (0, 1), (0, 1), (0, 2)]): + G.add_edge(u, v, weight=round(i / 3, 2)) + pos = {n: (n, n) for n in G} + # Raises on insufficient connectionstyle length + for conn_style in [ + "arc3,rad=0.1", + ["arc3,rad=0.1", "arc3,rad=0.1"], + ["arc3,rad=0.1", "arc3,rad=0.1", "arc3,rad=0.2"], + ]: + nx.draw_networkx_edges(G, pos, connectionstyle=conn_style) + arrows = nx.draw_networkx_edges(G, pos, connectionstyle=conn_style) + assert len(arrows) == expected_n_edges + + +@pytest.mark.parametrize( + ("G", "expected_n_edges"), + ([nx.DiGraph(), 2], [nx.MultiGraph(), 4], [nx.MultiDiGraph(), 4]), +) +def test_draw_networkx_edge_labels_multiedge_connectionstyle(G, expected_n_edges): + """Draws labels correctly for 3 types of graphs and checks for valid length and class names""" + for i, (u, v) in enumerate([(0, 1), (0, 1), (0, 1), (0, 2)]): + G.add_edge(u, v, weight=round(i / 3, 2)) + pos = {n: (n, n) for n in G} + # Raises on insufficient connectionstyle length + arrows = nx.draw_networkx_edges( + G, pos, connectionstyle=["arc3,rad=0.1", "arc3,rad=0.1", "arc3,rad=0.1"] + ) + for conn_style in [ + "arc3,rad=0.1", + ["arc3,rad=0.1", "arc3,rad=0.2"], + ["arc3,rad=0.1", "arc3,rad=0.1", "arc3,rad=0.1"], + ]: + text_items = nx.draw_networkx_edge_labels(G, pos, connectionstyle=conn_style) + assert len(text_items) == expected_n_edges + for ti in text_items.values(): + assert ti.__class__.__name__ == "CurvedArrowText" + + +def test_draw_networkx_edge_label_multiedge(): + G = nx.MultiGraph() + G.add_edge(0, 1, weight=10) + G.add_edge(0, 1, weight=20) + edge_labels = nx.get_edge_attributes(G, "weight") # Includes edge keys + pos = {n: (n, n) for n in G} + text_items = nx.draw_networkx_edge_labels( + G, + pos, + edge_labels=edge_labels, + connectionstyle=["arc3,rad=0.1", "arc3,rad=0.2"], + ) + assert len(text_items) == 2 + + +def test_draw_networkx_edge_label_empty_dict(): + """Regression test for draw_networkx_edge_labels with empty dict. See + gh-5372.""" + G = nx.path_graph(3) + pos = {n: (n, n) for n in G.nodes} + assert nx.draw_networkx_edge_labels(G, pos, edge_labels={}) == {} + + +def test_draw_networkx_edges_undirected_selfloop_colors(subplots): + """When an edgelist is supplied along with a sequence of colors, check that + the self-loops have the correct colors.""" + fig, ax = subplots + # Edge list and corresponding colors + edgelist = [(1, 3), (1, 2), (2, 3), (1, 1), (3, 3), (2, 2)] + edge_colors = ["pink", "cyan", "black", "red", "blue", "green"] + + G = nx.Graph(edgelist) + pos = {n: (n, n) for n in G.nodes} + nx.draw_networkx_edges(G, pos, ax=ax, edgelist=edgelist, edge_color=edge_colors) + + # Verify that there are three fancy arrow patches (1 per self loop) + assert len(ax.patches) == 3 + + # These are points that should be contained in the self loops. For example, + # sl_points[0] will be (1, 1.1), which is inside the "path" of the first + # self-loop but outside the others + sl_points = np.array(edgelist[-3:]) + np.array([0, 0.1]) + + # Check that the mapping between self-loop locations and their colors is + # correct + for fap, clr, slp in zip(ax.patches, edge_colors[-3:], sl_points): + assert fap.get_path().contains_point(slp) + assert mpl.colors.same_color(fap.get_edgecolor(), clr) + + +@pytest.mark.parametrize( + "fap_only_kwarg", # Non-default values for kwargs that only apply to FAPs + ( + {"arrowstyle": "-"}, + {"arrowsize": 20}, + {"connectionstyle": "arc3,rad=0.2"}, + {"min_source_margin": 10}, + {"min_target_margin": 10}, + ), +) +def test_user_warnings_for_unused_edge_drawing_kwargs(fap_only_kwarg, subplots): + """Users should get a warning when they specify a non-default value for + one of the kwargs that applies only to edges drawn with FancyArrowPatches, + but FancyArrowPatches aren't being used under the hood.""" + G = nx.path_graph(3) + pos = {n: (n, n) for n in G} + fig, ax = subplots + # By default, an undirected graph will use LineCollection to represent + # the edges + kwarg_name = list(fap_only_kwarg.keys())[0] + with pytest.warns( + UserWarning, match=f"\n\nThe {kwarg_name} keyword argument is not applicable" + ): + nx.draw_networkx_edges(G, pos, ax=ax, **fap_only_kwarg) + # FancyArrowPatches are always used when `arrows=True` is specified. + # Check that warnings are *not* raised in this case + with warnings.catch_warnings(): + # Escalate warnings -> errors so tests fail if warnings are raised + warnings.simplefilter("error") + warnings.filterwarnings("ignore", category=DeprecationWarning) + nx.draw_networkx_edges(G, pos, ax=ax, arrows=True, **fap_only_kwarg) + + +@pytest.mark.parametrize("draw_fn", (nx.draw, nx.draw_circular)) +def test_no_warning_on_default_draw_arrowstyle(draw_fn, subplots): + # See gh-7284 + fig, ax = subplots + G = nx.cycle_graph(5) + with warnings.catch_warnings(record=True) as w: + draw_fn(G, ax=ax) + assert len(w) == 0 + + +@pytest.mark.parametrize("hide_ticks", [False, True]) +@pytest.mark.parametrize( + "method", + [ + nx.draw_networkx, + nx.draw_networkx_edge_labels, + nx.draw_networkx_edges, + nx.draw_networkx_labels, + nx.draw_networkx_nodes, + ], +) +def test_hide_ticks(method, hide_ticks, subplots): + G = nx.path_graph(3) + pos = {n: (n, n) for n in G.nodes} + _, ax = subplots + method(G, pos=pos, ax=ax, hide_ticks=hide_ticks) + for axis in [ax.xaxis, ax.yaxis]: + assert bool(axis.get_ticklabels()) != hide_ticks + + +@pytest.mark.parametrize( + "style", ["angle", "angle3", "arc", "arc3,rad=0.0", "bar,fraction=0.1"] +) +def test_edge_label_all_connectionstyles(subplots, style): + """ + Check that FancyArrowPatches with all `connectionstyle`s are supported + in edge label rendering. See gh-7735 and gh-8106. + """ + fig, ax = subplots + edge = (0, 1) + G = nx.DiGraph([edge]) + pos = {n: (n, 0) for n in G} + + name = style.split(",")[0] + labels = nx.draw_networkx_edge_labels( + G, pos, edge_labels={edge: "edge"}, connectionstyle=style + ) + + hmid = (pos[0][0] + pos[1][0]) / 2 + vmid = (pos[0][1] + pos[1][1]) / 2 + if name in {"arc", "arc3"}: # The label should be at roughly the midpoint. + assert labels[edge].x, labels[edge].y == pytest.approx((hmid, vmid)) + elif name == "bar": # The label should be below the vertical midpoint. + assert labels[edge].y < vmid + + +@pytest.mark.parametrize("label_pos", [-0.1, 1.1]) +def test_edge_label_label_pos(subplots, label_pos): + """ + Check that label positions can be extrapolated outside [0, 1]. + """ + fig, ax = subplots + edge = (0, 1) + G = nx.DiGraph([edge]) + pos = {n: (n, n) for n in G} + lbl = nx.draw_networkx_edge_labels( + G, pos, edge_labels={edge: "edge"}, label_pos=label_pos, connectionstyle="angle" + ) + + assert lbl[edge].x, lbl[edge].y == pytest.approx((label_pos, label_pos)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ec027c2405b6f9de2e7b6a0f7c18d782ac8761c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__init__.py @@ -0,0 +1,34 @@ +""" +A package for generating various graphs in networkx. + +""" + +from networkx.generators.atlas import * +from networkx.generators.classic import * +from networkx.generators.cographs import * +from networkx.generators.community import * +from networkx.generators.degree_seq import * +from networkx.generators.directed import * +from networkx.generators.duplication import * +from networkx.generators.ego import * +from networkx.generators.expanders import * +from networkx.generators.geometric import * +from networkx.generators.harary_graph import * +from networkx.generators.internet_as_graphs import * +from networkx.generators.intersection import * +from networkx.generators.interval_graph import * +from networkx.generators.joint_degree_seq import * +from networkx.generators.lattice import * +from networkx.generators.line import * +from networkx.generators.mycielski import * +from networkx.generators.nonisomorphic_trees import * +from networkx.generators.random_clustered import * +from networkx.generators.random_graphs import * +from networkx.generators.small import * +from networkx.generators.social import * +from networkx.generators.spectral_graph_forge import * +from networkx.generators.stochastic import * +from networkx.generators.sudoku import * +from networkx.generators.time_series import * +from networkx.generators.trees import * +from networkx.generators.triads import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5693f68e78ddaa09db32e438c4eb7b14cbaac148 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/atlas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/atlas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ca9b6f13ea4f0b4cfbd9112875f34b9d1ce6a4e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/atlas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/classic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/classic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0a523665115a566d196929424e362c0338c3bf9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/classic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/cographs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/cographs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a7610f1f97ccb2a76687a0d3d45cf65073295b9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/cographs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/community.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/community.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b38655ef739c0b858f66fe57aa89cabc0103b7b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/community.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/degree_seq.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/degree_seq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1f4b50bebf8f4196db2564194767aa19565cffa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/degree_seq.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/directed.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/directed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22b78b4cecefa94074b17bb96c8ca01c94e202fd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/directed.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/duplication.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/duplication.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d56ae6aa6a16fd6a4107324282748b7105b50c76 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/duplication.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/ego.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/ego.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0121c9b8cff04c61faa38901d39a3dc7c6d7b3be Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/ego.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/expanders.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/expanders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11f3429c9e795481fea83b892a41899d87ec0889 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/expanders.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/geometric.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/geometric.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84696928f26c9033e61e2ac2d49f7bc9dd2dc9b6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/geometric.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/harary_graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/harary_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95e51001d7a82366e6a8cab369e29385d291b19b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/harary_graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/internet_as_graphs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/internet_as_graphs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc03c5df2d3ef0aad7a03500636a661666c26d99 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/internet_as_graphs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/intersection.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/intersection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ec3745e5bdcc90b353e1b9c3d14e800a779056d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/intersection.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/interval_graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/interval_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5841d9ff68f9b7a00a91414cfb87b14d81ca3cad Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/interval_graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/joint_degree_seq.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/joint_degree_seq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7b5674dbafa76e6d2a97479ce6e9b3ac7169b54 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/joint_degree_seq.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/lattice.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/lattice.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbf42252815d67305a7012584c3c9d304328794d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/lattice.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/line.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/line.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32fdac0ee06a3a069315233a2c226a6f1dc2ceab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/line.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/mycielski.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/mycielski.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a349939a3329e2c55f8e18509ff26441aebfce9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/mycielski.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/nonisomorphic_trees.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/nonisomorphic_trees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77f19c91cc9b81c8670265fe7f6e55d5aa4c06b1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/nonisomorphic_trees.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_clustered.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_clustered.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9dc90cf356a446580ff4edcce6cb14ae63cf42d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_clustered.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_graphs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_graphs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ed52f86e061a591f5a33ad9ced16771ba53f975 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/random_graphs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/small.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/small.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2b949d74f27c44057b9c14c26eaf96334fa446e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/small.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/social.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/social.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4211ec2b2653050cae4c64413df620de75117b10 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/social.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/spectral_graph_forge.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/spectral_graph_forge.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e71a1aba4733a3b1d2dc500e55d69da0ed315ac5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/spectral_graph_forge.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/stochastic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/stochastic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8cb3827ba9a213178a2d556e6fb538a5b6f57be6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/stochastic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/sudoku.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/sudoku.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09d72796d519a51ba8b3f9156e8ea1e4e9e589b2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/sudoku.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/time_series.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/time_series.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..456f21e6ce85bbef7972f77241c5ad73344dcde9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/time_series.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/trees.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/trees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e37c5fe11180291cfe36dbf876dbcadcd31fb56 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/trees.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/triads.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/triads.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a7145a3a51e374ab1171e02497b607cded6bab1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/__pycache__/triads.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/atlas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/atlas.py new file mode 100644 index 0000000000000000000000000000000000000000..000d478b89210fe65f4a9bfc1a02e35dded889f3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/atlas.py @@ -0,0 +1,227 @@ +""" +Generators for the small graph atlas. +""" + +import gzip +import importlib.resources +from itertools import islice + +import networkx as nx + +__all__ = ["graph_atlas", "graph_atlas_g"] + +#: The total number of graphs in the atlas. +#: +#: The graphs are labeled starting from 0 and extending to (but not +#: including) this number. +NUM_GRAPHS = 1253 + +#: The path to the data file containing the graph edge lists. +#: +#: This is the absolute path of the gzipped text file containing the +#: edge list for each graph in the atlas. The file contains one entry +#: per graph in the atlas, in sequential order, starting from graph +#: number 0 and extending through graph number 1252 (see +#: :data:`NUM_GRAPHS`). Each entry looks like +#: +#: .. sourcecode:: text +#: +#: GRAPH 6 +#: NODES 3 +#: 0 1 +#: 0 2 +#: +#: where the first two lines are the graph's index in the atlas and the +#: number of nodes in the graph, and the remaining lines are the edge +#: list. +#: +#: This file was generated from a Python list of graphs via code like +#: the following:: +#: +#: import gzip +#: from networkx.generators.atlas import graph_atlas_g +#: from networkx.readwrite.edgelist import write_edgelist +#: +#: with gzip.open('atlas.dat.gz', 'wb') as f: +#: for i, G in enumerate(graph_atlas_g()): +#: f.write(bytes(f'GRAPH {i}\n', encoding='utf-8')) +#: f.write(bytes(f'NODES {len(G)}\n', encoding='utf-8')) +#: write_edgelist(G, f, data=False) +#: + +# Path to the atlas file +ATLAS_FILE = importlib.resources.files("networkx.generators") / "atlas.dat.gz" + + +def _generate_graphs(): + """Sequentially read the file containing the edge list data for the + graphs in the atlas and generate the graphs one at a time. + + This function reads the file given in :data:`.ATLAS_FILE`. + + """ + with gzip.open(ATLAS_FILE, "rb") as f: + line = f.readline() + while line and line.startswith(b"GRAPH"): + # The first two lines of each entry tell us the index of the + # graph in the list and the number of nodes in the graph. + # They look like this: + # + # GRAPH 3 + # NODES 2 + # + graph_index = int(line[6:].rstrip()) + line = f.readline() + num_nodes = int(line[6:].rstrip()) + # The remaining lines contain the edge list, until the next + # GRAPH line (or until the end of the file). + edgelist = [] + line = f.readline() + while line and not line.startswith(b"GRAPH"): + edgelist.append(line.rstrip()) + line = f.readline() + G = nx.Graph() + G.name = f"G{graph_index}" + G.add_nodes_from(range(num_nodes)) + G.add_edges_from(tuple(map(int, e.split())) for e in edgelist) + yield G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def graph_atlas(i): + """Returns graph number `i` from the Graph Atlas. + + For more information, see :func:`.graph_atlas_g`. + + Parameters + ---------- + i : int + The index of the graph from the atlas to get. The graph at index + 0 is assumed to be the null graph. + + Returns + ------- + list + A list of :class:`~networkx.Graph` objects, the one at index *i* + corresponding to the graph *i* in the Graph Atlas. + + See also + -------- + graph_atlas_g + + Notes + ----- + The time required by this function increases linearly with the + argument `i`, since it reads a large file sequentially in order to + generate the graph [1]_. + + References + ---------- + .. [1] Ronald C. Read and Robin J. Wilson, *An Atlas of Graphs*. + Oxford University Press, 1998. + + """ + if not (0 <= i < NUM_GRAPHS): + raise ValueError(f"index must be between 0 and {NUM_GRAPHS}") + return next(islice(_generate_graphs(), i, None)) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def graph_atlas_g(): + """Returns the list of all graphs with up to seven nodes named in the + Graph Atlas. + + The graphs are listed in increasing order by + + 1. number of nodes, + 2. number of edges, + 3. degree sequence (for example 111223 < 112222), + 4. number of automorphisms, + + in that order, with three exceptions as described in the *Notes* + section below. This causes the list to correspond with the index of + the graphs in the Graph Atlas [atlas]_, with the first graph, + ``G[0]``, being the null graph. + + Returns + ------- + list + A list of :class:`~networkx.Graph` objects, the one at index *i* + corresponding to the graph *i* in the Graph Atlas. + + Examples + -------- + >>> from pprint import pprint + >>> atlas = nx.graph_atlas_g() + + There are 1253 graphs in the atlas + + >>> len(atlas) + 1253 + + The number of graphs with *n* nodes, where *n* ranges from 0 to 7: + + >>> from collections import Counter + >>> num_nodes_per_graph = [len(G) for G in atlas] + >>> Counter(num_nodes_per_graph) + Counter({7: 1044, 6: 156, 5: 34, 4: 11, 3: 4, 2: 2, 0: 1, 1: 1}) + + Since the atlas is ordered by the number of nodes in the graph, all graphs + with *n* nodes can be obtained by slicing the atlas. For example, all + graphs with 5 nodes: + + >>> G5_list = atlas[19:53] + >>> all(len(G) == 5 for G in G5_list) + True + + Or all graphs with at least 3 nodes but fewer than 7 nodes: + + >>> G3_6_list = atlas[4:209] + + More generally, the indices that partition the atlas by the number of nodes + per graph: + + >>> import itertools + >>> partition_indices = [0] + list( + ... itertools.accumulate(Counter(num_nodes_per_graph).values()) # cumsum + ... ) + >>> partition_indices + [0, 1, 2, 4, 8, 19, 53, 209, 1253] + >>> partition_mapping = dict(enumerate(itertools.pairwise(partition_indices))) + >>> pprint(partition_mapping) + {0: (0, 1), + 1: (1, 2), + 2: (2, 4), + 3: (4, 8), + 4: (8, 19), + 5: (19, 53), + 6: (53, 209), + 7: (209, 1253)} + + See also + -------- + graph_atlas + + Notes + ----- + This function may be expensive in both time and space, since it + reads a large file sequentially in order to populate the list. + + Although the NetworkX atlas functions match the order of graphs + given in the "Atlas of Graphs" book, there are (at least) three + errors in the ordering described in the book. The following three + pairs of nodes violate the lexicographically nondecreasing sorted + degree sequence rule: + + - graphs 55 and 56 with degree sequences 001111 and 000112, + - graphs 1007 and 1008 with degree sequences 3333444 and 3333336, + - graphs 1012 and 1213 with degree sequences 1244555 and 1244456. + + References + ---------- + .. [atlas] Ronald C. Read and Robin J. Wilson, + *An Atlas of Graphs*. + Oxford University Press, 1998. + + """ + return list(_generate_graphs()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/classic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/classic.py new file mode 100644 index 0000000000000000000000000000000000000000..c7522195b95679554d22c8627c47b721572f9175 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/classic.py @@ -0,0 +1,1091 @@ +"""Generators for some classic graphs. + +The typical graph builder function is called as follows: + +>>> G = nx.complete_graph(100) + +returning the complete graph on n nodes labeled 0, .., 99 +as a simple graph. Except for `empty_graph`, all the functions +in this module return a Graph class (i.e. a simple, undirected graph). + +""" + +import itertools +import numbers + +import networkx as nx +from networkx.classes import Graph +from networkx.exception import NetworkXError +from networkx.utils import nodes_or_number, pairwise + +__all__ = [ + "balanced_tree", + "barbell_graph", + "binomial_tree", + "complete_graph", + "complete_multipartite_graph", + "circular_ladder_graph", + "circulant_graph", + "cycle_graph", + "dorogovtsev_goltsev_mendes_graph", + "empty_graph", + "full_rary_tree", + "kneser_graph", + "ladder_graph", + "lollipop_graph", + "null_graph", + "path_graph", + "star_graph", + "tadpole_graph", + "trivial_graph", + "turan_graph", + "wheel_graph", +] + + +# ------------------------------------------------------------------- +# Some Classic Graphs +# ------------------------------------------------------------------- + + +def _tree_edges(n, r): + if n == 0: + return + # helper function for trees + # yields edges in rooted tree at 0 with n nodes and branching ratio r + nodes = iter(range(n)) + parents = [next(nodes)] # stack of max length r + while parents: + source = parents.pop(0) + for i in range(r): + try: + target = next(nodes) + parents.append(target) + yield source, target + except StopIteration: + break + + +@nx._dispatchable(graphs=None, returns_graph=True) +def full_rary_tree(r, n, create_using=None): + """Creates a full r-ary tree of `n` nodes. + + Sometimes called a k-ary, n-ary, or m-ary tree. + "... all non-leaf nodes have exactly r children and all levels + are full except for some rightmost position of the bottom level + (if a leaf at the bottom level is missing, then so are all of the + leaves to its right." [1]_ + + .. plot:: + + >>> nx.draw(nx.full_rary_tree(2, 10)) + + Parameters + ---------- + r : int + branching factor of the tree + n : int + Number of nodes in the tree + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + An r-ary tree with n nodes + + References + ---------- + .. [1] An introduction to data structures and algorithms, + James Andrew Storer, Birkhauser Boston 2001, (page 225). + """ + G = empty_graph(n, create_using) + G.add_edges_from(_tree_edges(n, r)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def kneser_graph(n, k): + """Returns the Kneser Graph with parameters `n` and `k`. + + The Kneser Graph has nodes that are k-tuples (subsets) of the integers + between 0 and ``n-1``. Nodes are adjacent if their corresponding sets are disjoint. + + Parameters + ---------- + n: int + Number of integers from which to make node subsets. + Subsets are drawn from ``set(range(n))``. + k: int + Size of the subsets. + + Returns + ------- + G : NetworkX Graph + + Examples + -------- + >>> G = nx.kneser_graph(5, 2) + >>> G.number_of_nodes() + 10 + >>> G.number_of_edges() + 15 + >>> nx.is_isomorphic(G, nx.petersen_graph()) + True + """ + if n <= 0: + raise NetworkXError("n should be greater than zero") + if k <= 0 or k > n: + raise NetworkXError("k should be greater than zero and smaller than n") + + G = nx.Graph() + # Create all k-subsets of [0, 1, ..., n-1] + subsets = list(itertools.combinations(range(n), k)) + + if 2 * k > n: + G.add_nodes_from(subsets) + + universe = set(range(n)) + comb = itertools.combinations # only to make it all fit on one line + G.add_edges_from((s, t) for s in subsets for t in comb(universe - set(s), k)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def balanced_tree(r, h, create_using=None): + """Returns the perfectly balanced `r`-ary tree of height `h`. + + .. plot:: + + >>> nx.draw(nx.balanced_tree(2, 3)) + + Parameters + ---------- + r : int + Branching factor of the tree; each node will have `r` + children. + + h : int + Height of the tree. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : NetworkX graph + A balanced `r`-ary tree of height `h`. + + Notes + ----- + This is the rooted tree where all leaves are at distance `h` from + the root. The root has degree `r` and all other internal nodes + have degree `r + 1`. + + Node labels are integers, starting from zero. + + A balanced tree is also known as a *complete r-ary tree*. + + """ + # The number of nodes in the balanced tree is `1 + r + ... + r^h`, + # which is computed by using the closed-form formula for a geometric + # sum with ratio `r`. In the special case that `r` is 1, the number + # of nodes is simply `h + 1` (since the tree is actually a path + # graph). + if r == 1: + n = h + 1 + else: + # This must be an integer if both `r` and `h` are integers. If + # they are not, we force integer division anyway. + n = (1 - r ** (h + 1)) // (1 - r) + return full_rary_tree(r, n, create_using=create_using) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def barbell_graph(m1, m2, create_using=None): + """Returns the Barbell Graph: two complete graphs connected by a path. + + .. plot:: + + >>> nx.draw(nx.barbell_graph(4, 2)) + + Parameters + ---------- + m1 : int + Size of the left and right barbells, must be greater than 2. + + m2 : int + Length of the path connecting the barbells. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Only undirected Graphs are supported. + + Returns + ------- + G : NetworkX graph + A barbell graph. + + Notes + ----- + + + Two identical complete graphs $K_{m1}$ form the left and right bells, + and are connected by a path $P_{m2}$. + + The `2*m1+m2` nodes are numbered + `0, ..., m1-1` for the left barbell, + `m1, ..., m1+m2-1` for the path, + and `m1+m2, ..., 2*m1+m2-1` for the right barbell. + + The 3 subgraphs are joined via the edges `(m1-1, m1)` and + `(m1+m2-1, m1+m2)`. If `m2=0`, this is merely two complete + graphs joined together. + + This graph is an extremal example in David Aldous + and Jim Fill's e-text on Random Walks on Graphs. + + """ + if m1 < 2: + raise NetworkXError("Invalid graph description, m1 should be >=2") + if m2 < 0: + raise NetworkXError("Invalid graph description, m2 should be >=0") + + # left barbell + G = complete_graph(m1, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + + # connecting path + G.add_nodes_from(range(m1, m1 + m2 - 1)) + if m2 > 1: + G.add_edges_from(pairwise(range(m1, m1 + m2))) + + # right barbell + G.add_edges_from( + (u, v) for u in range(m1 + m2, 2 * m1 + m2) for v in range(u + 1, 2 * m1 + m2) + ) + + # connect it up + G.add_edge(m1 - 1, m1) + if m2 > 0: + G.add_edge(m1 + m2 - 1, m1 + m2) + + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def binomial_tree(n, create_using=None): + """Returns the Binomial Tree of order n. + + The binomial tree of order 0 consists of a single node. A binomial tree of order k + is defined recursively by linking two binomial trees of order k-1: the root of one is + the leftmost child of the root of the other. + + .. plot:: + + >>> nx.draw(nx.binomial_tree(3)) + + Parameters + ---------- + n : int + Order of the binomial tree. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : NetworkX graph + A binomial tree of $2^n$ nodes and $2^n - 1$ edges. + + """ + G = nx.empty_graph(1, create_using) + + N = 1 + for i in range(n): + # Use G.edges() to ensure 2-tuples. G.edges is 3-tuple for MultiGraph + edges = [(u + N, v + N) for (u, v) in G.edges()] + G.add_edges_from(edges) + G.add_edge(0, N) + N *= 2 + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def complete_graph(n, create_using=None): + """Return the complete graph `K_n` with n nodes. + + A complete graph on `n` nodes means that all pairs + of distinct nodes have an edge connecting them. + + .. plot:: + + >>> nx.draw(nx.complete_graph(5)) + + Parameters + ---------- + n : int or iterable container of nodes + If n is an integer, nodes are from range(n). + If n is a container of nodes, those nodes appear in the graph. + Warning: n is not checked for duplicates and if present the + resulting graph may not be as desired. Make sure you have no duplicates. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Examples + -------- + >>> G = nx.complete_graph(9) + >>> len(G) + 9 + >>> G.size() + 36 + >>> G = nx.complete_graph(range(11, 14)) + >>> list(G.nodes()) + [11, 12, 13] + >>> G = nx.complete_graph(4, nx.DiGraph()) + >>> G.is_directed() + True + + """ + _, nodes = n + G = empty_graph(nodes, create_using) + if len(nodes) > 1: + if G.is_directed(): + edges = itertools.permutations(nodes, 2) + else: + edges = itertools.combinations(nodes, 2) + G.add_edges_from(edges) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def circular_ladder_graph(n, create_using=None): + """Returns the circular ladder graph $CL_n$ of length n. + + $CL_n$ consists of two concentric n-cycles in which + each of the n pairs of concentric nodes are joined by an edge. + + Node labels are the integers 0 to n-1 + + .. plot:: + + >>> nx.draw(nx.circular_ladder_graph(5)) + + """ + G = ladder_graph(n, create_using) + G.add_edge(0, n - 1) + G.add_edge(n, 2 * n - 1) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def circulant_graph(n, offsets, create_using=None): + r"""Returns the circulant graph $Ci_n(x_1, x_2, ..., x_m)$ with $n$ nodes. + + The circulant graph $Ci_n(x_1, ..., x_m)$ consists of $n$ nodes $0, ..., n-1$ + such that node $i$ is connected to nodes $(i + x) \mod n$ and $(i - x) \mod n$ + for all $x$ in $x_1, ..., x_m$. Thus $Ci_n(1)$ is a cycle graph. + + .. plot:: + + >>> nx.draw(nx.circulant_graph(10, [1])) + + Parameters + ---------- + n : integer + The number of nodes in the graph. + offsets : list of integers + A list of node offsets, $x_1$ up to $x_m$, as described above. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX Graph of type create_using + + Examples + -------- + Many well-known graph families are subfamilies of the circulant graphs; + for example, to create the cycle graph on n points, we connect every + node to nodes on either side (with offset plus or minus one). For n = 10, + + >>> G = nx.circulant_graph(10, [1]) + >>> edges = [ + ... (0, 9), + ... (0, 1), + ... (1, 2), + ... (2, 3), + ... (3, 4), + ... (4, 5), + ... (5, 6), + ... (6, 7), + ... (7, 8), + ... (8, 9), + ... ] + >>> sorted(edges) == sorted(G.edges()) + True + + Similarly, we can create the complete graph + on 5 points with the set of offsets [1, 2]: + + >>> G = nx.circulant_graph(5, [1, 2]) + >>> edges = [ + ... (0, 1), + ... (0, 2), + ... (0, 3), + ... (0, 4), + ... (1, 2), + ... (1, 3), + ... (1, 4), + ... (2, 3), + ... (2, 4), + ... (3, 4), + ... ] + >>> sorted(edges) == sorted(G.edges()) + True + + """ + G = empty_graph(n, create_using) + G.add_edges_from((i, (i - j) % n) for i in range(n) for j in offsets) + G.add_edges_from((i, (i + j) % n) for i in range(n) for j in offsets) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def cycle_graph(n, create_using=None): + """Returns the cycle graph $C_n$ of cyclically connected nodes. + + $C_n$ is a path with its two end-nodes connected. + + .. plot:: + + >>> nx.draw(nx.cycle_graph(5)) + + Parameters + ---------- + n : int or iterable container of nodes + If n is an integer, nodes are from `range(n)`. + If n is a container of nodes, those nodes appear in the graph. + Warning: n is not checked for duplicates and if present the + resulting graph may not be as desired. Make sure you have no duplicates. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Notes + ----- + If create_using is directed, the direction is in increasing order. + + """ + _, nodes = n + G = empty_graph(nodes, create_using) + G.add_edges_from(pairwise(nodes, cyclic=True)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def dorogovtsev_goltsev_mendes_graph(n, create_using=None): + """Returns the hierarchically constructed Dorogovtsev--Goltsev--Mendes graph. + + The Dorogovtsev--Goltsev--Mendes [1]_ procedure deterministically produces a + scale-free graph with ``3/2 * (3**(n-1) + 1)`` nodes + and ``3**n`` edges for a given `n`. + + Note that `n` denotes the number of times the state transition is applied, + starting from the base graph with ``n = 0`` (no transitions), as in [2]_. + This is different from the parameter ``t = n - 1`` in [1]_. + + .. plot:: + + >>> nx.draw(nx.dorogovtsev_goltsev_mendes_graph(3)) + + Parameters + ---------- + n : integer + The generation number. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. Directed graphs and multigraphs are not supported. + + Returns + ------- + G : NetworkX `Graph` + + Raises + ------ + NetworkXError + If `n` is less than zero. + + If `create_using` is a directed graph or multigraph. + + Examples + -------- + >>> G = nx.dorogovtsev_goltsev_mendes_graph(3) + >>> G.number_of_nodes() + 15 + >>> G.number_of_edges() + 27 + >>> nx.is_planar(G) + True + + References + ---------- + .. [1] S. N. Dorogovtsev, A. V. Goltsev and J. F. F. Mendes, + "Pseudofractal scale-free web", Physical Review E 65, 066122, 2002. + https://arxiv.org/pdf/cond-mat/0112143.pdf + .. [2] Weisstein, Eric W. "Dorogovtsev--Goltsev--Mendes Graph". + From MathWorld--A Wolfram Web Resource. + https://mathworld.wolfram.com/Dorogovtsev-Goltsev-MendesGraph.html + """ + if n < 0: + raise NetworkXError("n must be greater than or equal to 0") + + G = empty_graph(0, create_using) + if G.is_directed(): + raise NetworkXError("directed graph not supported") + if G.is_multigraph(): + raise NetworkXError("multigraph not supported") + + G.add_edge(0, 1) + new_node = 2 # next node to be added + for _ in range(n): # iterate over number of generations. + new_edges = [] + for u, v in G.edges(): + new_edges.append((u, new_node)) + new_edges.append((v, new_node)) + new_node += 1 + + G.add_edges_from(new_edges) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def empty_graph(n=0, create_using=None, default=Graph): + """Returns the empty graph with n nodes and zero edges. + + .. plot:: + + >>> nx.draw(nx.empty_graph(5)) + + Parameters + ---------- + n : int or iterable container of nodes (default = 0) + If n is an integer, nodes are from `range(n)`. + If n is a container of nodes, those nodes appear in the graph. + create_using : Graph Instance, Constructor or None + Indicator of type of graph to return. + If a Graph-type instance, then clear and use it. + If None, use the `default` constructor. + If a constructor, call it to create an empty graph. + default : Graph constructor (optional, default = nx.Graph) + The constructor to use if create_using is None. + If None, then nx.Graph is used. + This is used when passing an unknown `create_using` value + through your home-grown function to `empty_graph` and + you want a default constructor other than nx.Graph. + + Examples + -------- + >>> G = nx.empty_graph(10) + >>> G.number_of_nodes() + 10 + >>> G.number_of_edges() + 0 + >>> G = nx.empty_graph("ABC") + >>> G.number_of_nodes() + 3 + >>> sorted(G) + ['A', 'B', 'C'] + + Notes + ----- + The variable create_using should be a Graph Constructor or a + "graph"-like object. Constructors, e.g. `nx.Graph` or `nx.MultiGraph` + will be used to create the returned graph. "graph"-like objects + will be cleared (nodes and edges will be removed) and refitted as + an empty "graph" with nodes specified in n. This capability + is useful for specifying the class-nature of the resulting empty + "graph" (i.e. Graph, DiGraph, MyWeirdGraphClass, etc.). + + The variable create_using has three main uses: + Firstly, the variable create_using can be used to create an + empty digraph, multigraph, etc. For example, + + >>> n = 10 + >>> G = nx.empty_graph(n, create_using=nx.DiGraph) + + will create an empty digraph on n nodes. + + Secondly, one can pass an existing graph (digraph, multigraph, + etc.) via create_using. For example, if G is an existing graph + (resp. digraph, multigraph, etc.), then empty_graph(n, create_using=G) + will empty G (i.e. delete all nodes and edges using G.clear()) + and then add n nodes and zero edges, and return the modified graph. + + Thirdly, when constructing your home-grown graph creation function + you can use empty_graph to construct the graph by passing a user + defined create_using to empty_graph. In this case, if you want the + default constructor to be other than nx.Graph, specify `default`. + + >>> def mygraph(n, create_using=None): + ... G = nx.empty_graph(n, create_using, nx.MultiGraph) + ... G.add_edges_from([(0, 1), (0, 1)]) + ... return G + >>> G = mygraph(3) + >>> G.is_multigraph() + True + >>> G = mygraph(3, nx.Graph) + >>> G.is_multigraph() + False + + See also create_empty_copy(G). + + """ + if create_using is None: + G = default() + elif isinstance(create_using, type): + G = create_using() + elif not hasattr(create_using, "adj"): + raise TypeError("create_using is not a valid NetworkX graph type or instance") + else: + # create_using is a NetworkX style Graph + create_using.clear() + G = create_using + + _, nodes = n + G.add_nodes_from(nodes) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def ladder_graph(n, create_using=None): + """Returns the Ladder graph of length n. + + This is two paths of n nodes, with + each pair connected by a single edge. + + Node labels are the integers 0 to 2*n - 1. + + .. plot:: + + >>> nx.draw(nx.ladder_graph(5)) + + """ + G = empty_graph(2 * n, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + G.add_edges_from(pairwise(range(n))) + G.add_edges_from(pairwise(range(n, 2 * n))) + G.add_edges_from((v, v + n) for v in range(n)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number([0, 1]) +def lollipop_graph(m, n, create_using=None): + """Returns the Lollipop Graph; ``K_m`` connected to ``P_n``. + + This is the Barbell Graph without the right barbell. + + .. plot:: + + >>> nx.draw(nx.lollipop_graph(3, 4)) + + Parameters + ---------- + m, n : int or iterable container of nodes + If an integer, nodes are from ``range(m)`` and ``range(m, m+n)``. + If a container of nodes, those nodes appear in the graph. + Warning: `m` and `n` are not checked for duplicates and if present the + resulting graph may not be as desired. Make sure you have no duplicates. + + The nodes for `m` appear in the complete graph $K_m$ and the nodes + for `n` appear in the path $P_n$ + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + Networkx graph + A complete graph with `m` nodes connected to a path of length `n`. + + Notes + ----- + The 2 subgraphs are joined via an edge ``(m-1, m)``. + If ``n=0``, this is merely a complete graph. + + (This graph is an extremal example in David Aldous and Jim + Fill's etext on Random Walks on Graphs.) + + """ + m, m_nodes = m + M = len(m_nodes) + if M < 2: + raise NetworkXError("Invalid description: m should indicate at least 2 nodes") + + n, n_nodes = n + if isinstance(m, numbers.Integral) and isinstance(n, numbers.Integral): + n_nodes = list(range(M, M + n)) + N = len(n_nodes) + + # the ball + G = complete_graph(m_nodes, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + + # the stick + G.add_nodes_from(n_nodes) + if N > 1: + G.add_edges_from(pairwise(n_nodes)) + + if len(G) != M + N: + raise NetworkXError("Nodes must be distinct in containers m and n") + + # connect ball to stick + if M > 0 and N > 0: + G.add_edge(m_nodes[-1], n_nodes[0]) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def null_graph(create_using=None): + """Returns the Null graph with no nodes or edges. + + See empty_graph for the use of create_using. + + """ + G = empty_graph(0, create_using) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def path_graph(n, create_using=None): + """Returns the Path graph `P_n` of linearly connected nodes. + + .. plot:: + + >>> nx.draw(nx.path_graph(5)) + + Parameters + ---------- + n : int or iterable + If an integer, nodes are 0 to n - 1. + If an iterable of nodes, in the order they appear in the path. + Warning: n is not checked for duplicates and if present the + resulting graph may not be as desired. Make sure you have no duplicates. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + """ + _, nodes = n + G = empty_graph(nodes, create_using) + G.add_edges_from(pairwise(nodes)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def star_graph(n, create_using=None): + """Return a star graph. + + The star graph consists of one center node connected to `n` outer nodes. + + .. plot:: + + >>> nx.draw(nx.star_graph(6)) + + Parameters + ---------- + n : int or iterable + If an integer, node labels are ``0`` to `n`, with center ``0``. + If an iterable of nodes, the center is the first. + Warning: `n` is not checked for duplicates and if present, the + resulting graph may not be as desired. Make sure you have no duplicates. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Examples + -------- + A star graph with 3 spokes can be generated with + + >>> G = nx.star_graph(3) + >>> sorted(G.edges) + [(0, 1), (0, 2), (0, 3)] + + For directed graphs, the convention is to have edges pointing from the hub + to the spokes: + + >>> DG1 = nx.star_graph(3, create_using=nx.DiGraph) + >>> sorted(DG1.edges) + [(0, 1), (0, 2), (0, 3)] + + Other possible definitions have edges pointing from the spokes to the hub: + + >>> DG2 = nx.star_graph(3, create_using=nx.DiGraph).reverse() + >>> sorted(DG2.edges) + [(1, 0), (2, 0), (3, 0)] + + or have bidirectional edges: + + >>> DG3 = nx.star_graph(3).to_directed() + >>> sorted(DG3.edges) + [(0, 1), (0, 2), (0, 3), (1, 0), (2, 0), (3, 0)] + + Notes + ----- + The graph has ``n + 1`` nodes for integer `n`. + So ``star_graph(3)`` is the same as ``star_graph(range(4))``. + """ + n, nodes = n + if isinstance(n, numbers.Integral): + nodes.append(int(n)) # There should be n + 1 nodes. + G = empty_graph(nodes, create_using) + + if len(nodes) > 1: + hub, *spokes = nodes + G.add_edges_from((hub, node) for node in spokes) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number([0, 1]) +def tadpole_graph(m, n, create_using=None): + """Returns the (m,n)-tadpole graph; ``C_m`` connected to ``P_n``. + + This graph on m+n nodes connects a cycle of size `m` to a path of length `n`. + It looks like a tadpole. It is also called a kite graph or a dragon graph. + + .. plot:: + + >>> nx.draw(nx.tadpole_graph(3, 5)) + + Parameters + ---------- + m, n : int or iterable container of nodes + If an integer, nodes are from ``range(m)`` and ``range(m,m+n)``. + If a container of nodes, those nodes appear in the graph. + Warning: `m` and `n` are not checked for duplicates and if present the + resulting graph may not be as desired. + + The nodes for `m` appear in the cycle graph $C_m$ and the nodes + for `n` appear in the path $P_n$. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + Networkx graph + A cycle of size `m` connected to a path of length `n`. + + Raises + ------ + NetworkXError + If ``m < 2``. The tadpole graph is undefined for ``m<2``. + + Notes + ----- + The 2 subgraphs are joined via an edge ``(m-1, m)``. + If ``n=0``, this is a cycle graph. + `m` and/or `n` can be a container of nodes instead of an integer. + + """ + m, m_nodes = m + M = len(m_nodes) + if M < 2: + raise NetworkXError("Invalid description: m should indicate at least 2 nodes") + + n, n_nodes = n + if isinstance(m, numbers.Integral) and isinstance(n, numbers.Integral): + n_nodes = list(range(M, M + n)) + + # the circle + G = cycle_graph(m_nodes, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + + # the stick + nx.add_path(G, [m_nodes[-1]] + list(n_nodes)) + + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def trivial_graph(create_using=None): + """Return the Trivial graph with one node (with label 0) and no edges. + + .. plot:: + + >>> nx.draw(nx.trivial_graph(), with_labels=True) + + """ + G = empty_graph(1, create_using) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def turan_graph(n, r): + r"""Return the Turan Graph + + The Turan Graph is a complete multipartite graph on $n$ nodes + with $r$ disjoint subsets. That is, edges connect each node to + every node not in its subset. + + Given $n$ and $r$, we create a complete multipartite graph with + $r-(n \mod r)$ partitions of size $n/r$, rounded down, and + $n \mod r$ partitions of size $n/r+1$, rounded down. + + .. plot:: + + >>> nx.draw(nx.turan_graph(6, 2)) + + Parameters + ---------- + n : int + The number of nodes. + r : int + The number of partitions. + Must be less than or equal to n. + + Notes + ----- + Must satisfy $1 <= r <= n$. + The graph has $(r-1)(n^2)/(2r)$ edges, rounded down. + """ + + if not 1 <= r <= n: + raise NetworkXError("Must satisfy 1 <= r <= n") + + partitions = [n // r] * (r - (n % r)) + [n // r + 1] * (n % r) + G = complete_multipartite_graph(*partitions) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number(0) +def wheel_graph(n, create_using=None): + """Return the wheel graph + + The wheel graph consists of a hub node connected to a cycle of (n-1) nodes. + + .. plot:: + + >>> nx.draw(nx.wheel_graph(5)) + + Parameters + ---------- + n : int or iterable + If an integer, node labels are 0 to n with center 0. + If an iterable of nodes, the center is the first. + Warning: n is not checked for duplicates and if present the + resulting graph may not be as desired. Make sure you have no duplicates. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Node labels are the integers 0 to n - 1. + """ + _, nodes = n + G = empty_graph(nodes, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + + if len(nodes) > 1: + hub, *rim = nodes + G.add_edges_from((hub, node) for node in rim) + if len(rim) > 1: + G.add_edges_from(pairwise(rim, cyclic=True)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def complete_multipartite_graph(*subset_sizes): + """Returns the complete multipartite graph with the specified subset sizes. + + .. plot:: + + >>> nx.draw(nx.complete_multipartite_graph(1, 2, 3)) + + Parameters + ---------- + subset_sizes : tuple of integers or tuple of node iterables + The arguments can either all be integer number of nodes or they + can all be iterables of nodes. If integers, they represent the + number of nodes in each subset of the multipartite graph. + If iterables, each is used to create the nodes for that subset. + The length of subset_sizes is the number of subsets. + + Returns + ------- + G : NetworkX Graph + Returns the complete multipartite graph with the specified subsets. + + For each node, the node attribute 'subset' is an integer + indicating which subset contains the node. + + Examples + -------- + Creating a complete tripartite graph, with subsets of one, two, and three + nodes, respectively. + + >>> G = nx.complete_multipartite_graph(1, 2, 3) + >>> [G.nodes[u]["subset"] for u in G] + [0, 1, 1, 2, 2, 2] + >>> list(G.edges(0)) + [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5)] + >>> list(G.edges(2)) + [(2, 0), (2, 3), (2, 4), (2, 5)] + >>> list(G.edges(4)) + [(4, 0), (4, 1), (4, 2)] + + >>> G = nx.complete_multipartite_graph("a", "bc", "def") + >>> [G.nodes[u]["subset"] for u in sorted(G)] + [0, 1, 1, 2, 2, 2] + + Notes + ----- + This function generalizes several other graph builder functions. + + - If no subset sizes are given, this returns the null graph. + - If a single subset size `n` is given, this returns the empty graph on + `n` nodes. + - If two subset sizes `m` and `n` are given, this returns the complete + bipartite graph on `m + n` nodes. + - If subset sizes `1` and `n` are given, this returns the star graph on + `n + 1` nodes. + + See also + -------- + complete_bipartite_graph + """ + # The complete multipartite graph is an undirected simple graph. + G = Graph() + + if len(subset_sizes) == 0: + return G + + # set up subsets of nodes + try: + extents = pairwise(itertools.accumulate((0,) + subset_sizes)) + subsets = [range(start, end) for start, end in extents] + except TypeError: + subsets = subset_sizes + else: + if any(size < 0 for size in subset_sizes): + raise NetworkXError(f"Negative number of nodes not valid: {subset_sizes}") + + # add nodes with subset attribute + # while checking that ints are not mixed with iterables + try: + for i, subset in enumerate(subsets): + G.add_nodes_from(subset, subset=i) + except TypeError as err: + raise NetworkXError("Arguments must be all ints or all iterables") from err + + # Across subsets, all nodes should be adjacent. + # We can use itertools.combinations() because undirected. + for subset1, subset2 in itertools.combinations(subsets, 2): + G.add_edges_from(itertools.product(subset1, subset2)) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/cographs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/cographs.py new file mode 100644 index 0000000000000000000000000000000000000000..6635b32f691696c1b6f309ad0da81c3cbc43bed9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/cographs.py @@ -0,0 +1,68 @@ +r"""Generators for cographs + +A cograph is a graph containing no path on four vertices. +Cographs or $P_4$-free graphs can be obtained from a single vertex +by disjoint union and complementation operations. + +References +---------- +.. [0] D.G. Corneil, H. Lerchs, L.Stewart Burlingham, + "Complement reducible graphs", + Discrete Applied Mathematics, Volume 3, Issue 3, 1981, Pages 163-174, + ISSN 0166-218X. +""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = ["random_cograph"] + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_cograph(n, seed=None): + r"""Returns a random cograph with $2 ^ n$ nodes. + + A cograph is a graph containing no path on four vertices. + Cographs or $P_4$-free graphs can be obtained from a single vertex + by disjoint union and complementation operations. + + This generator starts off from a single vertex and performs disjoint + union and full join operations on itself. + The decision on which operation will take place is random. + + Parameters + ---------- + n : int + The order of the cograph. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : A random graph containing no path on four vertices. + + See Also + -------- + full_join + union + + References + ---------- + .. [1] D.G. Corneil, H. Lerchs, L.Stewart Burlingham, + "Complement reducible graphs", + Discrete Applied Mathematics, Volume 3, Issue 3, 1981, Pages 163-174, + ISSN 0166-218X. + """ + R = nx.empty_graph(1) + + for i in range(n): + RR = nx.relabel_nodes(R.copy(), lambda x: x + len(R)) + + if seed.randint(0, 1) == 0: + R = nx.full_join(R, RR) + else: + R = nx.disjoint_union(R, RR) + + return R diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/community.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/community.py new file mode 100644 index 0000000000000000000000000000000000000000..a7f2294c5cf9137dc1fab2a50d7ffcd6c59b6dec --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/community.py @@ -0,0 +1,1070 @@ +"""Generators for classes of graphs used in studying social networks.""" + +import itertools +import math + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "caveman_graph", + "connected_caveman_graph", + "relaxed_caveman_graph", + "random_partition_graph", + "planted_partition_graph", + "gaussian_random_partition_graph", + "ring_of_cliques", + "windmill_graph", + "stochastic_block_model", + "LFR_benchmark_graph", +] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def caveman_graph(l, k): + """Returns a caveman graph of `l` cliques of size `k`. + + Parameters + ---------- + l : int + Number of cliques + k : int + Size of cliques + + Returns + ------- + G : NetworkX Graph + caveman graph + + Notes + ----- + This returns an undirected graph, it can be converted to a directed + graph using :func:`nx.to_directed`, or a multigraph using + ``nx.MultiGraph(nx.caveman_graph(l, k))``. Only the undirected version is + described in [1]_ and it is unclear which of the directed + generalizations is most useful. + + Examples + -------- + >>> G = nx.caveman_graph(3, 3) + + See also + -------- + + connected_caveman_graph + + References + ---------- + .. [1] Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.' + Amer. J. Soc. 105, 493-527, 1999. + """ + # l disjoint cliques of size k + G = nx.empty_graph(l * k) + if k > 1: + for start in range(0, l * k, k): + edges = itertools.combinations(range(start, start + k), 2) + G.add_edges_from(edges) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def connected_caveman_graph(l, k): + """Returns a connected caveman graph of `l` cliques of size `k`. + + The connected caveman graph is formed by creating `n` cliques of size + `k`, then a single edge in each clique is rewired to a node in an + adjacent clique. + + Parameters + ---------- + l : int + number of cliques + k : int + size of cliques (k at least 2 or NetworkXError is raised) + + Returns + ------- + G : NetworkX Graph + connected caveman graph + + Raises + ------ + NetworkXError + If the size of cliques `k` is smaller than 2. + + Notes + ----- + This returns an undirected graph, it can be converted to a directed + graph using :func:`nx.to_directed`, or a multigraph using + ``nx.MultiGraph(nx.caveman_graph(l, k))``. Only the undirected version is + described in [1]_ and it is unclear which of the directed + generalizations is most useful. + + Examples + -------- + >>> G = nx.connected_caveman_graph(3, 3) + + References + ---------- + .. [1] Watts, D. J. 'Networks, Dynamics, and the Small-World Phenomenon.' + Amer. J. Soc. 105, 493-527, 1999. + """ + if k < 2: + raise nx.NetworkXError( + "The size of cliques in a connected caveman graph must be at least 2." + ) + + G = nx.caveman_graph(l, k) + for start in range(0, l * k, k): + G.remove_edge(start, start + 1) + G.add_edge(start, (start - 1) % (l * k)) + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def relaxed_caveman_graph(l, k, p, seed=None): + """Returns a relaxed caveman graph. + + A relaxed caveman graph starts with `l` cliques of size `k`. Edges are + then randomly rewired with probability `p` to link different cliques. + + Parameters + ---------- + l : int + Number of groups + k : int + Size of cliques + p : float + Probability of rewiring each edge. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : NetworkX Graph + Relaxed Caveman Graph + + Raises + ------ + NetworkXError + If p is not in [0,1] + + Examples + -------- + >>> G = nx.relaxed_caveman_graph(2, 3, 0.1, seed=42) + + References + ---------- + .. [1] Santo Fortunato, Community Detection in Graphs, + Physics Reports Volume 486, Issues 3-5, February 2010, Pages 75-174. + https://arxiv.org/abs/0906.0612 + """ + G = nx.caveman_graph(l, k) + nodes = list(G) + for u, v in G.edges(): + if seed.random() < p: # rewire the edge + x = seed.choice(nodes) + if G.has_edge(u, x): + continue + G.remove_edge(u, v) + G.add_edge(u, x) + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_partition_graph(sizes, p_in, p_out, seed=None, directed=False): + """Returns the random partition graph with a partition of sizes. + + A partition graph is a graph of communities with sizes defined by + s in sizes. Nodes in the same group are connected with probability + p_in and nodes of different groups are connected with probability + p_out. + + Parameters + ---------- + sizes : list of ints + Sizes of groups + p_in : float + probability of edges with in groups + p_out : float + probability of edges between groups + directed : boolean optional, default=False + Whether to create a directed graph + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : NetworkX Graph or DiGraph + random partition graph of size sum(gs) + + Raises + ------ + NetworkXError + If p_in or p_out is not in [0,1] + + Examples + -------- + >>> G = nx.random_partition_graph([10, 10, 10], 0.25, 0.01) + >>> len(G) + 30 + >>> partition = G.graph["partition"] + >>> len(partition) + 3 + + Notes + ----- + This is a generalization of the planted-l-partition described in + [1]_. It allows for the creation of groups of any size. + + The partition is store as a graph attribute 'partition'. + + References + ---------- + .. [1] Santo Fortunato 'Community Detection in Graphs' Physical Reports + Volume 486, Issue 3-5 p. 75-174. https://arxiv.org/abs/0906.0612 + """ + # Use geometric method for O(n+m) complexity algorithm + # partition = nx.community_sets(nx.get_node_attributes(G, 'affiliation')) + if not 0.0 <= p_in <= 1.0: + raise nx.NetworkXError("p_in must be in [0,1]") + if not 0.0 <= p_out <= 1.0: + raise nx.NetworkXError("p_out must be in [0,1]") + + # create connection matrix + num_blocks = len(sizes) + p = [[p_out for s in range(num_blocks)] for r in range(num_blocks)] + for r in range(num_blocks): + p[r][r] = p_in + + return stochastic_block_model( + sizes, + p, + nodelist=None, + seed=seed, + directed=directed, + selfloops=False, + sparse=True, + ) + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def planted_partition_graph(l, k, p_in, p_out, seed=None, directed=False): + """Returns the planted l-partition graph. + + This model partitions a graph with n=l*k vertices in + l groups with k vertices each. Vertices of the same + group are linked with a probability p_in, and vertices + of different groups are linked with probability p_out. + + Parameters + ---------- + l : int + Number of groups + k : int + Number of vertices in each group + p_in : float + probability of connecting vertices within a group + p_out : float + probability of connected vertices between groups + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + directed : bool,optional (default=False) + If True return a directed graph + + Returns + ------- + G : NetworkX Graph or DiGraph + planted l-partition graph + + Raises + ------ + NetworkXError + If `p_in`, `p_out` are not in `[0, 1]` + + Examples + -------- + >>> G = nx.planted_partition_graph(4, 3, 0.5, 0.1, seed=42) + + See Also + -------- + random_partition_model + + References + ---------- + .. [1] A. Condon, R.M. Karp, Algorithms for graph partitioning + on the planted partition model, + Random Struct. Algor. 18 (2001) 116-140. + + .. [2] Santo Fortunato 'Community Detection in Graphs' Physical Reports + Volume 486, Issue 3-5 p. 75-174. https://arxiv.org/abs/0906.0612 + """ + return random_partition_graph([k] * l, p_in, p_out, seed=seed, directed=directed) + + +@py_random_state(6) +@nx._dispatchable(graphs=None, returns_graph=True) +def gaussian_random_partition_graph(n, s, v, p_in, p_out, directed=False, seed=None): + """Generate a Gaussian random partition graph. + + A Gaussian random partition graph is created by creating k partitions + each with a size drawn from a normal distribution with mean s and variance + s/v. Nodes are connected within clusters with probability p_in and + between clusters with probability p_out[1] + + Parameters + ---------- + n : int + Number of nodes in the graph + s : float + Mean cluster size + v : float + Shape parameter. The variance of cluster size distribution is s/v. + p_in : float + Probability of intra cluster connection. + p_out : float + Probability of inter cluster connection. + directed : boolean, optional default=False + Whether to create a directed graph or not + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : NetworkX Graph or DiGraph + gaussian random partition graph + + Raises + ------ + NetworkXError + If s is > n + If p_in or p_out is not in [0,1] + + Notes + ----- + Note the number of partitions is dependent on s,v and n, and that the + last partition may be considerably smaller, as it is sized to simply + fill out the nodes [1] + + See Also + -------- + random_partition_graph + + Examples + -------- + >>> G = nx.gaussian_random_partition_graph(100, 10, 10, 0.25, 0.1) + >>> len(G) + 100 + + References + ---------- + .. [1] Ulrik Brandes, Marco Gaertler, Dorothea Wagner, + Experiments on Graph Clustering Algorithms, + In the proceedings of the 11th Europ. Symp. Algorithms, 2003. + """ + if s > n: + raise nx.NetworkXError("s must be <= n") + assigned = 0 + sizes = [] + while True: + size = int(seed.gauss(s, s / v + 0.5)) + if size < 1: # how to handle 0 or negative sizes? + continue + if assigned + size >= n: + sizes.append(n - assigned) + break + assigned += size + sizes.append(size) + return random_partition_graph(sizes, p_in, p_out, seed=seed, directed=directed) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def ring_of_cliques(num_cliques, clique_size): + """Defines a "ring of cliques" graph. + + A ring of cliques graph is consisting of cliques, connected through single + links. Each clique is a complete graph. + + Parameters + ---------- + num_cliques : int + Number of cliques + clique_size : int + Size of cliques + + Returns + ------- + G : NetworkX Graph + ring of cliques graph + + Raises + ------ + NetworkXError + If the number of cliques is lower than 2 or + if the size of cliques is smaller than 2. + + Examples + -------- + >>> G = nx.ring_of_cliques(8, 4) + + See Also + -------- + connected_caveman_graph + + Notes + ----- + The `connected_caveman_graph` graph removes a link from each clique to + connect it with the next clique. Instead, the `ring_of_cliques` graph + simply adds the link without removing any link from the cliques. + """ + if num_cliques < 2: + raise nx.NetworkXError("A ring of cliques must have at least two cliques") + if clique_size < 2: + raise nx.NetworkXError("The cliques must have at least two nodes") + + G = nx.Graph() + for i in range(num_cliques): + edges = itertools.combinations( + range(i * clique_size, i * clique_size + clique_size), 2 + ) + G.add_edges_from(edges) + G.add_edge( + i * clique_size + 1, (i + 1) * clique_size % (num_cliques * clique_size) + ) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def windmill_graph(n, k): + """Generate a windmill graph. + A windmill graph is a graph of `n` cliques each of size `k` that are all + joined at one node. + It can be thought of as taking a disjoint union of `n` cliques of size `k`, + selecting one point from each, and contracting all of the selected points. + Alternatively, one could generate `n` cliques of size `k-1` and one node + that is connected to all other nodes in the graph. + + Parameters + ---------- + n : int + Number of cliques + k : int + Size of cliques + + Returns + ------- + G : NetworkX Graph + windmill graph with n cliques of size k + + Raises + ------ + NetworkXError + If the number of cliques is less than two + If the size of the cliques are less than two + + Examples + -------- + >>> G = nx.windmill_graph(4, 5) + + Notes + ----- + The node labeled `0` will be the node connected to all other nodes. + Note that windmill graphs are usually denoted `Wd(k,n)`, so the parameters + are in the opposite order as the parameters of this method. + """ + if n < 2: + msg = "A windmill graph must have at least two cliques" + raise nx.NetworkXError(msg) + if k < 2: + raise nx.NetworkXError("The cliques must have at least two nodes") + + G = nx.disjoint_union_all( + itertools.chain( + [nx.complete_graph(k)], (nx.complete_graph(k - 1) for _ in range(n - 1)) + ) + ) + G.add_edges_from((0, i) for i in range(k, G.number_of_nodes())) + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def stochastic_block_model( + sizes, p, nodelist=None, seed=None, directed=False, selfloops=False, sparse=True +): + """Returns a stochastic block model graph. + + This model partitions the nodes in blocks of arbitrary sizes, and places + edges between pairs of nodes independently, with a probability that depends + on the blocks. + + Parameters + ---------- + sizes : list of ints + Sizes of blocks + p : list of list of floats + Element (r,s) gives the density of edges going from the nodes + of group r to nodes of group s. + p must match the number of groups (len(sizes) == len(p)), + and it must be symmetric if the graph is undirected. + nodelist : list, optional + The block tags are assigned according to the node identifiers + in nodelist. If nodelist is None, then the ordering is the + range [0,sum(sizes)-1]. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + directed : boolean optional, default=False + Whether to create a directed graph or not. + selfloops : boolean optional, default=False + Whether to include self-loops or not. + sparse: boolean optional, default=True + Use the sparse heuristic to speed up the generator. + + Returns + ------- + g : NetworkX Graph or DiGraph + Stochastic block model graph of size sum(sizes) + + Raises + ------ + NetworkXError + If probabilities are not in [0,1]. + If the probability matrix is not square (directed case). + If the probability matrix is not symmetric (undirected case). + If the sizes list does not match nodelist or the probability matrix. + If nodelist contains duplicate. + + Examples + -------- + >>> sizes = [75, 75, 300] + >>> probs = [[0.25, 0.05, 0.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]] + >>> g = nx.stochastic_block_model(sizes, probs, seed=0) + >>> len(g) + 450 + >>> H = nx.quotient_graph(g, g.graph["partition"], relabel=True) + >>> for v in H.nodes(data=True): + ... print(round(v[1]["density"], 3)) + 0.245 + 0.348 + 0.405 + >>> for v in H.edges(data=True): + ... print(round(1.0 * v[2]["weight"] / (sizes[v[0]] * sizes[v[1]]), 3)) + 0.051 + 0.022 + 0.07 + + See Also + -------- + random_partition_graph + planted_partition_graph + gaussian_random_partition_graph + gnp_random_graph + + References + ---------- + .. [1] Holland, P. W., Laskey, K. B., & Leinhardt, S., + "Stochastic blockmodels: First steps", + Social networks, 5(2), 109-137, 1983. + """ + # Check if dimensions match + if len(sizes) != len(p): + raise nx.NetworkXException("'sizes' and 'p' do not match.") + # Check for probability symmetry (undirected) and shape (directed) + for row in p: + if len(p) != len(row): + raise nx.NetworkXException("'p' must be a square matrix.") + if not directed: + p_transpose = [list(i) for i in zip(*p)] + for i in zip(p, p_transpose): + for j in zip(i[0], i[1]): + if abs(j[0] - j[1]) > 1e-08: + raise nx.NetworkXException("'p' must be symmetric.") + # Check for probability range + for row in p: + for prob in row: + if prob < 0 or prob > 1: + raise nx.NetworkXException("Entries of 'p' not in [0,1].") + # Check for nodelist consistency + if nodelist is not None: + if len(nodelist) != sum(sizes): + raise nx.NetworkXException("'nodelist' and 'sizes' do not match.") + if len(nodelist) != len(set(nodelist)): + raise nx.NetworkXException("nodelist contains duplicate.") + else: + nodelist = range(sum(sizes)) + + # Setup the graph conditionally to the directed switch. + block_range = range(len(sizes)) + if directed: + g = nx.DiGraph() + block_iter = itertools.product(block_range, block_range) + else: + g = nx.Graph() + block_iter = itertools.combinations_with_replacement(block_range, 2) + # Split nodelist in a partition (list of sets). + size_cumsum = [sum(sizes[0:x]) for x in range(len(sizes) + 1)] + g.graph["partition"] = [ + set(nodelist[size_cumsum[x] : size_cumsum[x + 1]]) + for x in range(len(size_cumsum) - 1) + ] + # Setup nodes and graph name + for block_id, nodes in enumerate(g.graph["partition"]): + for node in nodes: + g.add_node(node, block=block_id) + + g.name = "stochastic_block_model" + + # Test for edge existence + parts = g.graph["partition"] + for i, j in block_iter: + if i == j: + if directed: + if selfloops: + edges = itertools.product(parts[i], parts[i]) + else: + edges = itertools.permutations(parts[i], 2) + else: + edges = itertools.combinations(parts[i], 2) + if selfloops: + edges = itertools.chain(edges, zip(parts[i], parts[i])) + for e in edges: + if seed.random() < p[i][j]: + g.add_edge(*e) + else: + edges = itertools.product(parts[i], parts[j]) + if sparse: + if p[i][j] == 1: # Test edges cases p_ij = 0 or 1 + for e in edges: + g.add_edge(*e) + elif p[i][j] > 0: + while True: + try: + logrand = math.log(seed.random()) + skip = math.floor(logrand / math.log(1 - p[i][j])) + # consume "skip" edges + next(itertools.islice(edges, skip, skip), None) + e = next(edges) + g.add_edge(*e) # __safe + except StopIteration: + break + else: + for e in edges: + if seed.random() < p[i][j]: + g.add_edge(*e) # __safe + return g + + +def _zipf_rv_below(gamma, xmin, threshold, seed): + """Returns a random value chosen from the bounded Zipf distribution. + + Repeatedly draws values from the Zipf distribution until the + threshold is met, then returns that value. + """ + result = nx.utils.zipf_rv(gamma, xmin, seed) + while result > threshold: + result = nx.utils.zipf_rv(gamma, xmin, seed) + return result + + +def _powerlaw_sequence(gamma, low, high, condition, length, max_iters, seed): + """Returns a list of numbers obeying a constrained power law distribution. + + ``gamma`` and ``low`` are the parameters for the Zipf distribution. + + ``high`` is the maximum allowed value for values draw from the Zipf + distribution. For more information, see :func:`_zipf_rv_below`. + + ``condition`` and ``length`` are Boolean-valued functions on + lists. While generating the list, random values are drawn and + appended to the list until ``length`` is satisfied by the created + list. Once ``condition`` is satisfied, the sequence generated in + this way is returned. + + ``max_iters`` indicates the number of times to generate a list + satisfying ``length``. If the number of iterations exceeds this + value, :exc:`~networkx.exception.ExceededMaxIterations` is raised. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + """ + for i in range(max_iters): + seq = [] + while not length(seq): + seq.append(_zipf_rv_below(gamma, low, high, seed)) + if condition(seq): + return seq + raise nx.ExceededMaxIterations("Could not create power law sequence") + + +def _hurwitz_zeta(x, q, tolerance): + """The Hurwitz zeta function, or the Riemann zeta function of two arguments. + + ``x`` must be greater than one and ``q`` must be positive. + + This function repeatedly computes subsequent partial sums until + convergence, as decided by ``tolerance``. + """ + z = 0 + z_prev = -float("inf") + k = 0 + while abs(z - z_prev) > tolerance: + z_prev = z + z += 1 / ((k + q) ** x) + k += 1 + return z + + +def _generate_min_degree(gamma, average_degree, max_degree, tolerance, max_iters): + """Returns a minimum degree from the given average degree.""" + # Defines zeta function whether or not Scipy is available + try: + from scipy.special import zeta + except ImportError: + + def zeta(x, q): + return _hurwitz_zeta(x, q, tolerance) + + min_deg_top = max_degree + min_deg_bot = 1 + min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot + itrs = 0 + mid_avg_deg = 0 + while abs(mid_avg_deg - average_degree) > tolerance: + if itrs > max_iters: + raise nx.ExceededMaxIterations("Could not match average_degree") + mid_avg_deg = 0 + for x in range(int(min_deg_mid), max_degree + 1): + mid_avg_deg += (x ** (-gamma + 1)) / zeta(gamma, min_deg_mid) + if mid_avg_deg > average_degree: + min_deg_top = min_deg_mid + min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot + else: + min_deg_bot = min_deg_mid + min_deg_mid = (min_deg_top - min_deg_bot) / 2 + min_deg_bot + itrs += 1 + # return int(min_deg_mid + 0.5) + return round(min_deg_mid) + + +def _generate_communities(degree_seq, community_sizes, mu, max_iters, seed): + """Returns a list of sets, each of which represents a community. + + ``degree_seq`` is the degree sequence that must be met by the + graph. + + ``community_sizes`` is the community size distribution that must be + met by the generated list of sets. + + ``mu`` is a float in the interval [0, 1] indicating the fraction of + intra-community edges incident to each node. + + ``max_iters`` is the number of times to try to add a node to a + community. This must be greater than the length of + ``degree_seq``, otherwise this function will always fail. If + the number of iterations exceeds this value, + :exc:`~networkx.exception.ExceededMaxIterations` is raised. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + The communities returned by this are sets of integers in the set {0, + ..., *n* - 1}, where *n* is the length of ``degree_seq``. + + """ + # This assumes the nodes in the graph will be natural numbers. + result = [set() for _ in community_sizes] + n = len(degree_seq) + free = list(range(n)) + for i in range(max_iters): + v = free.pop() + c = seed.choice(range(len(community_sizes))) + # s = int(degree_seq[v] * (1 - mu) + 0.5) + s = round(degree_seq[v] * (1 - mu)) + # If the community is large enough, add the node to the chosen + # community. Otherwise, return it to the list of unaffiliated + # nodes. + if s < community_sizes[c]: + result[c].add(v) + else: + free.append(v) + # If the community is too big, remove a node from it. + if len(result[c]) > community_sizes[c]: + free.append(result[c].pop()) + if not free: + return result + msg = "Could not assign communities; try increasing min_community" + raise nx.ExceededMaxIterations(msg) + + +@py_random_state(11) +@nx._dispatchable(graphs=None, returns_graph=True) +def LFR_benchmark_graph( + n, + tau1, + tau2, + mu, + average_degree=None, + min_degree=None, + max_degree=None, + min_community=None, + max_community=None, + tol=1.0e-7, + max_iters=500, + seed=None, +): + r"""Returns the LFR benchmark graph. + + This algorithm proceeds as follows: + + 1) Find a degree sequence with a power law distribution, and minimum + value ``min_degree``, which has approximate average degree + ``average_degree``. This is accomplished by either + + a) specifying ``min_degree`` and not ``average_degree``, + b) specifying ``average_degree`` and not ``min_degree``, in which + case a suitable minimum degree will be found. + + ``max_degree`` can also be specified, otherwise it will be set to + ``n``. Each node *u* will have $\mu \mathrm{deg}(u)$ edges + joining it to nodes in communities other than its own and $(1 - + \mu) \mathrm{deg}(u)$ edges joining it to nodes in its own + community. + 2) Generate community sizes according to a power law distribution + with exponent ``tau2``. If ``min_community`` and + ``max_community`` are not specified they will be selected to be + ``min_degree`` and ``max_degree``, respectively. Community sizes + are generated until the sum of their sizes equals ``n``. + 3) Each node will be randomly assigned a community with the + condition that the community is large enough for the node's + intra-community degree, $(1 - \mu) \mathrm{deg}(u)$ as + described in step 2. If a community grows too large, a random node + will be selected for reassignment to a new community, until all + nodes have been assigned a community. + 4) Each node *u* then adds $(1 - \mu) \mathrm{deg}(u)$ + intra-community edges and $\mu \mathrm{deg}(u)$ inter-community + edges. + + Parameters + ---------- + n : int + Number of nodes in the created graph. + + tau1 : float + Power law exponent for the degree distribution of the created + graph. This value must be strictly greater than one. + + tau2 : float + Power law exponent for the community size distribution in the + created graph. This value must be strictly greater than one. + + mu : float + Fraction of inter-community edges incident to each node. This + value must be in the interval [0, 1]. + + average_degree : float + Desired average degree of nodes in the created graph. This value + must be in the interval [0, *n*]. Exactly one of this and + ``min_degree`` must be specified, otherwise a + :exc:`NetworkXError` is raised. + + min_degree : int + Minimum degree of nodes in the created graph. This value must be + in the interval [0, *n*]. Exactly one of this and + ``average_degree`` must be specified, otherwise a + :exc:`NetworkXError` is raised. + + max_degree : int + Maximum degree of nodes in the created graph. If not specified, + this is set to ``n``, the total number of nodes in the graph. + + min_community : int + Minimum size of communities in the graph. If not specified, this + is set to ``min_degree``. + + max_community : int + Maximum size of communities in the graph. If not specified, this + is set to ``n``, the total number of nodes in the graph. + + tol : float + Tolerance when comparing floats, specifically when comparing + average degree values. + + max_iters : int + Maximum number of iterations to try to create the community sizes, + degree distribution, and community affiliations. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : NetworkX graph + The LFR benchmark graph generated according to the specified + parameters. + + Each node in the graph has a node attribute ``'community'`` that + stores the community (that is, the set of nodes) that includes + it. + + Raises + ------ + NetworkXError + If any of the parameters do not meet their upper and lower bounds: + + - ``tau1`` and ``tau2`` must be strictly greater than 1. + - ``mu`` must be in [0, 1]. + - ``max_degree`` must be in {1, ..., *n*}. + - ``min_community`` and ``max_community`` must be in {0, ..., + *n*}. + + If not exactly one of ``average_degree`` and ``min_degree`` is + specified. + + If ``min_degree`` is not specified and a suitable ``min_degree`` + cannot be found. + + ExceededMaxIterations + If a valid degree sequence cannot be created within + ``max_iters`` number of iterations. + + If a valid set of community sizes cannot be created within + ``max_iters`` number of iterations. + + If a valid community assignment cannot be created within ``10 * + n * max_iters`` number of iterations. + + Examples + -------- + Basic usage:: + + >>> from networkx.generators.community import LFR_benchmark_graph + >>> n = 250 + >>> tau1 = 3 + >>> tau2 = 1.5 + >>> mu = 0.1 + >>> G = LFR_benchmark_graph( + ... n, tau1, tau2, mu, average_degree=5, min_community=20, seed=10 + ... ) + + Continuing the example above, you can get the communities from the + node attributes of the graph:: + + >>> communities = {frozenset(G.nodes[v]["community"]) for v in G} + + Notes + ----- + This algorithm differs slightly from the original way it was + presented in [1]. + + 1) Rather than connecting the graph via a configuration model then + rewiring to match the intra-community and inter-community + degrees, we do this wiring explicitly at the end, which should be + equivalent. + 2) The code posted on the author's website [2] calculates the random + power law distributed variables and their average using + continuous approximations, whereas we use the discrete + distributions here as both degree and community size are + discrete. + + Though the authors describe the algorithm as quite robust, testing + during development indicates that a somewhat narrower parameter set + is likely to successfully produce a graph. Some suggestions have + been provided in the event of exceptions. + + References + ---------- + .. [1] "Benchmark graphs for testing community detection algorithms", + Andrea Lancichinetti, Santo Fortunato, and Filippo Radicchi, + Phys. Rev. E 78, 046110 2008 + .. [2] https://www.santofortunato.net/resources + + """ + # Perform some basic parameter validation. + if not tau1 > 1: + raise nx.NetworkXError("tau1 must be greater than one") + if not tau2 > 1: + raise nx.NetworkXError("tau2 must be greater than one") + if not 0 <= mu <= 1: + raise nx.NetworkXError("mu must be in the interval [0, 1]") + + # Validate parameters for generating the degree sequence. + if max_degree is None: + max_degree = n + elif not 0 < max_degree <= n: + raise nx.NetworkXError("max_degree must be in the interval (0, n]") + if not ((min_degree is None) ^ (average_degree is None)): + raise nx.NetworkXError( + "Must assign exactly one of min_degree and average_degree" + ) + if min_degree is None: + min_degree = _generate_min_degree( + tau1, average_degree, max_degree, tol, max_iters + ) + + # Generate a degree sequence with a power law distribution. + low, high = min_degree, max_degree + + def condition(seq): + return sum(seq) % 2 == 0 + + def length(seq): + return len(seq) >= n + + deg_seq = _powerlaw_sequence(tau1, low, high, condition, length, max_iters, seed) + + # Validate parameters for generating the community size sequence. + if min_community is None: + min_community = min(deg_seq) + if max_community is None: + max_community = max(deg_seq) + + # Generate a community size sequence with a power law distribution. + # + # TODO The original code incremented the number of iterations each + # time a new Zipf random value was drawn from the distribution. This + # differed from the way the number of iterations was incremented in + # `_powerlaw_degree_sequence`, so this code was changed to match + # that one. As a result, this code is allowed many more chances to + # generate a valid community size sequence. + low, high = min_community, max_community + + def condition(seq): + return sum(seq) == n + + def length(seq): + return sum(seq) >= n + + comms = _powerlaw_sequence(tau2, low, high, condition, length, max_iters, seed) + + # Generate the communities based on the given degree sequence and + # community sizes. + max_iters *= 10 * n + communities = _generate_communities(deg_seq, comms, mu, max_iters, seed) + + # Finally, generate the benchmark graph based on the given + # communities, joining nodes according to the intra- and + # inter-community degrees. + G = nx.Graph() + G.add_nodes_from(range(n)) + for c in communities: + for u in c: + while G.degree(u) < round(deg_seq[u] * (1 - mu)): + v = seed.choice(list(c)) + G.add_edge(u, v) + while G.degree(u) < deg_seq[u]: + v = seed.choice(range(n)) + if v not in c: + G.add_edge(u, v) + G.nodes[u]["community"] = c + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/degree_seq.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/degree_seq.py new file mode 100644 index 0000000000000000000000000000000000000000..2a374f47c9932fda1757163dbf8868d8c343edb6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/degree_seq.py @@ -0,0 +1,886 @@ +"""Generate graphs with a given degree sequence or expected degree sequence.""" + +import heapq +import math +from itertools import chain, combinations, zip_longest +from operator import itemgetter + +import networkx as nx +from networkx.utils import py_random_state, random_weighted_sample + +__all__ = [ + "configuration_model", + "directed_configuration_model", + "expected_degree_graph", + "havel_hakimi_graph", + "directed_havel_hakimi_graph", + "degree_sequence_tree", + "random_degree_sequence_graph", +] + +chaini = chain.from_iterable + + +def _to_stublist(degree_sequence): + """Returns a list of degree-repeated node numbers. + + ``degree_sequence`` is a list of nonnegative integers representing + the degrees of nodes in a graph. + + This function returns a list of node numbers with multiplicities + according to the given degree sequence. For example, if the first + element of ``degree_sequence`` is ``3``, then the first node number, + ``0``, will appear at the head of the returned list three times. The + node numbers are assumed to be the numbers zero through + ``len(degree_sequence) - 1``. + + Examples + -------- + + >>> degree_sequence = [1, 2, 3] + >>> _to_stublist(degree_sequence) + [0, 1, 1, 2, 2, 2] + + If a zero appears in the sequence, that means the node exists but + has degree zero, so that number will be skipped in the returned + list:: + + >>> degree_sequence = [2, 0, 1] + >>> _to_stublist(degree_sequence) + [0, 0, 2] + + """ + return list(chaini([n] * d for n, d in enumerate(degree_sequence))) + + +def _configuration_model( + deg_sequence, create_using, directed=False, in_deg_sequence=None, seed=None +): + """Helper function for generating either undirected or directed + configuration model graphs. + + ``deg_sequence`` is a list of nonnegative integers representing the + degree of the node whose label is the index of the list element. + + ``create_using`` see :func:`~networkx.empty_graph`. + + ``directed`` and ``in_deg_sequence`` are required if you want the + returned graph to be generated using the directed configuration + model algorithm. If ``directed`` is ``False``, then ``deg_sequence`` + is interpreted as the degree sequence of an undirected graph and + ``in_deg_sequence`` is ignored. Otherwise, if ``directed`` is + ``True``, then ``deg_sequence`` is interpreted as the out-degree + sequence and ``in_deg_sequence`` as the in-degree sequence of a + directed graph. + + .. note:: + + ``deg_sequence`` and ``in_deg_sequence`` need not be the same + length. + + ``seed`` is a random.Random or numpy.random.RandomState instance + + This function returns a graph, directed if and only if ``directed`` + is ``True``, generated according to the configuration model + algorithm. For more information on the algorithm, see the + :func:`configuration_model` or :func:`directed_configuration_model` + functions. + + """ + n = len(deg_sequence) + G = nx.empty_graph(n, create_using) + # If empty, return the null graph immediately. + if n == 0: + return G + # Build a list of available degree-repeated nodes. For example, + # for degree sequence [3, 2, 1, 1, 1], the "stub list" is + # initially [0, 0, 0, 1, 1, 2, 3, 4], that is, node 0 has degree + # 3 and thus is repeated 3 times, etc. + # + # Also, shuffle the stub list in order to get a random sequence of + # node pairs. + if directed: + pairs = zip_longest(deg_sequence, in_deg_sequence, fillvalue=0) + # Unzip the list of pairs into a pair of lists. + out_deg, in_deg = zip(*pairs) + + out_stublist = _to_stublist(out_deg) + in_stublist = _to_stublist(in_deg) + + seed.shuffle(out_stublist) + seed.shuffle(in_stublist) + else: + stublist = _to_stublist(deg_sequence) + # Choose a random balanced bipartition of the stublist, which + # gives a random pairing of nodes. In this implementation, we + # shuffle the list and then split it in half. + n = len(stublist) + half = n // 2 + seed.shuffle(stublist) + out_stublist, in_stublist = stublist[:half], stublist[half:] + G.add_edges_from(zip(out_stublist, in_stublist)) + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def configuration_model(deg_sequence, create_using=None, seed=None): + """Returns a random graph with the given degree sequence. + + The configuration model generates a random pseudograph (graph with + parallel edges and self loops) by randomly assigning edges to + match the given degree sequence. + + Parameters + ---------- + deg_sequence : list of nonnegative integers + Each list entry corresponds to the degree of a node. + create_using : NetworkX graph constructor, optional (default MultiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : MultiGraph + A graph with the specified degree sequence. + Nodes are labeled starting at 0 with an index + corresponding to the position in deg_sequence. + + Raises + ------ + NetworkXError + If the degree sequence does not have an even sum. + + See Also + -------- + is_graphical + + Notes + ----- + As described by Newman [1]_. + + A non-graphical degree sequence (not realizable by some simple + graph) is allowed since this function returns graphs with self + loops and parallel edges. An exception is raised if the degree + sequence does not have an even sum. + + This configuration model construction process can lead to + duplicate edges and loops. You can remove the self-loops and + parallel edges (see below) which will likely result in a graph + that doesn't have the exact degree sequence specified. + + The density of self-loops and parallel edges tends to decrease as + the number of nodes increases. However, typically the number of + self-loops will approach a Poisson distribution with a nonzero mean, + and similarly for the number of parallel edges. Consider a node + with *k* stubs. The probability of being joined to another stub of + the same node is basically (*k* - *1*) / *N*, where *k* is the + degree and *N* is the number of nodes. So the probability of a + self-loop scales like *c* / *N* for some constant *c*. As *N* grows, + this means we expect *c* self-loops. Similarly for parallel edges. + + References + ---------- + .. [1] M.E.J. Newman, "The structure and function of complex networks", + SIAM REVIEW 45-2, pp 167-256, 2003. + + Examples + -------- + You can create a degree sequence following a particular distribution + by using the one of the distribution functions in + :mod:`~networkx.utils.random_sequence` (or one of your own). For + example, to create an undirected multigraph on one hundred nodes + with degree sequence chosen from the power law distribution: + + >>> sequence = nx.random_powerlaw_tree_sequence(100, tries=5000) + >>> G = nx.configuration_model(sequence) + >>> len(G) + 100 + >>> actual_degrees = [d for v, d in G.degree()] + >>> actual_degrees == sequence + True + + The returned graph is a multigraph, which may have parallel + edges. To remove any parallel edges from the returned graph: + + >>> G = nx.Graph(G) + + Similarly, to remove self-loops: + + >>> G.remove_edges_from(nx.selfloop_edges(G)) + + """ + if sum(deg_sequence) % 2 != 0: + msg = "Invalid degree sequence: sum of degrees must be even, not odd" + raise nx.NetworkXError(msg) + + G = nx.empty_graph(0, create_using, default=nx.MultiGraph) + if G.is_directed(): + raise nx.NetworkXNotImplemented("not implemented for directed graphs") + + G = _configuration_model(deg_sequence, G, seed=seed) + + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def directed_configuration_model( + in_degree_sequence, out_degree_sequence, create_using=None, seed=None +): + """Returns a directed_random graph with the given degree sequences. + + The configuration model generates a random directed pseudograph + (graph with parallel edges and self loops) by randomly assigning + edges to match the given degree sequences. + + Parameters + ---------- + in_degree_sequence : list of nonnegative integers + Each list entry corresponds to the in-degree of a node. + out_degree_sequence : list of nonnegative integers + Each list entry corresponds to the out-degree of a node. + create_using : NetworkX graph constructor, optional (default MultiDiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : MultiDiGraph + A graph with the specified degree sequences. + Nodes are labeled starting at 0 with an index + corresponding to the position in deg_sequence. + + Raises + ------ + NetworkXError + If the degree sequences do not have the same sum. + + See Also + -------- + configuration_model + + Notes + ----- + Algorithm as described by Newman [1]_. + + A non-graphical degree sequence (not realizable by some simple + graph) is allowed since this function returns graphs with self + loops and parallel edges. An exception is raised if the degree + sequences does not have the same sum. + + This configuration model construction process can lead to + duplicate edges and loops. You can remove the self-loops and + parallel edges (see below) which will likely result in a graph + that doesn't have the exact degree sequence specified. This + "finite-size effect" decreases as the size of the graph increases. + + References + ---------- + .. [1] Newman, M. E. J. and Strogatz, S. H. and Watts, D. J. + Random graphs with arbitrary degree distributions and their applications + Phys. Rev. E, 64, 026118 (2001) + + Examples + -------- + One can modify the in- and out-degree sequences from an existing + directed graph in order to create a new directed graph. For example, + here we modify the directed path graph: + + >>> D = nx.DiGraph([(0, 1), (1, 2), (2, 3)]) + >>> din = list(d for n, d in D.in_degree()) + >>> dout = list(d for n, d in D.out_degree()) + >>> din.append(1) + >>> dout[0] = 2 + >>> # We now expect an edge from node 0 to a new node, node 3. + ... D = nx.directed_configuration_model(din, dout) + + The returned graph is a directed multigraph, which may have parallel + edges. To remove any parallel edges from the returned graph: + + >>> D = nx.DiGraph(D) + + Similarly, to remove self-loops: + + >>> D.remove_edges_from(nx.selfloop_edges(D)) + + """ + if sum(in_degree_sequence) != sum(out_degree_sequence): + msg = "Invalid degree sequences: sequences must have equal sums" + raise nx.NetworkXError(msg) + + if create_using is None: + create_using = nx.MultiDiGraph + + G = _configuration_model( + out_degree_sequence, + create_using, + directed=True, + in_deg_sequence=in_degree_sequence, + seed=seed, + ) + + name = "directed configuration_model {} nodes {} edges" + return G + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def expected_degree_graph(w, seed=None, selfloops=True): + r"""Returns a random graph with given expected degrees. + + Given a sequence of expected degrees $W=(w_0,w_1,\ldots,w_{n-1})$ + of length $n$ this algorithm assigns an edge between node $u$ and + node $v$ with probability + + .. math:: + + p_{uv} = \frac{w_u w_v}{\sum_k w_k} . + + Parameters + ---------- + w : list + The list of expected degrees. + selfloops: bool (default=True) + Set to False to remove the possibility of self-loop edges. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + Graph + + Examples + -------- + >>> z = [10 for i in range(100)] + >>> G = nx.expected_degree_graph(z) + + Notes + ----- + The nodes have integer labels corresponding to index of expected degrees + input sequence. + + The complexity of this algorithm is $\mathcal{O}(n+m)$ where $n$ is the + number of nodes and $m$ is the expected number of edges. + + The model in [1]_ includes the possibility of self-loop edges. + Set selfloops=False to produce a graph without self loops. + + For finite graphs this model doesn't produce exactly the given + expected degree sequence. Instead the expected degrees are as + follows. + + For the case without self loops (selfloops=False), + + .. math:: + + E[deg(u)] = \sum_{v \ne u} p_{uv} + = w_u \left( 1 - \frac{w_u}{\sum_k w_k} \right) . + + + NetworkX uses the standard convention that a self-loop edge counts 2 + in the degree of a node, so with self loops (selfloops=True), + + .. math:: + + E[deg(u)] = \sum_{v \ne u} p_{uv} + 2 p_{uu} + = w_u \left( 1 + \frac{w_u}{\sum_k w_k} \right) . + + References + ---------- + .. [1] Fan Chung and L. Lu, Connected components in random graphs with + given expected degree sequences, Ann. Combinatorics, 6, + pp. 125-145, 2002. + .. [2] Joel Miller and Aric Hagberg, + Efficient generation of networks with given expected degrees, + in Algorithms and Models for the Web-Graph (WAW 2011), + Alan Frieze, Paul Horn, and Paweł Prałat (Eds), LNCS 6732, + pp. 115-126, 2011. + """ + n = len(w) + G = nx.empty_graph(n) + + # If there are no nodes are no edges in the graph, return the empty graph. + if n == 0 or max(w) == 0: + return G + + rho = 1 / sum(w) + # Sort the weights in decreasing order. The original order of the + # weights dictates the order of the (integer) node labels, so we + # need to remember the permutation applied in the sorting. + order = sorted(enumerate(w), key=itemgetter(1), reverse=True) + mapping = {c: u for c, (u, v) in enumerate(order)} + seq = [v for u, v in order] + last = n + if not selfloops: + last -= 1 + for u in range(last): + v = u + if not selfloops: + v += 1 + factor = seq[u] * rho + p = min(seq[v] * factor, 1) + while v < n and p > 0: + if p != 1: + r = seed.random() + v += math.floor(math.log(r, 1 - p)) + if v < n: + q = min(seq[v] * factor, 1) + if seed.random() < q / p: + G.add_edge(mapping[u], mapping[v]) + v += 1 + p = q + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def havel_hakimi_graph(deg_sequence, create_using=None): + """Returns a simple graph with given degree sequence constructed + using the Havel-Hakimi algorithm. + + Parameters + ---------- + deg_sequence: list of integers + Each integer corresponds to the degree of a node (need not be sorted). + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Directed graphs are not allowed. + + Raises + ------ + NetworkXException + For a non-graphical degree sequence (i.e. one + not realizable by some simple graph). + + Notes + ----- + The Havel-Hakimi algorithm constructs a simple graph by + successively connecting the node of highest degree to other nodes + of highest degree, resorting remaining nodes by degree, and + repeating the process. The resulting graph has a high + degree-associativity. Nodes are labeled 1,.., len(deg_sequence), + corresponding to their position in deg_sequence. + + The basic algorithm is from Hakimi [1]_ and was generalized by + Kleitman and Wang [2]_. + + References + ---------- + .. [1] Hakimi S., On Realizability of a Set of Integers as + Degrees of the Vertices of a Linear Graph. I, + Journal of SIAM, 10(3), pp. 496-506 (1962) + .. [2] Kleitman D.J. and Wang D.L. + Algorithms for Constructing Graphs and Digraphs with Given Valences + and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973) + """ + if not nx.is_graphical(deg_sequence): + raise nx.NetworkXError("Invalid degree sequence") + + p = len(deg_sequence) + G = nx.empty_graph(p, create_using) + if G.is_directed(): + raise nx.NetworkXError("Directed graphs are not supported") + num_degs = [[] for i in range(p)] + dmax, dsum, n = 0, 0, 0 + for d in deg_sequence: + # Process only the non-zero integers + if d > 0: + num_degs[d].append(n) + dmax, dsum, n = max(dmax, d), dsum + d, n + 1 + # Return graph if no edges + if n == 0: + return G + + modstubs = [(0, 0)] * (dmax + 1) + # Successively reduce degree sequence by removing the maximum degree + while n > 0: + # Retrieve the maximum degree in the sequence + while len(num_degs[dmax]) == 0: + dmax -= 1 + # If there are not enough stubs to connect to, then the sequence is + # not graphical + if dmax > n - 1: + raise nx.NetworkXError("Non-graphical integer sequence") + + # Remove largest stub in list + source = num_degs[dmax].pop() + n -= 1 + # Reduce the next dmax largest stubs + mslen = 0 + k = dmax + for i in range(dmax): + while len(num_degs[k]) == 0: + k -= 1 + target = num_degs[k].pop() + G.add_edge(source, target) + n -= 1 + if k > 1: + modstubs[mslen] = (k - 1, target) + mslen += 1 + # Add back to the list any nonzero stubs that were removed + for i in range(mslen): + (stubval, stubtarget) = modstubs[i] + num_degs[stubval].append(stubtarget) + n += 1 + + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def directed_havel_hakimi_graph(in_deg_sequence, out_deg_sequence, create_using=None): + """Returns a directed graph with the given degree sequences. + + Parameters + ---------- + in_deg_sequence : list of integers + Each list entry corresponds to the in-degree of a node. + out_deg_sequence : list of integers + Each list entry corresponds to the out-degree of a node. + create_using : NetworkX graph constructor, optional (default DiGraph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : DiGraph + A graph with the specified degree sequences. + Nodes are labeled starting at 0 with an index + corresponding to the position in deg_sequence + + Raises + ------ + NetworkXError + If the degree sequences are not digraphical. + + See Also + -------- + configuration_model + + Notes + ----- + Algorithm as described by Kleitman and Wang [1]_. + + References + ---------- + .. [1] D.J. Kleitman and D.L. Wang + Algorithms for Constructing Graphs and Digraphs with Given Valences + and Factors Discrete Mathematics, 6(1), pp. 79-88 (1973) + """ + in_deg_sequence = nx.utils.make_list_of_ints(in_deg_sequence) + out_deg_sequence = nx.utils.make_list_of_ints(out_deg_sequence) + + # Process the sequences and form two heaps to store degree pairs with + # either zero or nonzero out degrees + sumin, sumout = 0, 0 + nin, nout = len(in_deg_sequence), len(out_deg_sequence) + maxn = max(nin, nout) + G = nx.empty_graph(maxn, create_using, default=nx.DiGraph) + if maxn == 0: + return G + maxin = 0 + stubheap, zeroheap = [], [] + for n in range(maxn): + in_deg, out_deg = 0, 0 + if n < nout: + out_deg = out_deg_sequence[n] + if n < nin: + in_deg = in_deg_sequence[n] + if in_deg < 0 or out_deg < 0: + raise nx.NetworkXError( + "Invalid degree sequences. Sequence values must be positive." + ) + sumin, sumout, maxin = sumin + in_deg, sumout + out_deg, max(maxin, in_deg) + if in_deg > 0: + stubheap.append((-1 * out_deg, -1 * in_deg, n)) + elif out_deg > 0: + zeroheap.append((-1 * out_deg, n)) + if sumin != sumout: + raise nx.NetworkXError( + "Invalid degree sequences. Sequences must have equal sums." + ) + heapq.heapify(stubheap) + heapq.heapify(zeroheap) + + modstubs = [(0, 0, 0)] * (maxin + 1) + # Successively reduce degree sequence by removing the maximum + while stubheap: + # Remove first value in the sequence with a non-zero in degree + (freeout, freein, target) = heapq.heappop(stubheap) + freein *= -1 + if freein > len(stubheap) + len(zeroheap): + raise nx.NetworkXError("Non-digraphical integer sequence") + + # Attach arcs from the nodes with the most stubs + mslen = 0 + for i in range(freein): + if zeroheap and (not stubheap or stubheap[0][0] > zeroheap[0][0]): + (stubout, stubsource) = heapq.heappop(zeroheap) + stubin = 0 + else: + (stubout, stubin, stubsource) = heapq.heappop(stubheap) + if stubout == 0: + raise nx.NetworkXError("Non-digraphical integer sequence") + G.add_edge(stubsource, target) + # Check if source is now totally connected + if stubout + 1 < 0 or stubin < 0: + modstubs[mslen] = (stubout + 1, stubin, stubsource) + mslen += 1 + + # Add the nodes back to the heaps that still have available stubs + for i in range(mslen): + stub = modstubs[i] + if stub[1] < 0: + heapq.heappush(stubheap, stub) + else: + heapq.heappush(zeroheap, (stub[0], stub[2])) + if freeout < 0: + heapq.heappush(zeroheap, (freeout, target)) + + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def degree_sequence_tree(deg_sequence, create_using=None): + """Return a tree with the given degree sequence. + + Two conditions must be met for a degree sequence to be valid for a tree: + + 1. The number of nodes must be one more than the number of edges. + 2. The degree sequence must be trivial or have only strictly positive + node degrees. + + Parameters + ---------- + degree_sequence : iterable + Iterable of node degrees. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + networkx.Graph + A tree with the given degree sequence. + + Raises + ------ + NetworkXError + If the degree sequence is not valid for a tree. + + If `create_using` is directed. + + See Also + -------- + random_degree_sequence_graph + """ + deg_sequence = list(deg_sequence) + valid, reason = nx.utils.is_valid_tree_degree_sequence(deg_sequence) + if not valid: + raise nx.NetworkXError(reason) + + G = nx.empty_graph(0, create_using) + if G.is_directed(): + raise nx.NetworkXError("Directed Graph not supported") + + if deg_sequence == [0]: + G.add_node(0) + return G + + # Sort all degrees greater than 1 in decreasing order. + # + # TODO Does this need to be sorted in reverse order? + deg = sorted((s for s in deg_sequence if s > 1), reverse=True) + + # make path graph as backbone + n = len(deg) + 2 + nx.add_path(G, range(n)) + last = n + + # add the leaves + for source in range(1, n - 1): + nedges = deg.pop() - 2 + G.add_edges_from((source, target) for target in range(last, last + nedges)) + last += nedges + return G + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_degree_sequence_graph(sequence, seed=None, tries=10): + r"""Returns a simple random graph with the given degree sequence. + + If the maximum degree $d_m$ in the sequence is $O(m^{1/4})$ then the + algorithm produces almost uniform random graphs in $O(m d_m)$ time + where $m$ is the number of edges. + + Parameters + ---------- + sequence : list of integers + Sequence of degrees + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + tries : int, optional + Maximum number of tries to create a graph + + Returns + ------- + G : Graph + A graph with the specified degree sequence. + Nodes are labeled starting at 0 with an index + corresponding to the position in the sequence. + + Raises + ------ + NetworkXUnfeasible + If the degree sequence is not graphical. + NetworkXError + If a graph is not produced in specified number of tries + + See Also + -------- + is_graphical, configuration_model + + Notes + ----- + The generator algorithm [1]_ is not guaranteed to produce a graph. + + References + ---------- + .. [1] Moshen Bayati, Jeong Han Kim, and Amin Saberi, + A sequential algorithm for generating random graphs. + Algorithmica, Volume 58, Number 4, 860-910, + DOI: 10.1007/s00453-009-9340-1 + + Examples + -------- + >>> sequence = [1, 2, 2, 3] + >>> G = nx.random_degree_sequence_graph(sequence, seed=42) + >>> sorted(d for n, d in G.degree()) + [1, 2, 2, 3] + """ + DSRG = DegreeSequenceRandomGraph(sequence, seed) + for try_n in range(tries): + try: + return DSRG.generate() + except nx.NetworkXUnfeasible: + pass + raise nx.NetworkXError(f"failed to generate graph in {tries} tries") + + +class DegreeSequenceRandomGraph: + # class to generate random graphs with a given degree sequence + # use random_degree_sequence_graph() + def __init__(self, degree, rng): + self.rng = rng + self.degree = list(degree) + if not nx.is_graphical(self.degree): + raise nx.NetworkXUnfeasible("degree sequence is not graphical") + # node labels are integers 0,...,n-1 + self.m = sum(self.degree) / 2.0 # number of edges + try: + self.dmax = max(self.degree) # maximum degree + except ValueError: + self.dmax = 0 + + def generate(self): + # remaining_degree is mapping from int->remaining degree + self.remaining_degree = dict(enumerate(self.degree)) + # add all nodes to make sure we get isolated nodes + self.graph = nx.Graph() + self.graph.add_nodes_from(self.remaining_degree) + # remove zero degree nodes + for n, d in list(self.remaining_degree.items()): + if d == 0: + del self.remaining_degree[n] + if len(self.remaining_degree) > 0: + # build graph in three phases according to how many unmatched edges + self.phase1() + self.phase2() + self.phase3() + return self.graph + + def update_remaining(self, u, v, aux_graph=None): + # decrement remaining nodes, modify auxiliary graph if in phase3 + if aux_graph is not None: + # remove edges from auxiliary graph + aux_graph.remove_edge(u, v) + if self.remaining_degree[u] == 1: + del self.remaining_degree[u] + if aux_graph is not None: + aux_graph.remove_node(u) + else: + self.remaining_degree[u] -= 1 + if self.remaining_degree[v] == 1: + del self.remaining_degree[v] + if aux_graph is not None: + aux_graph.remove_node(v) + else: + self.remaining_degree[v] -= 1 + + def p(self, u, v): + # degree probability + return 1 - self.degree[u] * self.degree[v] / (4.0 * self.m) + + def q(self, u, v): + # remaining degree probability + norm = max(self.remaining_degree.values()) ** 2 + return self.remaining_degree[u] * self.remaining_degree[v] / norm + + def suitable_edge(self): + """Returns True if and only if an arbitrary remaining node can + potentially be joined with some other remaining node. + + """ + nodes = iter(self.remaining_degree) + u = next(nodes) + return any(v not in self.graph[u] for v in nodes) + + def phase1(self): + # choose node pairs from (degree) weighted distribution + rem_deg = self.remaining_degree + while sum(rem_deg.values()) >= 2 * self.dmax**2: + u, v = sorted(random_weighted_sample(rem_deg, 2, self.rng)) + if self.graph.has_edge(u, v): + continue + if self.rng.random() < self.p(u, v): # accept edge + self.graph.add_edge(u, v) + self.update_remaining(u, v) + + def phase2(self): + # choose remaining nodes uniformly at random and use rejection sampling + remaining_deg = self.remaining_degree + rng = self.rng + while len(remaining_deg) >= 2 * self.dmax: + while True: + u, v = sorted(rng.sample(list(remaining_deg.keys()), 2)) + if self.graph.has_edge(u, v): + continue + if rng.random() < self.q(u, v): + break + if rng.random() < self.p(u, v): # accept edge + self.graph.add_edge(u, v) + self.update_remaining(u, v) + + def phase3(self): + # build potential remaining edges and choose with rejection sampling + potential_edges = combinations(self.remaining_degree, 2) + # build auxiliary graph of potential edges not already in graph + H = nx.Graph( + [(u, v) for (u, v) in potential_edges if not self.graph.has_edge(u, v)] + ) + rng = self.rng + while self.remaining_degree: + if not self.suitable_edge(): + raise nx.NetworkXUnfeasible("no suitable edges left") + while True: + u, v = sorted(rng.choice(list(H.edges()))) + if rng.random() < self.q(u, v): + break + if rng.random() < self.p(u, v): # accept edge + self.graph.add_edge(u, v) + self.update_remaining(u, v, aux_graph=H) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/directed.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/directed.py new file mode 100644 index 0000000000000000000000000000000000000000..759ce2f9d9106d8dbb6c5ca001391c7be74b636a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/directed.py @@ -0,0 +1,572 @@ +""" +Generators for some directed graphs, including growing network (GN) graphs and +scale-free graphs. + +""" + +import numbers +from collections import Counter + +import networkx as nx +from networkx.generators.classic import empty_graph +from networkx.utils import ( + discrete_sequence, + np_random_state, + py_random_state, + weighted_choice, +) + +__all__ = [ + "gn_graph", + "gnc_graph", + "gnr_graph", + "random_k_out_graph", + "scale_free_graph", +] + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def gn_graph(n, kernel=None, create_using=None, seed=None): + """Returns the growing network (GN) digraph with `n` nodes. + + The GN graph is built by adding nodes one at a time with a link to one + previously added node. The target node for the link is chosen with + probability based on degree. The default attachment kernel is a linear + function of the degree of a node. + + The graph is always a (directed) tree. + + Parameters + ---------- + n : int + The number of nodes for the generated graph. + kernel : function + The attachment kernel. + create_using : NetworkX graph constructor, optional (default DiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Examples + -------- + To create the undirected GN graph, use the :meth:`~DiGraph.to_directed` + method:: + + >>> D = nx.gn_graph(10) # the GN graph + >>> G = D.to_undirected() # the undirected version + + To specify an attachment kernel, use the `kernel` keyword argument:: + + >>> D = nx.gn_graph(10, kernel=lambda x: x**1.5) # A_k = k^1.5 + + References + ---------- + .. [1] P. L. Krapivsky and S. Redner, + Organization of Growing Random Networks, + Phys. Rev. E, 63, 066123, 2001. + """ + G = empty_graph(1, create_using, default=nx.DiGraph) + if not G.is_directed(): + raise nx.NetworkXError("create_using must indicate a Directed Graph") + + if kernel is None: + + def kernel(x): + return x + + if n == 1: + return G + + G.add_edge(1, 0) # get started + ds = [1, 1] # degree sequence + + for source in range(2, n): + # compute distribution from kernel and degree + dist = [kernel(d) for d in ds] + # choose target from discrete distribution + target = discrete_sequence(1, distribution=dist, seed=seed)[0] + G.add_edge(source, target) + ds.append(1) # the source has only one link (degree one) + ds[target] += 1 # add one to the target link degree + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def gnr_graph(n, p, create_using=None, seed=None): + """Returns the growing network with redirection (GNR) digraph with `n` + nodes and redirection probability `p`. + + The GNR graph is built by adding nodes one at a time with a link to one + previously added node. The previous target node is chosen uniformly at + random. With probability `p` the link is instead "redirected" to the + successor node of the target. + + The graph is always a (directed) tree. + + Parameters + ---------- + n : int + The number of nodes for the generated graph. + p : float + The redirection probability. + create_using : NetworkX graph constructor, optional (default DiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Examples + -------- + To create the undirected GNR graph, use the :meth:`~DiGraph.to_directed` + method:: + + >>> D = nx.gnr_graph(10, 0.5) # the GNR graph + >>> G = D.to_undirected() # the undirected version + + References + ---------- + .. [1] P. L. Krapivsky and S. Redner, + Organization of Growing Random Networks, + Phys. Rev. E, 63, 066123, 2001. + """ + G = empty_graph(1, create_using, default=nx.DiGraph) + if not G.is_directed(): + raise nx.NetworkXError("create_using must indicate a Directed Graph") + + if n == 1: + return G + + for source in range(1, n): + target = seed.randrange(0, source) + if seed.random() < p and target != 0: + target = next(G.successors(target)) + G.add_edge(source, target) + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def gnc_graph(n, create_using=None, seed=None): + """Returns the growing network with copying (GNC) digraph with `n` nodes. + + The GNC graph is built by adding nodes one at a time with a link to one + previously added node (chosen uniformly at random) and to all of that + node's successors. + + Parameters + ---------- + n : int + The number of nodes for the generated graph. + create_using : NetworkX graph constructor, optional (default DiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + References + ---------- + .. [1] P. L. Krapivsky and S. Redner, + Network Growth by Copying, + Phys. Rev. E, 71, 036118, 2005k.}, + """ + G = empty_graph(1, create_using, default=nx.DiGraph) + if not G.is_directed(): + raise nx.NetworkXError("create_using must indicate a Directed Graph") + + if n == 1: + return G + + for source in range(1, n): + target = seed.randrange(0, source) + for succ in G.successors(target): + G.add_edge(source, succ) + G.add_edge(source, target) + return G + + +@py_random_state(6) +@nx._dispatchable(graphs=None, returns_graph=True) +def scale_free_graph( + n, + alpha=0.41, + beta=0.54, + gamma=0.05, + delta_in=0.2, + delta_out=0, + seed=None, + initial_graph=None, +): + """Returns a scale-free directed graph. + + Parameters + ---------- + n : integer + Number of nodes in graph + alpha : float + Probability for adding a new node connected to an existing node + chosen randomly according to the in-degree distribution. + beta : float + Probability for adding an edge between two existing nodes. + One existing node is chosen randomly according the in-degree + distribution and the other chosen randomly according to the out-degree + distribution. + gamma : float + Probability for adding a new node connected to an existing node + chosen randomly according to the out-degree distribution. + delta_in : float + Bias for choosing nodes from in-degree distribution. + delta_out : float + Bias for choosing nodes from out-degree distribution. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + initial_graph : MultiDiGraph instance, optional + Build the scale-free graph starting from this initial MultiDiGraph, + if provided. + + Returns + ------- + MultiDiGraph + + Examples + -------- + Create a scale-free graph on one hundred nodes:: + + >>> G = nx.scale_free_graph(100) + + Notes + ----- + The sum of `alpha`, `beta`, and `gamma` must be 1. + + References + ---------- + .. [1] B. Bollobás, C. Borgs, J. Chayes, and O. Riordan, + Directed scale-free graphs, + Proceedings of the fourteenth annual ACM-SIAM Symposium on + Discrete Algorithms, 132--139, 2003. + """ + + def _choose_node(candidates, node_list, delta): + if delta > 0: + bias_sum = len(node_list) * delta + p_delta = bias_sum / (bias_sum + len(candidates)) + if seed.random() < p_delta: + return seed.choice(node_list) + return seed.choice(candidates) + + if initial_graph is not None and hasattr(initial_graph, "_adj"): + if not isinstance(initial_graph, nx.MultiDiGraph): + raise nx.NetworkXError("initial_graph must be a MultiDiGraph.") + G = initial_graph + else: + # Start with 3-cycle + G = nx.MultiDiGraph([(0, 1), (1, 2), (2, 0)]) + + if alpha <= 0: + raise ValueError("alpha must be > 0.") + if beta <= 0: + raise ValueError("beta must be > 0.") + if gamma <= 0: + raise ValueError("gamma must be > 0.") + + if abs(alpha + beta + gamma - 1.0) >= 1e-9: + raise ValueError("alpha+beta+gamma must equal 1.") + + if delta_in < 0: + raise ValueError("delta_in must be >= 0.") + + if delta_out < 0: + raise ValueError("delta_out must be >= 0.") + + # pre-populate degree states + vs = sum((count * [idx] for idx, count in G.out_degree()), []) + ws = sum((count * [idx] for idx, count in G.in_degree()), []) + + # pre-populate node state + node_list = list(G.nodes()) + + # see if there already are number-based nodes + numeric_nodes = [n for n in node_list if isinstance(n, numbers.Number)] + if len(numeric_nodes) > 0: + # set cursor for new nodes appropriately + cursor = max(int(n.real) for n in numeric_nodes) + 1 + else: + # or start at zero + cursor = 0 + + while len(G) < n: + r = seed.random() + + # random choice in alpha,beta,gamma ranges + if r < alpha: + # alpha + # add new node v + v = cursor + cursor += 1 + # also add to node state + node_list.append(v) + # choose w according to in-degree and delta_in + w = _choose_node(ws, node_list, delta_in) + + elif r < alpha + beta: + # beta + # choose v according to out-degree and delta_out + v = _choose_node(vs, node_list, delta_out) + # choose w according to in-degree and delta_in + w = _choose_node(ws, node_list, delta_in) + + else: + # gamma + # choose v according to out-degree and delta_out + v = _choose_node(vs, node_list, delta_out) + # add new node w + w = cursor + cursor += 1 + # also add to node state + node_list.append(w) + + # add edge to graph + G.add_edge(v, w) + + # update degree states + vs.append(v) + ws.append(w) + + return G + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_uniform_k_out_graph(n, k, self_loops=True, with_replacement=True, seed=None): + """Returns a random `k`-out graph with uniform attachment. + + A random `k`-out graph with uniform attachment is a multidigraph + generated by the following algorithm. For each node *u*, choose + `k` nodes *v* uniformly at random (with replacement). Add a + directed edge joining *u* to *v*. + + Parameters + ---------- + n : int + The number of nodes in the returned graph. + + k : int + The out-degree of each node in the returned graph. + + self_loops : bool + If True, self-loops are allowed when generating the graph. + + with_replacement : bool + If True, neighbors are chosen with replacement and the + returned graph will be a directed multigraph. Otherwise, + neighbors are chosen without replacement and the returned graph + will be a directed graph. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + NetworkX graph + A `k`-out-regular directed graph generated according to the + above algorithm. It will be a multigraph if and only if + `with_replacement` is True. + + Raises + ------ + ValueError + If `with_replacement` is False and `k` is greater than + `n`. + + See also + -------- + random_k_out_graph + + Notes + ----- + The return digraph or multidigraph may not be strongly connected, or + even weakly connected. + + If `with_replacement` is True, this function is similar to + :func:`random_k_out_graph`, if that function had parameter `alpha` + set to positive infinity. + + """ + if with_replacement: + create_using = nx.MultiDiGraph() + + def sample(v, nodes): + if not self_loops: + nodes = nodes - {v} + return (seed.choice(list(nodes)) for i in range(k)) + + else: + create_using = nx.DiGraph() + + def sample(v, nodes): + if not self_loops: + nodes = nodes - {v} + return seed.sample(list(nodes), k) + + G = nx.empty_graph(n, create_using) + nodes = set(G) + for u in G: + G.add_edges_from((u, v) for v in sample(u, nodes)) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def random_k_out_graph(n, k, alpha, self_loops=True, seed=None): + """Returns a random `k`-out graph with preferential attachment. + + .. versionchanged:: 3.5 + Different implementations will be used based on whether NumPy is + available. See Notes for details. + + A random `k`-out graph with preferential attachment is a + multidigraph generated by the following algorithm. + + 1. Begin with an empty digraph, and initially set each node to have + weight `alpha`. + 2. Choose a node `u` with out-degree less than `k` uniformly at + random. + 3. Choose a node `v` from with probability proportional to its + weight. + 4. Add a directed edge from `u` to `v`, and increase the weight + of `v` by one. + 5. If each node has out-degree `k`, halt, otherwise repeat from + step 2. + + For more information on this model of random graph, see [1]_. + + Parameters + ---------- + n : int + The number of nodes in the returned graph. + + k : int + The out-degree of each node in the returned graph. + + alpha : float + A positive :class:`float` representing the initial weight of + each vertex. A higher number means that in step 3 above, nodes + will be chosen more like a true uniformly random sample, and a + lower number means that nodes are more likely to be chosen as + their in-degree increases. If this parameter is not positive, a + :exc:`ValueError` is raised. + + self_loops : bool + If True, self-loops are allowed when generating the graph. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + :class:`~networkx.classes.MultiDiGraph` + A `k`-out-regular multidigraph generated according to the above + algorithm. + + Raises + ------ + ValueError + If `alpha` is not positive. + + Notes + ----- + The returned multidigraph may not be strongly connected, or even + weakly connected. + + `random_k_out_graph` has two implementations: an array-based formulation that + uses `numpy` (``_random_k_out_graph_numpy``), and a pure-Python + implementation (``_random_k_out_graph_python``). + The NumPy implementation is more performant, especially for large `n`, and is + therefore used by default. If NumPy is not installed in the environment, + then the pure Python implementation is executed. + However, you can explicitly control which implementation is executed by directly + calling the corresponding function:: + + # Use numpy if available, else Python + nx.random_k_out_graph(1000, 5, alpha=1) + + # Use the numpy-based implementation (raises ImportError if numpy not installed) + nx.generators.directed._random_k_out_graph_numpy(1000, 5, alpha=1) + + # Use the Python-based implementation + nx.generators.directed._random_k_out_graph_python(1000, 5, alpha=1) + + References + ---------- + .. [1] Peterson, Nicholas R., and Boris Pittel. + "Distance between two random `k`-out digraphs, with and without preferential attachment." + arXiv preprint arXiv:1311.5961 (2013) . + + """ + if alpha < 0: + raise ValueError("alpha must be positive") + try: # Use numpy if available, otherwise fall back to pure Python implementation + return _random_k_out_graph_numpy(n, k, alpha, self_loops, seed) + except ImportError: + return _random_k_out_graph_python(n, k, alpha, self_loops, seed) + + +@np_random_state(4) +def _random_k_out_graph_numpy(n, k, alpha, self_loops=True, seed=None): + import numpy as np + + G = nx.empty_graph(n, create_using=nx.MultiDiGraph) + nodes = np.arange(n) + remaining_mask = np.full(n, True) + weights = np.full(n, alpha) + total_weight = n * alpha + out_strengths = np.zeros(n) + + for i in range(k * n): + u = seed.choice(nodes[remaining_mask]) + + if self_loops: + v = seed.choice(nodes, p=weights / total_weight) + else: # Ignore weight of u when selecting v + u_weight = weights[u] + weights[u] = 0 + v = seed.choice(nodes, p=weights / (total_weight - u_weight)) + weights[u] = u_weight + + G.add_edge(u.item(), v.item()) + weights[v] += 1 + total_weight += 1 + out_strengths[u] += 1 + if out_strengths[u] == k: + remaining_mask[u] = False + return G + + +@py_random_state(4) +def _random_k_out_graph_python(n, k, alpha, self_loops=True, seed=None): + G = nx.empty_graph(n, create_using=nx.MultiDiGraph) + weights = Counter({v: alpha for v in G}) + out_strengths = Counter({v: 0 for v in G}) + + for i in range(k * n): + u = seed.choice(list(out_strengths.keys())) + # If self-loops are not allowed, make the source node `u` have + # weight zero. + if not self_loops: + uweight = weights.pop(u) + + v = weighted_choice(weights, seed=seed) + + if not self_loops: + weights[u] = uweight + + G.add_edge(u, v) + weights[v] += 1 + out_strengths[u] += 1 + if out_strengths[u] == k: + out_strengths.pop(u) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/duplication.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/duplication.py new file mode 100644 index 0000000000000000000000000000000000000000..3c3ade63f58237eeb927ff631b25f025d7d83fc1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/duplication.py @@ -0,0 +1,174 @@ +"""Functions for generating graphs based on the "duplication" method. + +These graph generators start with a small initial graph then duplicate +nodes and (partially) duplicate their edges. These functions are +generally inspired by biological networks. + +""" + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.utils import py_random_state +from networkx.utils.misc import check_create_using + +__all__ = ["partial_duplication_graph", "duplication_divergence_graph"] + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def partial_duplication_graph(N, n, p, q, seed=None, *, create_using=None): + """Returns a random graph using the partial duplication model. + + Parameters + ---------- + N : int + The total number of nodes in the final graph. + + n : int + The number of nodes in the initial clique. + + p : float + The probability of joining each neighbor of a node to the + duplicate node. Must be a number in the between zero and one, + inclusive. + + q : float + The probability of joining the source node to the duplicate + node. Must be a number in the between zero and one, inclusive. + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + A graph of nodes is grown by creating a fully connected graph + of size `n`. The following procedure is then repeated until + a total of `N` nodes have been reached. + + 1. A random node, *u*, is picked and a new node, *v*, is created. + 2. For each neighbor of *u* an edge from the neighbor to *v* is created + with probability `p`. + 3. An edge from *u* to *v* is created with probability `q`. + + This algorithm appears in [1]. + + This implementation allows the possibility of generating + disconnected graphs. + + References + ---------- + .. [1] Knudsen Michael, and Carsten Wiuf. "A Markov chain approach to + randomly grown graphs." Journal of Applied Mathematics 2008. + + + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if p < 0 or p > 1 or q < 0 or q > 1: + msg = "partial duplication graph must have 0 <= p, q <= 1." + raise NetworkXError(msg) + if n > N: + raise NetworkXError("partial duplication graph must have n <= N.") + + G = nx.complete_graph(n, create_using) + for new_node in range(n, N): + # Pick a random vertex, u, already in the graph. + src_node = seed.randint(0, new_node - 1) + + # Add a new vertex, v, to the graph. + G.add_node(new_node) + + # For each neighbor of u... + for nbr_node in list(nx.all_neighbors(G, src_node)): + # Add the neighbor to v with probability p. + if seed.random() < p: + G.add_edge(new_node, nbr_node) + + # Join v and u with probability q. + if seed.random() < q: + G.add_edge(new_node, src_node) + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def duplication_divergence_graph(n, p, seed=None, *, create_using=None): + """Returns an undirected graph using the duplication-divergence model. + + A graph of `n` nodes is created by duplicating the initial nodes + and retaining edges incident to the original nodes with a retention + probability `p`. + + Parameters + ---------- + n : int + The desired number of nodes in the graph. + p : float + The probability for retaining the edge of the replicated node. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If `p` is not a valid probability. + If `n` is less than 2. + + Notes + ----- + This algorithm appears in [1]. + + This implementation disallows the possibility of generating + disconnected graphs. + + References + ---------- + .. [1] I. Ispolatov, P. L. Krapivsky, A. Yuryev, + "Duplication-divergence model of protein interaction network", + Phys. Rev. E, 71, 061911, 2005. + + """ + if p > 1 or p < 0: + msg = f"NetworkXError p={p} is not in [0,1]." + raise nx.NetworkXError(msg) + if n < 2: + msg = "n must be greater than or equal to 2" + raise nx.NetworkXError(msg) + + create_using = check_create_using(create_using, directed=False, multigraph=False) + G = nx.empty_graph(create_using=create_using) + + # Initialize the graph with two connected nodes. + G.add_edge(0, 1) + i = 2 + while i < n: + # Choose a random node from current graph to duplicate. + random_node = seed.choice(list(G)) + # Make the replica. + G.add_node(i) + # flag indicates whether at least one edge is connected on the replica. + flag = False + for nbr in G.neighbors(random_node): + if seed.random() < p: + # Link retention step. + G.add_edge(i, nbr) + flag = True + if not flag: + # Delete replica if no edges retained. + G.remove_node(i) + else: + # Successful duplication. + i += 1 + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/ego.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/ego.py new file mode 100644 index 0000000000000000000000000000000000000000..91ff3e3fe8573e0764094d030a8656fafa59c929 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/ego.py @@ -0,0 +1,66 @@ +""" +Ego graph. +""" + +__all__ = ["ego_graph"] + +import networkx as nx + + +@nx._dispatchable(preserve_all_attrs=True, returns_graph=True) +def ego_graph(G, n, radius=1, center=True, undirected=False, distance=None): + """Returns induced subgraph of neighbors centered at node n within + a given radius. + + Parameters + ---------- + G : graph + A NetworkX Graph or DiGraph + + n : node + A single node + + radius : number, optional + Include all neighbors of distance<=radius from n. + + center : bool, optional + If False, do not include center node in graph + + undirected : bool, optional + If True use both in- and out-neighbors of directed graphs. + + distance : key, optional + Use specified edge data key as distance. For example, setting + distance='weight' will use the edge weight to measure the + distance from the node n. + + Notes + ----- + For directed graphs D this produces the "out" neighborhood + or successors. If you want the neighborhood of predecessors + first reverse the graph with D.reverse(). If you want both + directions use the keyword argument undirected=True. + + Node, edge, and graph attributes are copied to the returned subgraph. + """ + if undirected: + if distance is not None: + sp, _ = nx.single_source_dijkstra( + G.to_undirected(), n, cutoff=radius, weight=distance + ) + else: + sp = dict( + nx.single_source_shortest_path_length( + G.to_undirected(), n, cutoff=radius + ) + ) + else: + if distance is not None: + sp, _ = nx.single_source_dijkstra(G, n, cutoff=radius, weight=distance) + else: + sp = nx.single_source_shortest_path_length(G, n, cutoff=radius) + + H = G.subgraph(sp).copy() + if not center: + H.remove_node(n) + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/expanders.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/expanders.py new file mode 100644 index 0000000000000000000000000000000000000000..a7d6c21f9452f1e8d584864a010ab724cac2f048 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/expanders.py @@ -0,0 +1,499 @@ +"""Provides explicit constructions of expander graphs.""" + +import itertools + +import networkx as nx + +__all__ = [ + "margulis_gabber_galil_graph", + "chordal_cycle_graph", + "paley_graph", + "maybe_regular_expander", + "maybe_regular_expander_graph", + "is_regular_expander", + "random_regular_expander_graph", +] + + +# Other discrete torus expanders can be constructed by using the following edge +# sets. For more information, see Chapter 4, "Expander Graphs", in +# "Pseudorandomness", by Salil Vadhan. +# +# For a directed expander, add edges from (x, y) to: +# +# (x, y), +# ((x + 1) % n, y), +# (x, (y + 1) % n), +# (x, (x + y) % n), +# (-y % n, x) +# +# For an undirected expander, add the reverse edges. +# +# Also appearing in the paper of Gabber and Galil: +# +# (x, y), +# (x, (x + y) % n), +# (x, (x + y + 1) % n), +# ((x + y) % n, y), +# ((x + y + 1) % n, y) +# +# and: +# +# (x, y), +# ((x + 2*y) % n, y), +# ((x + (2*y + 1)) % n, y), +# ((x + (2*y + 2)) % n, y), +# (x, (y + 2*x) % n), +# (x, (y + (2*x + 1)) % n), +# (x, (y + (2*x + 2)) % n), +# +@nx._dispatchable(graphs=None, returns_graph=True) +def margulis_gabber_galil_graph(n, create_using=None): + r"""Returns the Margulis-Gabber-Galil undirected MultiGraph on `n^2` nodes. + + The undirected MultiGraph is regular with degree `8`. Nodes are integer + pairs. The second-largest eigenvalue of the adjacency matrix of the graph + is at most `5 \sqrt{2}`, regardless of `n`. + + Parameters + ---------- + n : int + Determines the number of nodes in the graph: `n^2`. + create_using : NetworkX graph constructor, optional (default MultiGraph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : graph + The constructed undirected multigraph. + + Raises + ------ + NetworkXError + If the graph is directed or not a multigraph. + + """ + G = nx.empty_graph(0, create_using, default=nx.MultiGraph) + if G.is_directed() or not G.is_multigraph(): + msg = "`create_using` must be an undirected multigraph." + raise nx.NetworkXError(msg) + + for x, y in itertools.product(range(n), repeat=2): + for u, v in ( + ((x + 2 * y) % n, y), + ((x + (2 * y + 1)) % n, y), + (x, (y + 2 * x) % n), + (x, (y + (2 * x + 1)) % n), + ): + G.add_edge((x, y), (u, v)) + G.graph["name"] = f"margulis_gabber_galil_graph({n})" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def chordal_cycle_graph(p, create_using=None): + """Returns the chordal cycle graph on `p` nodes. + + The returned graph is a cycle graph on `p` nodes with chords joining each + vertex `x` to its inverse modulo `p`. This graph is a (mildly explicit) + 3-regular expander [1]_. + + `p` *must* be a prime number. + + Parameters + ---------- + p : a prime number + + The number of vertices in the graph. This also indicates where the + chordal edges in the cycle will be created. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : graph + The constructed undirected multigraph. + + Raises + ------ + NetworkXError + + If `create_using` indicates directed or not a multigraph. + + References + ---------- + + .. [1] Theorem 4.4.2 in A. Lubotzky. "Discrete groups, expanding graphs and + invariant measures", volume 125 of Progress in Mathematics. + Birkhäuser Verlag, Basel, 1994. + + """ + G = nx.empty_graph(0, create_using, default=nx.MultiGraph) + if G.is_directed() or not G.is_multigraph(): + msg = "`create_using` must be an undirected multigraph." + raise nx.NetworkXError(msg) + + for x in range(p): + left = (x - 1) % p + right = (x + 1) % p + # Here we apply Fermat's Little Theorem to compute the multiplicative + # inverse of x in Z/pZ. By Fermat's Little Theorem, + # + # x^p = x (mod p) + # + # Therefore, + # + # x * x^(p - 2) = 1 (mod p) + # + # The number 0 is a special case: we just let its inverse be itself. + chord = pow(x, p - 2, p) if x > 0 else 0 + for y in (left, right, chord): + G.add_edge(x, y) + G.graph["name"] = f"chordal_cycle_graph({p})" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def paley_graph(p, create_using=None): + r"""Returns the Paley $\frac{(p-1)}{2}$ -regular graph on $p$ nodes. + + The returned graph is a graph on $\mathbb{Z}/p\mathbb{Z}$ with edges between $x$ and $y$ + if and only if $x-y$ is a nonzero square in $\mathbb{Z}/p\mathbb{Z}$. + + If $p \equiv 1 \pmod 4$, $-1$ is a square in + $\mathbb{Z}/p\mathbb{Z}$ and therefore $x-y$ is a square if and + only if $y-x$ is also a square, i.e the edges in the Paley graph are symmetric. + + If $p \equiv 3 \pmod 4$, $-1$ is not a square in $\mathbb{Z}/p\mathbb{Z}$ + and therefore either $x-y$ or $y-x$ is a square in $\mathbb{Z}/p\mathbb{Z}$ but not both. + + Note that a more general definition of Paley graphs extends this construction + to graphs over $q=p^n$ vertices, by using the finite field $F_q$ instead of + $\mathbb{Z}/p\mathbb{Z}$. + This construction requires to compute squares in general finite fields and is + not what is implemented here (i.e `paley_graph(25)` does not return the true + Paley graph associated with $5^2$). + + Parameters + ---------- + p : int, an odd prime number. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : graph + The constructed directed graph. + + Raises + ------ + NetworkXError + If the graph is a multigraph. + + References + ---------- + Chapter 13 in B. Bollobas, Random Graphs. Second edition. + Cambridge Studies in Advanced Mathematics, 73. + Cambridge University Press, Cambridge (2001). + """ + G = nx.empty_graph(0, create_using, default=nx.DiGraph) + if G.is_multigraph(): + msg = "`create_using` cannot be a multigraph." + raise nx.NetworkXError(msg) + + # Compute the squares in Z/pZ. + # Make it a set to uniquify (there are exactly (p-1)/2 squares in Z/pZ + # when is prime). + square_set = {(x**2) % p for x in range(1, p) if (x**2) % p != 0} + + for x in range(p): + for x2 in square_set: + G.add_edge(x, (x + x2) % p) + G.graph["name"] = f"paley({p})" + return G + + +@nx.utils.decorators.np_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def maybe_regular_expander_graph(n, d, *, create_using=None, max_tries=100, seed=None): + r"""Utility for creating a random regular expander. + + Returns a random $d$-regular graph on $n$ nodes which is an expander + graph with very good probability. + + Parameters + ---------- + n : int + The number of nodes. + d : int + The degree of each node. + create_using : Graph Instance or Constructor + Indicator of type of graph to return. + If a Graph-type instance, then clear and use it. + If a constructor, call it to create an empty graph. + Use the Graph constructor by default. + max_tries : int. (default: 100) + The number of allowed loops when generating each independent cycle + seed : (default: None) + Seed used to set random number generation state. See :ref`Randomness`. + + Notes + ----- + The nodes are numbered from $0$ to $n - 1$. + + The graph is generated by taking $d / 2$ random independent cycles. + + Joel Friedman proved that in this model the resulting + graph is an expander with probability + $1 - O(n^{-\tau})$ where $\tau = \lceil (\sqrt{d - 1}) / 2 \rceil - 1$. [1]_ + + Examples + -------- + >>> G = nx.maybe_regular_expander_graph(n=200, d=6, seed=8020) + + Returns + ------- + G : graph + The constructed undirected graph. + + Raises + ------ + NetworkXError + If $d % 2 != 0$ as the degree must be even. + If $n - 1$ is less than $ 2d $ as the graph is complete at most. + If max_tries is reached + + See Also + -------- + is_regular_expander + random_regular_expander_graph + + References + ---------- + .. [1] Joel Friedman, + A Proof of Alon's Second Eigenvalue Conjecture and Related Problems, 2004 + https://arxiv.org/abs/cs/0405020 + + """ + + import numpy as np + + if n < 1: + raise nx.NetworkXError("n must be a positive integer") + + if not (d >= 2): + raise nx.NetworkXError("d must be greater than or equal to 2") + + if not (d % 2 == 0): + raise nx.NetworkXError("d must be even") + + if not (n - 1 >= d): + raise nx.NetworkXError( + f"Need n-1>= d to have room for {d // 2} independent cycles with {n} nodes" + ) + + G = nx.empty_graph(n, create_using) + + if n < 2: + return G + + cycles = [] + edges = set() + + # Create d / 2 cycles + for i in range(d // 2): + iterations = max_tries + # Make sure the cycles are independent to have a regular graph + while len(edges) != (i + 1) * n: + iterations -= 1 + # Faster than random.permutation(n) since there are only + # (n-1)! distinct cycles against n! permutations of size n + cycle = seed.permutation(n - 1).tolist() + cycle.append(n - 1) + + new_edges = { + (u, v) + for u, v in nx.utils.pairwise(cycle, cyclic=True) + if (u, v) not in edges and (v, u) not in edges + } + # If the new cycle has no edges in common with previous cycles + # then add it to the list otherwise try again + if len(new_edges) == n: + cycles.append(cycle) + edges.update(new_edges) + + if iterations == 0: + msg = "Too many iterations in maybe_regular_expander_graph" + raise nx.NetworkXError(msg) + + G.add_edges_from(edges) + + return G + + +def maybe_regular_expander(n, d, *, create_using=None, max_tries=100, seed=None): + """ + .. deprecated:: 3.6 + `maybe_regular_expander` is a deprecated alias + for `maybe_regular_expander_graph`. + Use `maybe_regular_expander_graph` instead. + """ + import warnings + + warnings.warn( + "maybe_regular_expander is deprecated, " + "use `maybe_regular_expander_graph` instead.", + category=DeprecationWarning, + stacklevel=2, + ) + return maybe_regular_expander_graph( + n, d, create_using=create_using, max_tries=max_tries, seed=seed + ) + + +@nx.utils.not_implemented_for("directed") +@nx.utils.not_implemented_for("multigraph") +@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}}) +def is_regular_expander(G, *, epsilon=0): + r"""Determines whether the graph G is a regular expander. [1]_ + + An expander graph is a sparse graph with strong connectivity properties. + + More precisely, this helper checks whether the graph is a + regular $(n, d, \lambda)$-expander with $\lambda$ close to + the Alon-Boppana bound and given by + $\lambda = 2 \sqrt{d - 1} + \epsilon$. [2]_ + + In the case where $\epsilon = 0$ then if the graph successfully passes the test + it is a Ramanujan graph. [3]_ + + A Ramanujan graph has spectral gap almost as large as possible, which makes them + excellent expanders. + + Parameters + ---------- + G : NetworkX graph + epsilon : int, float, default=0 + + Returns + ------- + bool + Whether the given graph is a regular $(n, d, \lambda)$-expander + where $\lambda = 2 \sqrt{d - 1} + \epsilon$. + + Examples + -------- + >>> G = nx.random_regular_expander_graph(20, 4) + >>> nx.is_regular_expander(G) + True + + See Also + -------- + maybe_regular_expander_graph + random_regular_expander_graph + + References + ---------- + .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph + .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound + .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph + + """ + + import numpy as np + import scipy as sp + + if epsilon < 0: + raise nx.NetworkXError("epsilon must be non negative") + + if not nx.is_regular(G): + return False + + _, d = nx.utils.arbitrary_element(G.degree) + + A = nx.adjacency_matrix(G, dtype=float) + lams = sp.sparse.linalg.eigsh(A, which="LM", k=2, return_eigenvectors=False) + + # lambda2 is the second biggest eigenvalue + lambda2 = min(lams) + + # Use bool() to convert numpy scalar to Python Boolean + return bool(abs(lambda2) < 2 * np.sqrt(d - 1) + epsilon) + + +@nx.utils.decorators.np_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_regular_expander_graph( + n, d, *, epsilon=0, create_using=None, max_tries=100, seed=None +): + r"""Returns a random regular expander graph on $n$ nodes with degree $d$. + + An expander graph is a sparse graph with strong connectivity properties. [1]_ + + More precisely the returned graph is a $(n, d, \lambda)$-expander with + $\lambda = 2 \sqrt{d - 1} + \epsilon$, close to the Alon-Boppana bound. [2]_ + + In the case where $\epsilon = 0$ it returns a Ramanujan graph. + A Ramanujan graph has spectral gap almost as large as possible, + which makes them excellent expanders. [3]_ + + Parameters + ---------- + n : int + The number of nodes. + d : int + The degree of each node. + epsilon : int, float, default=0 + max_tries : int, (default: 100) + The number of allowed loops, + also used in the `maybe_regular_expander_graph` utility + seed : (default: None) + Seed used to set random number generation state. See :ref`Randomness`. + + Raises + ------ + NetworkXError + If max_tries is reached + + Examples + -------- + >>> G = nx.random_regular_expander_graph(20, 4) + >>> nx.is_regular_expander(G) + True + + Notes + ----- + This loops over `maybe_regular_expander_graph` and can be slow when + $n$ is too big or $\epsilon$ too small. + + See Also + -------- + maybe_regular_expander_graph + is_regular_expander + + References + ---------- + .. [1] Expander graph, https://en.wikipedia.org/wiki/Expander_graph + .. [2] Alon-Boppana bound, https://en.wikipedia.org/wiki/Alon%E2%80%93Boppana_bound + .. [3] Ramanujan graphs, https://en.wikipedia.org/wiki/Ramanujan_graph + + """ + G = maybe_regular_expander_graph( + n, d, create_using=create_using, max_tries=max_tries, seed=seed + ) + iterations = max_tries + + while not is_regular_expander(G, epsilon=epsilon): + iterations -= 1 + G = maybe_regular_expander_graph( + n=n, d=d, create_using=create_using, max_tries=max_tries, seed=seed + ) + + if iterations == 0: + raise nx.NetworkXError( + "Too many iterations in random_regular_expander_graph" + ) + + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/geometric.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/geometric.py new file mode 100644 index 0000000000000000000000000000000000000000..fdd4f627b46a1f0fbd2f6cd1700aecb12549516a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/geometric.py @@ -0,0 +1,1037 @@ +"""Generators for geometric graphs.""" + +import math +from bisect import bisect_left +from itertools import accumulate, combinations, product + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "geometric_edges", + "geographical_threshold_graph", + "navigable_small_world_graph", + "random_geometric_graph", + "soft_random_geometric_graph", + "thresholded_random_geometric_graph", + "waxman_graph", + "geometric_soft_configuration_graph", +] + + +@nx._dispatchable(node_attrs="pos_name") +def geometric_edges(G, radius, p=2, *, pos_name="pos"): + """Returns edge list of node pairs within `radius` of each other. + + Parameters + ---------- + G : networkx graph + The graph from which to generate the edge list. The nodes in `G` should + have an attribute ``pos`` corresponding to the node position, which is + used to compute the distance to other nodes. + radius : scalar + The distance threshold. Edges are included in the edge list if the + distance between the two nodes is less than `radius`. + pos_name : string, default="pos" + The name of the node attribute which represents the position of each + node in 2D coordinates. Every node in the Graph must have this attribute. + p : scalar, default=2 + The `Minkowski distance metric + `_ used to compute + distances. The default value is 2, i.e. Euclidean distance. + + Returns + ------- + edges : list + List of edges whose distances are less than `radius` + + Notes + ----- + Radius uses Minkowski distance metric `p`. + If scipy is available, `scipy.spatial.cKDTree` is used to speed computation. + + Examples + -------- + Create a graph with nodes that have a "pos" attribute representing 2D + coordinates. + + >>> G = nx.Graph() + >>> G.add_nodes_from( + ... [ + ... (0, {"pos": (0, 0)}), + ... (1, {"pos": (3, 0)}), + ... (2, {"pos": (8, 0)}), + ... ] + ... ) + >>> nx.geometric_edges(G, radius=1) + [] + >>> nx.geometric_edges(G, radius=4) + [(0, 1)] + >>> nx.geometric_edges(G, radius=6) + [(0, 1), (1, 2)] + >>> nx.geometric_edges(G, radius=9) + [(0, 1), (0, 2), (1, 2)] + """ + # Input validation - every node must have a "pos" attribute + for n, pos in G.nodes(data=pos_name): + if pos is None: + raise nx.NetworkXError( + f"Node {n} (and all nodes) must have a '{pos_name}' attribute." + ) + + # NOTE: See _geometric_edges for the actual implementation. The reason this + # is split into two functions is to avoid the overhead of input validation + # every time the function is called internally in one of the other + # geometric generators + return _geometric_edges(G, radius, p, pos_name) + + +def _geometric_edges(G, radius, p, pos_name): + """ + Implements `geometric_edges` without input validation. See `geometric_edges` + for complete docstring. + """ + nodes_pos = G.nodes(data=pos_name) + try: + import scipy as sp + except ImportError: + # no scipy KDTree so compute by for-loop + radius_p = radius**p + edges = [ + (u, v) + for (u, pu), (v, pv) in combinations(nodes_pos, 2) + if sum(abs(a - b) ** p for a, b in zip(pu, pv)) <= radius_p + ] + return edges + # scipy KDTree is available + nodes, coords = list(zip(*nodes_pos)) + kdtree = sp.spatial.cKDTree(coords) # Cannot provide generator. + edge_indexes = kdtree.query_pairs(radius, p) + edges = [(nodes[u], nodes[v]) for u, v in sorted(edge_indexes)] + return edges + + +@py_random_state(5) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_geometric_graph( + n, radius, dim=2, pos=None, p=2, seed=None, *, pos_name="pos" +): + """Returns a random geometric graph in the unit cube of dimensions `dim`. + + The random geometric graph model places `n` nodes uniformly at + random in the unit cube. Two nodes are joined by an edge if the + distance between the nodes is at most `radius`. + + Edges are determined using a KDTree when SciPy is available. + This reduces the time complexity from $O(n^2)$ to $O(n)$. + + Parameters + ---------- + n : int or iterable + Number of nodes or iterable of nodes + radius: float + Distance threshold value + dim : int, optional + Dimension of graph + pos : dict, optional + A dictionary keyed by node with node positions as values. + p : float, optional + Which Minkowski distance metric to use. `p` has to meet the condition + ``1 <= p <= infinity``. + + If this argument is not specified, the :math:`L^2` metric + (the Euclidean distance metric), p = 2 is used. + This should not be confused with the `p` of an Erdős-Rényi random + graph, which represents probability. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + pos_name : string, default="pos" + The name of the node attribute which represents the position + in 2D coordinates of the node in the returned graph. + + Returns + ------- + Graph + A random geometric graph, undirected and without self-loops. + Each node has a node attribute ``'pos'`` that stores the + position of that node in Euclidean space as provided by the + ``pos`` keyword argument or, if ``pos`` was not provided, as + generated by this function. + + Examples + -------- + Create a random geometric graph on twenty nodes where nodes are joined by + an edge if their distance is at most 0.1:: + + >>> G = nx.random_geometric_graph(20, 0.1) + + Notes + ----- + This uses a *k*-d tree to build the graph. + + The `pos` keyword argument can be used to specify node positions so you + can create an arbitrary distribution and domain for positions. + + For example, to use a 2D Gaussian distribution of node positions with mean + (0, 0) and standard deviation 2:: + + >>> import random + >>> n = 20 + >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)} + >>> G = nx.random_geometric_graph(n, 0.2, pos=pos) + + References + ---------- + .. [1] Penrose, Mathew, *Random Geometric Graphs*, + Oxford Studies in Probability, 5, 2003. + + """ + # TODO Is this function just a special case of the geographical + # threshold graph? + # + # half_radius = {v: radius / 2 for v in n} + # return geographical_threshold_graph(nodes, theta=1, alpha=1, + # weight=half_radius) + # + G = nx.empty_graph(n) + # If no positions are provided, choose uniformly random vectors in + # Euclidean space of the specified dimension. + if pos is None: + pos = {v: [seed.random() for i in range(dim)] for v in G} + nx.set_node_attributes(G, pos, pos_name) + + G.add_edges_from(_geometric_edges(G, radius, p, pos_name)) + return G + + +@py_random_state(6) +@nx._dispatchable(graphs=None, returns_graph=True) +def soft_random_geometric_graph( + n, radius, dim=2, pos=None, p=2, p_dist=None, seed=None, *, pos_name="pos" +): + r"""Returns a soft random geometric graph in the unit cube. + + The soft random geometric graph [1] model places `n` nodes uniformly at + random in the unit cube in dimension `dim`. Two nodes of distance, `dist`, + computed by the `p`-Minkowski distance metric are joined by an edge with + probability `p_dist` if the computed distance metric value of the nodes + is at most `radius`, otherwise they are not joined. + + Edges within `radius` of each other are determined using a KDTree when + SciPy is available. This reduces the time complexity from :math:`O(n^2)` + to :math:`O(n)`. + + Parameters + ---------- + n : int or iterable + Number of nodes or iterable of nodes + radius: float + Distance threshold value + dim : int, optional + Dimension of graph + pos : dict, optional + A dictionary keyed by node with node positions as values. + p : float, optional + Which Minkowski distance metric to use. + `p` has to meet the condition ``1 <= p <= infinity``. + + If this argument is not specified, the :math:`L^2` metric + (the Euclidean distance metric), p = 2 is used. + + This should not be confused with the `p` of an Erdős-Rényi random + graph, which represents probability. + p_dist : function, optional + A probability density function computing the probability of + connecting two nodes that are of distance, dist, computed by the + Minkowski distance metric. The probability density function, `p_dist`, + must be any function that takes the metric value as input + and outputs a single probability value between 0-1. The `scipy.stats` + package has many probability distribution functions implemented and + tools for custom probability distribution definitions [2], and passing + the .pdf method of `scipy.stats` distributions can be used here. If the + probability function, `p_dist`, is not supplied, the default function + is an exponential distribution with rate parameter :math:`\lambda=1`. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + pos_name : string, default="pos" + The name of the node attribute which represents the position + in 2D coordinates of the node in the returned graph. + + Returns + ------- + Graph + A soft random geometric graph, undirected and without self-loops. + Each node has a node attribute ``'pos'`` that stores the + position of that node in Euclidean space as provided by the + ``pos`` keyword argument or, if ``pos`` was not provided, as + generated by this function. + + Notes + ----- + This uses a *k*-d tree to build the graph. + + References + ---------- + .. [1] Penrose, Mathew D. "Connectivity of soft random geometric graphs." + The Annals of Applied Probability 26.2 (2016): 986-1028. + + Examples + -------- + Default Graph: + + >>> G = nx.soft_random_geometric_graph(50, 0.2) + + Custom Graph: + + The `pos` keyword argument can be used to specify node positions so you + can create an arbitrary distribution and domain for positions. + + The `scipy.stats` package can be used to define the probability distribution + with the ``.pdf`` method used as `p_dist`. + + For example, create a soft random geometric graph on 100 nodes using a 2D + Gaussian distribution of node positions with mean (0, 0) and standard deviation 2, + where nodes are joined by an edge with probability computed from an + exponential distribution with rate parameter :math:`\lambda=1` if their + Euclidean distance is at most 0.2. + + >>> import random + >>> from scipy.stats import expon + >>> n = 100 + >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)} + >>> p_dist = lambda x: expon.pdf(x, scale=1) + >>> G = nx.soft_random_geometric_graph(n, 0.2, pos=pos, p_dist=p_dist) + + """ + G = nx.empty_graph(n) + G.name = f"soft_random_geometric_graph({n}, {radius}, {dim})" + # If no positions are provided, choose uniformly random vectors in + # Euclidean space of the specified dimension. + if pos is None: + pos = {v: [seed.random() for i in range(dim)] for v in G} + nx.set_node_attributes(G, pos, pos_name) + + # if p_dist function not supplied the default function is an exponential + # distribution with rate parameter :math:`\lambda=1`. + if p_dist is None: + + def p_dist(dist): + return math.exp(-dist) + + def should_join(edge): + u, v = edge + dist = (sum(abs(a - b) ** p for a, b in zip(pos[u], pos[v]))) ** (1 / p) + return seed.random() < p_dist(dist) + + G.add_edges_from(filter(should_join, _geometric_edges(G, radius, p, pos_name))) + return G + + +@py_random_state(7) +@nx._dispatchable(graphs=None, returns_graph=True) +def geographical_threshold_graph( + n, + theta, + dim=2, + pos=None, + weight=None, + metric=None, + p_dist=None, + seed=None, + *, + pos_name="pos", + weight_name="weight", +): + r"""Returns a geographical threshold graph. + + The geographical threshold graph model places $n$ nodes uniformly at + random in a rectangular domain. Each node $u$ is assigned a weight + $w_u$. Two nodes $u$ and $v$ are joined by an edge if + + .. math:: + + (w_u + w_v)p_{dist}(r) \ge \theta + + where `r` is the distance between `u` and `v`, `p_dist` is any function of + `r`, and :math:`\theta` as the threshold parameter. `p_dist` is used to + give weight to the distance between nodes when deciding whether or not + they should be connected. The larger `p_dist` is, the more prone nodes + separated by `r` are to be connected, and vice versa. + + Parameters + ---------- + n : int or iterable + Number of nodes or iterable of nodes + theta: float + Threshold value + dim : int, optional + Dimension of graph + pos : dict + Node positions as a dictionary of tuples keyed by node. + weight : dict + Node weights as a dictionary of numbers keyed by node. + metric : function + A metric on vectors of numbers (represented as lists or + tuples). This must be a function that accepts two lists (or + tuples) as input and yields a number as output. The function + must also satisfy the four requirements of a `metric`_. + Specifically, if $d$ is the function and $x$, $y$, + and $z$ are vectors in the graph, then $d$ must satisfy + + 1. $d(x, y) \ge 0$, + 2. $d(x, y) = 0$ if and only if $x = y$, + 3. $d(x, y) = d(y, x)$, + 4. $d(x, z) \le d(x, y) + d(y, z)$. + + If this argument is not specified, the Euclidean distance metric is + used. + + .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29 + p_dist : function, optional + Any function used to give weight to the distance between nodes when + deciding whether or not they should be connected. `p_dist` was + originally conceived as a probability density function giving the + probability of connecting two nodes that are of metric distance `r` + apart. The implementation here allows for more arbitrary definitions + of `p_dist` that do not need to correspond to valid probability + density functions. The :mod:`scipy.stats` package has many + probability density functions implemented and tools for custom + probability density definitions, and passing the ``.pdf`` method of + `scipy.stats` distributions can be used here. If ``p_dist=None`` + (the default), the exponential function :math:`r^{-2}` is used. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + pos_name : string, default="pos" + The name of the node attribute which represents the position + in 2D coordinates of the node in the returned graph. + weight_name : string, default="weight" + The name of the node attribute which represents the weight + of the node in the returned graph. + + Returns + ------- + Graph + A random geographic threshold graph, undirected and without + self-loops. + + Each node has a node attribute ``pos`` that stores the + position of that node in Euclidean space as provided by the + ``pos`` keyword argument or, if ``pos`` was not provided, as + generated by this function. Similarly, each node has a node + attribute ``weight`` that stores the weight of that node as + provided or as generated. + + Examples + -------- + Specify an alternate distance metric using the ``metric`` keyword + argument. For example, to use the `taxicab metric`_ instead of the + default `Euclidean metric`_:: + + >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y)) + >>> G = nx.geographical_threshold_graph(10, 0.1, metric=dist) + + .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry + .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance + + Notes + ----- + If weights are not specified they are assigned to nodes by drawing randomly + from the exponential distribution with rate parameter $\lambda=1$. + To specify weights from a different distribution, use the `weight` keyword + argument:: + + >>> import random + >>> n = 20 + >>> w = {i: random.expovariate(5.0) for i in range(n)} + >>> G = nx.geographical_threshold_graph(20, 50, weight=w) + + If node positions are not specified they are randomly assigned from the + uniform distribution. + + References + ---------- + .. [1] Masuda, N., Miwa, H., Konno, N.: + Geographical threshold graphs with small-world and scale-free + properties. + Physical Review E 71, 036108 (2005) + .. [2] Milan Bradonjić, Aric Hagberg and Allon G. Percus, + Giant component and connectivity in geographical threshold graphs, + in Algorithms and Models for the Web-Graph (WAW 2007), + Antony Bonato and Fan Chung (Eds), pp. 209--216, 2007 + """ + G = nx.empty_graph(n) + # If no weights are provided, choose them from an exponential + # distribution. + if weight is None: + weight = {v: seed.expovariate(1) for v in G} + # If no positions are provided, choose uniformly random vectors in + # Euclidean space of the specified dimension. + if pos is None: + pos = {v: [seed.random() for i in range(dim)] for v in G} + # If no distance metric is provided, use Euclidean distance. + if metric is None: + metric = math.dist + nx.set_node_attributes(G, weight, weight_name) + nx.set_node_attributes(G, pos, pos_name) + + # if p_dist is not supplied, use default r^-2 + if p_dist is None: + + def p_dist(r): + return r**-2 + + # Returns ``True`` if and only if the nodes whose attributes are + # ``du`` and ``dv`` should be joined, according to the threshold + # condition. + def should_join(pair): + u, v = pair + u_pos, v_pos = pos[u], pos[v] + u_weight, v_weight = weight[u], weight[v] + return (u_weight + v_weight) * p_dist(metric(u_pos, v_pos)) >= theta + + G.add_edges_from(filter(should_join, combinations(G, 2))) + return G + + +@py_random_state(6) +@nx._dispatchable(graphs=None, returns_graph=True) +def waxman_graph( + n, + beta=0.4, + alpha=0.1, + L=None, + domain=(0, 0, 1, 1), + metric=None, + seed=None, + *, + pos_name="pos", +): + r"""Returns a Waxman random graph. + + The Waxman random graph model places `n` nodes uniformly at random + in a rectangular domain. Each pair of nodes at distance `d` is + joined by an edge with probability + + .. math:: + p = \beta \exp(-d / \alpha L). + + This function implements both Waxman models, using the `L` keyword + argument. + + * Waxman-1: if `L` is not specified, it is set to be the maximum distance + between any pair of nodes. + * Waxman-2: if `L` is specified, the distance between a pair of nodes is + chosen uniformly at random from the interval `[0, L]`. + + Parameters + ---------- + n : int or iterable + Number of nodes or iterable of nodes + beta: float + Model parameter + alpha: float + Model parameter + L : float, optional + Maximum distance between nodes. If not specified, the actual distance + is calculated. + domain : four-tuple of numbers, optional + Domain size, given as a tuple of the form `(x_min, y_min, x_max, + y_max)`. + metric : function + A metric on vectors of numbers (represented as lists or + tuples). This must be a function that accepts two lists (or + tuples) as input and yields a number as output. The function + must also satisfy the four requirements of a `metric`_. + Specifically, if $d$ is the function and $x$, $y$, + and $z$ are vectors in the graph, then $d$ must satisfy + + 1. $d(x, y) \ge 0$, + 2. $d(x, y) = 0$ if and only if $x = y$, + 3. $d(x, y) = d(y, x)$, + 4. $d(x, z) \le d(x, y) + d(y, z)$. + + If this argument is not specified, the Euclidean distance metric is + used. + + .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29 + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + pos_name : string, default="pos" + The name of the node attribute which represents the position + in 2D coordinates of the node in the returned graph. + + Returns + ------- + Graph + A random Waxman graph, undirected and without self-loops. Each + node has a node attribute ``'pos'`` that stores the position of + that node in Euclidean space as generated by this function. + + Examples + -------- + Specify an alternate distance metric using the ``metric`` keyword + argument. For example, to use the "`taxicab metric`_" instead of the + default `Euclidean metric`_:: + + >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y)) + >>> G = nx.waxman_graph(10, 0.5, 0.1, metric=dist) + + .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry + .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance + + Notes + ----- + Starting in NetworkX 2.0 the parameters alpha and beta align with their + usual roles in the probability distribution. In earlier versions their + positions in the expression were reversed. Their position in the calling + sequence reversed as well to minimize backward incompatibility. + + References + ---------- + .. [1] B. M. Waxman, *Routing of multipoint connections*. + IEEE J. Select. Areas Commun. 6(9),(1988) 1617--1622. + """ + G = nx.empty_graph(n) + (xmin, ymin, xmax, ymax) = domain + # Each node gets a uniformly random position in the given rectangle. + pos = {v: (seed.uniform(xmin, xmax), seed.uniform(ymin, ymax)) for v in G} + nx.set_node_attributes(G, pos, pos_name) + # If no distance metric is provided, use Euclidean distance. + if metric is None: + metric = math.dist + # If the maximum distance L is not specified (that is, we are in the + # Waxman-1 model), then find the maximum distance between any pair + # of nodes. + # + # In the Waxman-1 model, join nodes randomly based on distance. In + # the Waxman-2 model, join randomly based on random l. + if L is None: + L = max(metric(x, y) for x, y in combinations(pos.values(), 2)) + + def dist(u, v): + return metric(pos[u], pos[v]) + + else: + + def dist(u, v): + return seed.random() * L + + # `pair` is the pair of nodes to decide whether to join. + def should_join(pair): + return seed.random() < beta * math.exp(-dist(*pair) / (alpha * L)) + + G.add_edges_from(filter(should_join, combinations(G, 2))) + return G + + +@py_random_state(5) +@nx._dispatchable(graphs=None, returns_graph=True) +def navigable_small_world_graph(n, p=1, q=1, r=2, dim=2, seed=None): + r"""Returns a navigable small-world graph. + + A navigable small-world graph is a directed grid with additional long-range + connections that are chosen randomly. + + [...] we begin with a set of nodes [...] that are identified with the set + of lattice points in an $n \times n$ square, + $\{(i, j): i \in \{1, 2, \ldots, n\}, j \in \{1, 2, \ldots, n\}\}$, + and we define the *lattice distance* between two nodes $(i, j)$ and + $(k, l)$ to be the number of "lattice steps" separating them: + $d((i, j), (k, l)) = |k - i| + |l - j|$. + + For a universal constant $p >= 1$, the node $u$ has a directed edge to + every other node within lattice distance $p$---these are its *local + contacts*. For universal constants $q >= 0$ and $r >= 0$ we also + construct directed edges from $u$ to $q$ other nodes (the *long-range + contacts*) using independent random trials; the $i$th directed edge from + $u$ has endpoint $v$ with probability proportional to $[d(u,v)]^{-r}$. + + -- [1]_ + + Parameters + ---------- + n : int + The length of one side of the lattice; the number of nodes in + the graph is therefore $n^2$. + p : int + The diameter of short range connections. Each node is joined with every + other node within this lattice distance. + q : int + The number of long-range connections for each node. + r : float + Exponent for decaying probability of connections. The probability of + connecting to a node at lattice distance $d$ is $1/d^r$. + dim : int + Dimension of grid + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + References + ---------- + .. [1] J. Kleinberg. The small-world phenomenon: An algorithmic + perspective. Proc. 32nd ACM Symposium on Theory of Computing, 2000. + """ + if p < 1: + raise nx.NetworkXException("p must be >= 1") + if q < 0: + raise nx.NetworkXException("q must be >= 0") + if r < 0: + raise nx.NetworkXException("r must be >= 0") + + G = nx.DiGraph() + nodes = list(product(range(n), repeat=dim)) + for p1 in nodes: + probs = [0] + for p2 in nodes: + if p1 == p2: + continue + d = sum((abs(b - a) for a, b in zip(p1, p2))) + if d <= p: + G.add_edge(p1, p2) + probs.append(d**-r) + cdf = list(accumulate(probs)) + for _ in range(q): + target = nodes[bisect_left(cdf, seed.uniform(0, cdf[-1]))] + G.add_edge(p1, target) + return G + + +@py_random_state(7) +@nx._dispatchable(graphs=None, returns_graph=True) +def thresholded_random_geometric_graph( + n, + radius, + theta, + dim=2, + pos=None, + weight=None, + p=2, + seed=None, + *, + pos_name="pos", + weight_name="weight", +): + r"""Returns a thresholded random geometric graph in the unit cube. + + The thresholded random geometric graph [1] model places `n` nodes + uniformly at random in the unit cube of dimensions `dim`. Each node + `u` is assigned a weight :math:`w_u`. Two nodes `u` and `v` are + joined by an edge if they are within the maximum connection distance, + `radius` computed by the `p`-Minkowski distance and the summation of + weights :math:`w_u` + :math:`w_v` is greater than or equal + to the threshold parameter `theta`. + + Edges within `radius` of each other are determined using a KDTree when + SciPy is available. This reduces the time complexity from :math:`O(n^2)` + to :math:`O(n)`. + + Parameters + ---------- + n : int or iterable + Number of nodes or iterable of nodes + radius: float + Distance threshold value + theta: float + Threshold value + dim : int, optional + Dimension of graph + pos : dict, optional + A dictionary keyed by node with node positions as values. + weight : dict, optional + Node weights as a dictionary of numbers keyed by node. + p : float, optional (default 2) + Which Minkowski distance metric to use. `p` has to meet the condition + ``1 <= p <= infinity``. + + If this argument is not specified, the :math:`L^2` metric + (the Euclidean distance metric), p = 2 is used. + + This should not be confused with the `p` of an Erdős-Rényi random + graph, which represents probability. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + pos_name : string, default="pos" + The name of the node attribute which represents the position + in 2D coordinates of the node in the returned graph. + weight_name : string, default="weight" + The name of the node attribute which represents the weight + of the node in the returned graph. + + Returns + ------- + Graph + A thresholded random geographic graph, undirected and without + self-loops. + + Each node has a node attribute ``'pos'`` that stores the + position of that node in Euclidean space as provided by the + ``pos`` keyword argument or, if ``pos`` was not provided, as + generated by this function. Similarly, each node has a nodethre + attribute ``'weight'`` that stores the weight of that node as + provided or as generated. + + Notes + ----- + This uses a *k*-d tree to build the graph. + + References + ---------- + .. [1] http://cole-maclean.github.io/blog/files/thesis.pdf + + Examples + -------- + Default Graph: + + >>> G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1) + + Custom Graph: + + The `pos` keyword argument can be used to specify node positions so you + can create an arbitrary distribution and domain for positions. + + If weights are not specified they are assigned to nodes by drawing randomly + from the exponential distribution with rate parameter :math:`\lambda=1`. + To specify weights from a different distribution, use the `weight` keyword + argument. + + For example, create a thresholded random geometric graph on 50 nodes using a 2D + Gaussian distribution of node positions with mean (0, 0) and standard deviation 2, + where nodes are joined by an edge if their sum weights drawn from + a exponential distribution with rate = 5 are >= theta = 0.1 and their + Euclidean distance is at most 0.2. + + >>> import random + >>> n = 50 + >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)} + >>> w = {i: random.expovariate(5.0) for i in range(n)} + >>> G = nx.thresholded_random_geometric_graph(n, 0.2, 0.1, 2, pos, w) + + """ + G = nx.empty_graph(n) + G.name = f"thresholded_random_geometric_graph({n}, {radius}, {theta}, {dim})" + # If no weights are provided, choose them from an exponential + # distribution. + if weight is None: + weight = {v: seed.expovariate(1) for v in G} + # If no positions are provided, choose uniformly random vectors in + # Euclidean space of the specified dimension. + if pos is None: + pos = {v: [seed.random() for i in range(dim)] for v in G} + # If no distance metric is provided, use Euclidean distance. + nx.set_node_attributes(G, weight, weight_name) + nx.set_node_attributes(G, pos, pos_name) + + edges = ( + (u, v) + for u, v in _geometric_edges(G, radius, p, pos_name) + if weight[u] + weight[v] >= theta + ) + G.add_edges_from(edges) + return G + + +@py_random_state(5) +@nx._dispatchable(graphs=None, returns_graph=True) +def geometric_soft_configuration_graph( + *, beta, n=None, gamma=None, mean_degree=None, kappas=None, seed=None +): + r"""Returns a random graph from the geometric soft configuration model. + + The $\mathbb{S}^1$ model [1]_ is the geometric soft configuration model + which is able to explain many fundamental features of real networks such as + small-world property, heteregenous degree distributions, high level of + clustering, and self-similarity. + + In the geometric soft configuration model, a node $i$ is assigned two hidden + variables: a hidden degree $\kappa_i$, quantifying its popularity, influence, + or importance, and an angular position $\theta_i$ in a circle abstracting the + similarity space, where angular distances between nodes are a proxy for their + similarity. Focusing on the angular position, this model is often called + the $\mathbb{S}^1$ model (a one-dimensional sphere). The circle's radius is + adjusted to $R = N/2\pi$, where $N$ is the number of nodes, so that the density + is set to 1 without loss of generality. + + The connection probability between any pair of nodes increases with + the product of their hidden degrees (i.e., their combined popularities), + and decreases with the angular distance between the two nodes. + Specifically, nodes $i$ and $j$ are connected with the probability + + $p_{ij} = \frac{1}{1 + \frac{d_{ij}^\beta}{\left(\mu \kappa_i \kappa_j\right)^{\max(1, \beta)}}}$ + + where $d_{ij} = R\Delta\theta_{ij}$ is the arc length of the circle between + nodes $i$ and $j$ separated by an angular distance $\Delta\theta_{ij}$. + Parameters $\mu$ and $\beta$ (also called inverse temperature) control the + average degree and the clustering coefficient, respectively. + + It can be shown [2]_ that the model undergoes a structural phase transition + at $\beta=1$ so that for $\beta<1$ networks are unclustered in the thermodynamic + limit (when $N\to \infty$) whereas for $\beta>1$ the ensemble generates + networks with finite clustering coefficient. + + The $\mathbb{S}^1$ model can be expressed as a purely geometric model + $\mathbb{H}^2$ in the hyperbolic plane [3]_ by mapping the hidden degree of + each node into a radial coordinate as + + $r_i = \hat{R} - \frac{2 \max(1, \beta)}{\beta \zeta} \ln \left(\frac{\kappa_i}{\kappa_0}\right)$ + + where $\hat{R}$ is the radius of the hyperbolic disk and $\zeta$ is the curvature, + + $\hat{R} = \frac{2}{\zeta} \ln \left(\frac{N}{\pi}\right) + - \frac{2\max(1, \beta)}{\beta \zeta} \ln (\mu \kappa_0^2)$ + + The connection probability then reads + + $p_{ij} = \frac{1}{1 + \exp\left({\frac{\beta\zeta}{2} (x_{ij} - \hat{R})}\right)}$ + + where + + $x_{ij} = r_i + r_j + \frac{2}{\zeta} \ln \frac{\Delta\theta_{ij}}{2}$ + + is a good approximation of the hyperbolic distance between two nodes separated + by an angular distance $\Delta\theta_{ij}$ with radial coordinates $r_i$ and $r_j$. + For $\beta > 1$, the curvature $\zeta = 1$, for $\beta < 1$, $\zeta = \beta^{-1}$. + + + Parameters + ---------- + Either `n`, `gamma`, `mean_degree` are provided or `kappas`. The values of + `n`, `gamma`, `mean_degree` (if provided) are used to construct a random + kappa-dict keyed by node with values sampled from a power-law distribution. + + beta : positive number + Inverse temperature, controlling the clustering coefficient. + n : int (default: None) + Size of the network (number of nodes). + If not provided, `kappas` must be provided and holds the nodes. + gamma : float (default: None) + Exponent of the power-law distribution for hidden degrees `kappas`. + If not provided, `kappas` must be provided directly. + mean_degree : float (default: None) + The mean degree in the network. + If not provided, `kappas` must be provided directly. + kappas : dict (default: None) + A dict keyed by node to its hidden degree value. + If not provided, random values are computed based on a power-law + distribution using `n`, `gamma` and `mean_degree`. + seed : int, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + Graph + A random geometric soft configuration graph (undirected with no self-loops). + Each node has three node-attributes: + + - ``kappa`` that represents the hidden degree. + + - ``theta`` the position in the similarity space ($\mathbb{S}^1$) which is + also the angular position in the hyperbolic plane. + + - ``radius`` the radial position in the hyperbolic plane + (based on the hidden degree). + + + Examples + -------- + Generate a network with specified parameters: + + >>> G = nx.geometric_soft_configuration_graph( + ... beta=1.5, n=100, gamma=2.7, mean_degree=5 + ... ) + + Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter + is set to 1.5 and the exponent of the powerlaw distribution of the hidden + degrees is 2.7 with mean value of 5. + + Generate a network with predefined hidden degrees: + + >>> kappas = {i: 10 for i in range(100)} + >>> G = nx.geometric_soft_configuration_graph(beta=2.5, kappas=kappas) + + Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter + is set to 2.5 and all nodes with hidden degree $\kappa=10$. + + + References + ---------- + .. [1] Serrano, M. Á., Krioukov, D., & Boguñá, M. (2008). Self-similarity + of complex networks and hidden metric spaces. Physical review letters, 100(7), 078701. + + .. [2] van der Kolk, J., Serrano, M. Á., & Boguñá, M. (2022). An anomalous + topological phase transition in spatial random graphs. Communications Physics, 5(1), 245. + + .. [3] Krioukov, D., Papadopoulos, F., Kitsak, M., Vahdat, A., & Boguná, M. (2010). + Hyperbolic geometry of complex networks. Physical Review E, 82(3), 036106. + + """ + if beta <= 0: + raise nx.NetworkXError("The parameter beta cannot be smaller or equal to 0.") + + if kappas is not None: + if not all((n is None, gamma is None, mean_degree is None)): + raise nx.NetworkXError( + "When kappas is input, n, gamma and mean_degree must not be." + ) + + n = len(kappas) + mean_degree = sum(kappas) / len(kappas) + else: + if any((n is None, gamma is None, mean_degree is None)): + raise nx.NetworkXError( + "Please provide either kappas, or all 3 of: n, gamma and mean_degree." + ) + + # Generate `n` hidden degrees from a powerlaw distribution + # with given exponent `gamma` and mean value `mean_degree` + gam_ratio = (gamma - 2) / (gamma - 1) + kappa_0 = mean_degree * gam_ratio * (1 - 1 / n) / (1 - 1 / n**gam_ratio) + base = 1 - 1 / n + power = 1 / (1 - gamma) + kappas = {i: kappa_0 * (1 - seed.random() * base) ** power for i in range(n)} + + G = nx.Graph() + R = n / (2 * math.pi) + + # Approximate values for mu in the thermodynamic limit (when n -> infinity) + if beta > 1: + mu = beta * math.sin(math.pi / beta) / (2 * math.pi * mean_degree) + elif beta == 1: + mu = 1 / (2 * mean_degree * math.log(n)) + else: + mu = (1 - beta) / (2**beta * mean_degree * n ** (1 - beta)) + + # Generate random positions on a circle + thetas = {k: seed.uniform(0, 2 * math.pi) for k in kappas} + + for u in kappas: + for v in list(G): + angle = math.pi - math.fabs(math.pi - math.fabs(thetas[u] - thetas[v])) + dij = math.pow(R * angle, beta) + mu_kappas = math.pow(mu * kappas[u] * kappas[v], max(1, beta)) + p_ij = 1 / (1 + dij / mu_kappas) + + # Create an edge with a certain connection probability + if seed.random() < p_ij: + G.add_edge(u, v) + G.add_node(u) + + nx.set_node_attributes(G, thetas, "theta") + nx.set_node_attributes(G, kappas, "kappa") + + # Map hidden degrees into the radial coordinates + zeta = 1 if beta > 1 else 1 / beta + kappa_min = min(kappas.values()) + R_c = 2 * max(1, beta) / (beta * zeta) + R_hat = (2 / zeta) * math.log(n / math.pi) - R_c * math.log(mu * kappa_min) + radii = {node: R_hat - R_c * math.log(kappa) for node, kappa in kappas.items()} + nx.set_node_attributes(G, radii, "radius") + + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/harary_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/harary_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..e5dde2e8998f25dbaf68ab8bd7a64f846cd29275 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/harary_graph.py @@ -0,0 +1,163 @@ +"""Generators for Harary graphs + +This module gives two generators for the Harary graph, which was +introduced by the famous mathematician Frank Harary in his 1962 work [H]_. +The first generator gives the Harary graph that maximizes the node +connectivity with given number of nodes and given number of edges. +The second generator gives the Harary graph that minimizes +the number of edges in the graph with given node connectivity and +number of nodes. + +References +---------- +.. [H] Harary, F. "The Maximum Connectivity of a Graph." + Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962. + +""" + +import networkx as nx +from networkx.exception import NetworkXError + +__all__ = ["hnm_harary_graph", "hkn_harary_graph"] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def hnm_harary_graph(n, m, create_using=None): + r"""Return the Harary graph with given numbers of nodes and edges. + + The Harary graph $H_{n, m}$ is the graph that maximizes node connectivity + with $n$ nodes and $m$ edges. + + This maximum node connectivity is known to be $\lfloor 2m/n \rfloor$. [1]_ + + Parameters + ---------- + n: integer + The number of nodes the generated graph is to contain. + + m: integer + The number of edges the generated graph is to contain. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + The Harary graph $H_{n, m}$. + + See Also + -------- + hkn_harary_graph + + Notes + ----- + This algorithm runs in $O(m)$ time. + The implementation follows [2]_. + + References + ---------- + .. [1] F. T. Boesch, A. Satyanarayana, and C. L. Suffel, + "A Survey of Some Network Reliability Analysis and Synthesis Results," + Networks, pp. 99-107, 2009. + + .. [2] Harary, F. "The Maximum Connectivity of a Graph." + Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962. + """ + + if n < 1: + raise NetworkXError("The number of nodes must be >= 1!") + if m < n - 1: + raise NetworkXError("The number of edges must be >= n - 1 !") + if m > n * (n - 1) // 2: + raise NetworkXError("The number of edges must be <= n(n-1)/2") + + # Get the floor of average node degree. + d = 2 * m // n + + offset = d // 2 + H = nx.circulant_graph(n, range(1, offset + 1), create_using=create_using) + + half = n // 2 + if (n % 2 == 0) or (d % 2 == 0): + # If d is odd; n must be even. + if d % 2 == 1: + # Add edges diagonally. + H.add_edges_from((i, i + half) for i in range(half)) + + r = 2 * m % n + # Add remaining edges at offset + 1. + H.add_edges_from((i, i + offset + 1) for i in range(r // 2)) + else: + # Add the remaining m - n * offset edges between i and i + half. + H.add_edges_from((i, (i + half) % n) for i in range(m - n * offset)) + + return H + + +@nx._dispatchable(graphs=None, returns_graph=True) +def hkn_harary_graph(k, n, create_using=None): + r"""Return the Harary graph with given node connectivity and node number. + + The Harary graph $H_{k, n}$ is the graph that minimizes the number of + edges needed with given node connectivity $k$ and node number $n$. + + This smallest number of edges is known to be $\lceil kn/2 \rceil$ [1]_. + + Parameters + ---------- + k: integer + The node connectivity of the generated graph. + + n: integer + The number of nodes the generated graph is to contain. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + The Harary graph $H_{k, n}$. + + See Also + -------- + hnm_harary_graph + + Notes + ----- + This algorithm runs in $O(kn)$ time. + The implementation follows [2]_. + + References + ---------- + .. [1] Weisstein, Eric W. "Harary Graph." From MathWorld--A Wolfram Web + Resource. http://mathworld.wolfram.com/HararyGraph.html. + + .. [2] Harary, F. "The Maximum Connectivity of a Graph." + Proc. Nat. Acad. Sci. USA 48, 1142-1146, 1962. + """ + + if k < 1: + raise NetworkXError("The node connectivity must be >= 1!") + if n < k + 1: + raise NetworkXError("The number of nodes must be >= k+1 !") + + # In case of connectivity 1, simply return the path graph. + if k == 1: + return nx.path_graph(n, create_using) + + offset = k // 2 + H = nx.circulant_graph(n, range(1, offset + 1), create_using=create_using) + + half = n // 2 + if (k % 2 == 0) or (n % 2 == 0): + # If k is odd; n must be even. + if k % 2 == 1: + # Add edges diagonally. + H.add_edges_from((i, i + half) for i in range(half)) + else: + # Add half + 1 edges between i and i + half. + H.add_edges_from((i, (i + half) % n) for i in range(half + 1)) + + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py new file mode 100644 index 0000000000000000000000000000000000000000..31fbb6dd03a796aa8abc0dc5afd7c991baf5a993 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/internet_as_graphs.py @@ -0,0 +1,443 @@ +"""Generates graphs resembling the Internet Autonomous System network""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = ["random_internet_as_graph"] + + +def uniform_int_from_avg(a, m, seed): + """Pick a random integer with uniform probability. + + Returns a random integer uniformly taken from a distribution with + minimum value 'a' and average value 'm', X~U(a,b), E[X]=m, X in N where + b = 2*m - a. + + Notes + ----- + p = (b-floor(b))/2 + X = X1 + X2; X1~U(a,floor(b)), X2~B(p) + E[X] = E[X1] + E[X2] = (floor(b)+a)/2 + (b-floor(b))/2 = (b+a)/2 = m + """ + + from math import floor + + assert m >= a + b = 2 * m - a + p = (b - floor(b)) / 2 + X1 = round(seed.random() * (floor(b) - a) + a) + if seed.random() < p: + X2 = 1 + else: + X2 = 0 + return X1 + X2 + + +@py_random_state("seed") +def choose_pref_attach(degs, seed): + """Pick a random value, with a probability given by its weight. + + Returns a random choice among degs keys, each of which has a + probability proportional to the corresponding dictionary value. + + Parameters + ---------- + degs: dictionary + It contains the possible values (keys) and the corresponding + probabilities (values) + seed: random state + + Returns + ------- + v: object + A key of degs or None if degs is empty + """ + + if len(degs) == 0: + return None + s = sum(degs.values()) + if s == 0: + return seed.choice(list(degs.keys())) + v = seed.random() * s + + nodes = list(degs.keys()) + i = 0 + acc = degs[nodes[i]] + while v > acc: + i += 1 + acc += degs[nodes[i]] + return nodes[i] + + +class AS_graph_generator: + """Generates random internet AS graphs.""" + + @py_random_state("seed") + def __init__(self, n, seed): + """Initializes variables. Immediate numbers are taken from [1]. + + Parameters + ---------- + n: integer + Number of graph nodes + seed: random state + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + GG: AS_graph_generator object + + References + ---------- + [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of + BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas + in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010. + """ + + self.seed = seed + self.n_t = min(n, round(self.seed.random() * 2 + 4)) # num of T nodes + self.n_m = round(0.15 * n) # number of M nodes + self.n_cp = round(0.05 * n) # number of CP nodes + self.n_c = max(0, n - self.n_t - self.n_m - self.n_cp) # number of C nodes + + self.d_m = 2 + (2.5 * n) / 10000 # average multihoming degree for M nodes + self.d_cp = 2 + (1.5 * n) / 10000 # avg multihoming degree for CP nodes + self.d_c = 1 + (5 * n) / 100000 # average multihoming degree for C nodes + + self.p_m_m = 1 + (2 * n) / 10000 # avg num of peer edges between M and M + self.p_cp_m = 0.2 + (2 * n) / 10000 # avg num of peer edges between CP, M + self.p_cp_cp = 0.05 + (2 * n) / 100000 # avg num of peer edges btwn CP, CP + + self.t_m = 0.375 # probability M's provider is T + self.t_cp = 0.375 # probability CP's provider is T + self.t_c = 0.125 # probability C's provider is T + + def t_graph(self): + """Generates the core mesh network of tier one nodes of a AS graph. + + Returns + ------- + G: Networkx Graph + Core network + """ + + self.G = nx.Graph() + for i in range(self.n_t): + self.G.add_node(i, type="T") + for r in self.regions: + self.regions[r].add(i) + for j in self.G.nodes(): + if i != j: + self.add_edge(i, j, "peer") + self.customers[i] = set() + self.providers[i] = set() + return self.G + + def add_edge(self, i, j, kind): + if kind == "transit": + customer = str(i) + else: + customer = "none" + self.G.add_edge(i, j, type=kind, customer=customer) + + def choose_peer_pref_attach(self, node_list): + """Pick a node with a probability weighted by its peer degree. + + Pick a node from node_list with preferential attachment + computed only on their peer degree + """ + + d = {} + for n in node_list: + d[n] = self.G.nodes[n]["peers"] + return choose_pref_attach(d, self.seed) + + def choose_node_pref_attach(self, node_list): + """Pick a node with a probability weighted by its degree. + + Pick a node from node_list with preferential attachment + computed on their degree + """ + + degs = dict(self.G.degree(node_list)) + return choose_pref_attach(degs, self.seed) + + def add_customer(self, i, j): + """Keep the dictionaries 'customers' and 'providers' consistent.""" + + self.customers[j].add(i) + self.providers[i].add(j) + for z in self.providers[j]: + self.customers[z].add(i) + self.providers[i].add(z) + + def add_node(self, i, kind, reg2prob, avg_deg, t_edge_prob): + """Add a node and its customer transit edges to the graph. + + Parameters + ---------- + i: object + Identifier of the new node + kind: string + Type of the new node. Options are: 'M' for middle node, 'CP' for + content provider and 'C' for customer. + reg2prob: float + Probability the new node can be in two different regions. + avg_deg: float + Average number of transit nodes of which node i is customer. + t_edge_prob: float + Probability node i establish a customer transit edge with a tier + one (T) node + + Returns + ------- + i: object + Identifier of the new node + """ + + regs = 1 # regions in which node resides + if self.seed.random() < reg2prob: # node is in two regions + regs = 2 + node_options = set() + + self.G.add_node(i, type=kind, peers=0) + self.customers[i] = set() + self.providers[i] = set() + self.nodes[kind].add(i) + for r in self.seed.sample(list(self.regions), regs): + node_options = node_options.union(self.regions[r]) + self.regions[r].add(i) + + edge_num = uniform_int_from_avg(1, avg_deg, self.seed) + + t_options = node_options.intersection(self.nodes["T"]) + m_options = node_options.intersection(self.nodes["M"]) + if i in m_options: + m_options.remove(i) + d = 0 + while d < edge_num and (len(t_options) > 0 or len(m_options) > 0): + if len(m_options) == 0 or ( + len(t_options) > 0 and self.seed.random() < t_edge_prob + ): # add edge to a T node + j = self.choose_node_pref_attach(t_options) + t_options.remove(j) + else: + j = self.choose_node_pref_attach(m_options) + m_options.remove(j) + self.add_edge(i, j, "transit") + self.add_customer(i, j) + d += 1 + + return i + + def add_m_peering_link(self, m, to_kind): + """Add a peering link between two middle tier (M) nodes. + + Target node j is drawn considering a preferential attachment based on + other M node peering degree. + + Parameters + ---------- + m: object + Node identifier + to_kind: string + type for target node j (must be always M) + + Returns + ------- + success: boolean + """ + + # candidates are of type 'M' and are not customers of m + node_options = self.nodes["M"].difference(self.customers[m]) + # candidates are not providers of m + node_options = node_options.difference(self.providers[m]) + # remove self + if m in node_options: + node_options.remove(m) + + # remove candidates we are already connected to + for j in self.G.neighbors(m): + if j in node_options: + node_options.remove(j) + + if len(node_options) > 0: + j = self.choose_peer_pref_attach(node_options) + self.add_edge(m, j, "peer") + self.G.nodes[m]["peers"] += 1 + self.G.nodes[j]["peers"] += 1 + return True + else: + return False + + def add_cp_peering_link(self, cp, to_kind): + """Add a peering link to a content provider (CP) node. + + Target node j can be CP or M and it is drawn uniformly among the nodes + belonging to the same region as cp. + + Parameters + ---------- + cp: object + Node identifier + to_kind: string + type for target node j (must be M or CP) + + Returns + ------- + success: boolean + """ + + node_options = set() + for r in self.regions: # options include nodes in the same region(s) + if cp in self.regions[r]: + node_options = node_options.union(self.regions[r]) + + # options are restricted to the indicated kind ('M' or 'CP') + node_options = self.nodes[to_kind].intersection(node_options) + + # remove self + if cp in node_options: + node_options.remove(cp) + + # remove nodes that are cp's providers + node_options = node_options.difference(self.providers[cp]) + + # remove nodes we are already connected to + for j in self.G.neighbors(cp): + if j in node_options: + node_options.remove(j) + + if len(node_options) > 0: + j = self.seed.sample(list(node_options), 1)[0] + self.add_edge(cp, j, "peer") + self.G.nodes[cp]["peers"] += 1 + self.G.nodes[j]["peers"] += 1 + return True + else: + return False + + def graph_regions(self, rn): + """Initializes AS network regions. + + Parameters + ---------- + rn: integer + Number of regions + """ + + self.regions = {} + for i in range(rn): + self.regions["REG" + str(i)] = set() + + def add_peering_links(self, from_kind, to_kind): + """Utility function to add peering links among node groups.""" + peer_link_method = None + if from_kind == "M": + peer_link_method = self.add_m_peering_link + m = self.p_m_m + if from_kind == "CP": + peer_link_method = self.add_cp_peering_link + if to_kind == "M": + m = self.p_cp_m + else: + m = self.p_cp_cp + + for i in self.nodes[from_kind]: + num = uniform_int_from_avg(0, m, self.seed) + for _ in range(num): + peer_link_method(i, to_kind) + + def generate(self): + """Generates a random AS network graph as described in [1]. + + Returns + ------- + G: Graph object + + Notes + ----- + The process steps are the following: first we create the core network + of tier one nodes, then we add the middle tier (M), the content + provider (CP) and the customer (C) nodes along with their transit edges + (link i,j means i is customer of j). Finally we add peering links + between M nodes, between M and CP nodes and between CP node couples. + For a detailed description of the algorithm, please refer to [1]. + + References + ---------- + [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of + BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas + in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010. + """ + + self.graph_regions(5) + self.customers = {} + self.providers = {} + self.nodes = {"T": set(), "M": set(), "CP": set(), "C": set()} + + self.t_graph() + self.nodes["T"] = set(self.G.nodes()) + + i = len(self.nodes["T"]) + for _ in range(self.n_m): + self.nodes["M"].add(self.add_node(i, "M", 0.2, self.d_m, self.t_m)) + i += 1 + for _ in range(self.n_cp): + self.nodes["CP"].add(self.add_node(i, "CP", 0.05, self.d_cp, self.t_cp)) + i += 1 + for _ in range(self.n_c): + self.nodes["C"].add(self.add_node(i, "C", 0, self.d_c, self.t_c)) + i += 1 + + self.add_peering_links("M", "M") + self.add_peering_links("CP", "M") + self.add_peering_links("CP", "CP") + + return self.G + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_internet_as_graph(n, seed=None): + """Generates a random undirected graph resembling the Internet AS network + + Parameters + ---------- + n: integer in [1000, 10000] + Number of graph nodes + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G: Networkx Graph object + A randomly generated undirected graph + + Notes + ----- + This algorithm returns an undirected graph resembling the Internet + Autonomous System (AS) network, it uses the approach by Elmokashfi et al. + [1]_ and it grants the properties described in the related paper [1]_. + + Each node models an autonomous system, with an attribute 'type' specifying + its kind; tier-1 (T), mid-level (M), customer (C) or content-provider (CP). + Each edge models an ADV communication link (hence, bidirectional) with + attributes: + + - type: transit|peer, the kind of commercial agreement between nodes; + - customer: , the identifier of the node acting as customer + ('none' if type is peer). + + References + ---------- + .. [1] A. Elmokashfi, A. Kvalbein and C. Dovrolis, "On the Scalability of + BGP: The Role of Topology Growth," in IEEE Journal on Selected Areas + in Communications, vol. 28, no. 8, pp. 1250-1261, October 2010. + """ + + GG = AS_graph_generator(n, seed) + G = GG.generate() + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/intersection.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/intersection.py new file mode 100644 index 0000000000000000000000000000000000000000..e63af5be8eed2e65b9cced2cc50827ffe6a802ba --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/intersection.py @@ -0,0 +1,125 @@ +""" +Generators for random intersection graphs. +""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "uniform_random_intersection_graph", + "k_random_intersection_graph", + "general_random_intersection_graph", +] + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def uniform_random_intersection_graph(n, m, p, seed=None): + """Returns a uniform random intersection graph. + + Parameters + ---------- + n : int + The number of nodes in the first bipartite set (nodes) + m : int + The number of nodes in the second bipartite set (attributes) + p : float + Probability of connecting nodes between bipartite sets + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + See Also + -------- + gnp_random_graph + + References + ---------- + .. [1] K.B. Singer-Cohen, Random Intersection Graphs, 1995, + PhD thesis, Johns Hopkins University + .. [2] Fill, J. A., Scheinerman, E. R., and Singer-Cohen, K. B., + Random intersection graphs when m = !(n): + An equivalence theorem relating the evolution of the g(n, m, p) + and g(n, p) models. Random Struct. Algorithms 16, 2 (2000), 156–176. + """ + from networkx.algorithms import bipartite + + G = bipartite.random_graph(n, m, p, seed) + return nx.projected_graph(G, range(n)) + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def k_random_intersection_graph(n, m, k, seed=None): + """Returns a intersection graph with randomly chosen attribute sets for + each node that are of equal size (k). + + Parameters + ---------- + n : int + The number of nodes in the first bipartite set (nodes) + m : int + The number of nodes in the second bipartite set (attributes) + k : float + Size of attribute set to assign to each node. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + See Also + -------- + gnp_random_graph, uniform_random_intersection_graph + + References + ---------- + .. [1] Godehardt, E., and Jaworski, J. + Two models of random intersection graphs and their applications. + Electronic Notes in Discrete Mathematics 10 (2001), 129--132. + """ + G = nx.empty_graph(n + m) + mset = range(n, n + m) + for v in range(n): + targets = seed.sample(mset, k) + G.add_edges_from(zip([v] * len(targets), targets)) + return nx.projected_graph(G, range(n)) + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def general_random_intersection_graph(n, m, p, seed=None): + """Returns a random intersection graph with independent probabilities + for connections between node and attribute sets. + + Parameters + ---------- + n : int + The number of nodes in the first bipartite set (nodes) + m : int + The number of nodes in the second bipartite set (attributes) + p : list of floats of length m + Probabilities for connecting nodes to each attribute + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + See Also + -------- + gnp_random_graph, uniform_random_intersection_graph + + References + ---------- + .. [1] Nikoletseas, S. E., Raptopoulos, C., and Spirakis, P. G. + The existence and efficient construction of large independent sets + in general random intersection graphs. In ICALP (2004), J. D´ıaz, + J. Karhum¨aki, A. Lepist¨o, and D. Sannella, Eds., vol. 3142 + of Lecture Notes in Computer Science, Springer, pp. 1029–1040. + """ + if len(p) != m: + raise ValueError("Probability list p must have m elements.") + G = nx.empty_graph(n + m) + mset = range(n, n + m) + for u in range(n): + for v, q in zip(mset, p): + if seed.random() < q: + G.add_edge(u, v) + return nx.projected_graph(G, range(n)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/interval_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/interval_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..6a3fda45acec52af6a5f060b96d9af1067fc002b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/interval_graph.py @@ -0,0 +1,70 @@ +""" +Generators for interval graph. +""" + +from collections.abc import Sequence + +import networkx as nx + +__all__ = ["interval_graph"] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def interval_graph(intervals): + """Generates an interval graph for a list of intervals given. + + In graph theory, an interval graph is an undirected graph formed from a set + of closed intervals on the real line, with a vertex for each interval + and an edge between vertices whose intervals intersect. + It is the intersection graph of the intervals. + + More information can be found at: + https://en.wikipedia.org/wiki/Interval_graph + + Parameters + ---------- + intervals : a sequence of intervals, say (l, r) where l is the left end, + and r is the right end of the closed interval. + + Returns + ------- + G : networkx graph + + Examples + -------- + >>> intervals = [(-2, 3), [1, 4], (2, 3), (4, 6)] + >>> G = nx.interval_graph(intervals) + >>> sorted(G.edges) + [((-2, 3), (1, 4)), ((-2, 3), (2, 3)), ((1, 4), (2, 3)), ((1, 4), (4, 6))] + + Raises + ------ + :exc:`TypeError` + if `intervals` contains None or an element which is not + collections.abc.Sequence or not a length of 2. + :exc:`ValueError` + if `intervals` contains an interval such that min1 > max1 + where min1,max1 = interval + """ + intervals = list(intervals) + for interval in intervals: + if not (isinstance(interval, Sequence) and len(interval) == 2): + raise TypeError( + "Each interval must have length 2, and be a " + "collections.abc.Sequence such as tuple or list." + ) + if interval[0] > interval[1]: + raise ValueError(f"Interval must have lower value first. Got {interval}") + + graph = nx.Graph() + + tupled_intervals = [tuple(interval) for interval in intervals] + graph.add_nodes_from(tupled_intervals) + + while tupled_intervals: + min1, max1 = interval1 = tupled_intervals.pop() + for interval2 in tupled_intervals: + min2, max2 = interval2 + if max1 >= min2 and max2 >= min1: + graph.add_edge(interval1, interval2) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py new file mode 100644 index 0000000000000000000000000000000000000000..c426df944ad27aef4584371838a6ddb280b90dca --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/joint_degree_seq.py @@ -0,0 +1,664 @@ +"""Generate graphs with a given joint degree and directed joint degree""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "is_valid_joint_degree", + "is_valid_directed_joint_degree", + "joint_degree_graph", + "directed_joint_degree_graph", +] + + +@nx._dispatchable(graphs=None) +def is_valid_joint_degree(joint_degrees): + """Checks whether the given joint degree dictionary is realizable. + + A *joint degree dictionary* is a dictionary of dictionaries, in + which entry ``joint_degrees[k][l]`` is an integer representing the + number of edges joining nodes of degree *k* with nodes of degree + *l*. Such a dictionary is realizable as a simple graph if and only + if the following conditions are satisfied. + + - each entry must be an integer, + - the total number of nodes of degree *k*, computed by + ``sum(joint_degrees[k].values()) / k``, must be an integer, + - the total number of edges joining nodes of degree *k* with + nodes of degree *l* cannot exceed the total number of possible edges, + - each diagonal entry ``joint_degrees[k][k]`` must be even (this is + a convention assumed by the :func:`joint_degree_graph` function). + + + Parameters + ---------- + joint_degrees : dictionary of dictionary of integers + A joint degree dictionary in which entry ``joint_degrees[k][l]`` + is the number of edges joining nodes of degree *k* with nodes of + degree *l*. + + Returns + ------- + bool + Whether the given joint degree dictionary is realizable as a + simple graph. + + References + ---------- + .. [1] M. Gjoka, M. Kurant, A. Markopoulou, "2.5K Graphs: from Sampling + to Generation", IEEE Infocom, 2013. + .. [2] I. Stanton, A. Pinar, "Constructing and sampling graphs with a + prescribed joint degree distribution", Journal of Experimental + Algorithmics, 2012. + """ + + degree_count = {} + for k in joint_degrees: + if k > 0: + k_size = sum(joint_degrees[k].values()) / k + if not k_size.is_integer(): + return False + degree_count[k] = k_size + + for k in joint_degrees: + for l in joint_degrees[k]: + if not float(joint_degrees[k][l]).is_integer(): + return False + + if (k != l) and (joint_degrees[k][l] > degree_count[k] * degree_count[l]): + return False + elif k == l: + if joint_degrees[k][k] > degree_count[k] * (degree_count[k] - 1): + return False + if joint_degrees[k][k] % 2 != 0: + return False + + # if all above conditions have been satisfied then the input + # joint degree is realizable as a simple graph. + return True + + +def _neighbor_switch(G, w, unsat, h_node_residual, avoid_node_id=None): + """Releases one free stub for ``w``, while preserving joint degree in G. + + Parameters + ---------- + G : NetworkX graph + Graph in which the neighbor switch will take place. + w : integer + Node id for which we will execute this neighbor switch. + unsat : set of integers + Set of unsaturated node ids that have the same degree as w. + h_node_residual: dictionary of integers + Keeps track of the remaining stubs for a given node. + avoid_node_id: integer + Node id to avoid when selecting w_prime. + + Notes + ----- + First, it selects *w_prime*, an unsaturated node that has the same degree + as ``w``. Second, it selects *switch_node*, a neighbor node of ``w`` that + is not connected to *w_prime*. Then it executes an edge swap i.e. removes + (``w``,*switch_node*) and adds (*w_prime*,*switch_node*). Gjoka et. al. [1] + prove that such an edge swap is always possible. + + References + ---------- + .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple + Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15 + """ + + if (avoid_node_id is None) or (h_node_residual[avoid_node_id] > 1): + # select unsaturated node w_prime that has the same degree as w + w_prime = next(iter(unsat)) + else: + # assume that the node pair (v,w) has been selected for connection. if + # - neighbor_switch is called for node w, + # - nodes v and w have the same degree, + # - node v=avoid_node_id has only one stub left, + # then prevent v=avoid_node_id from being selected as w_prime. + + iter_var = iter(unsat) + while True: + w_prime = next(iter_var) + if w_prime != avoid_node_id: + break + + # select switch_node, a neighbor of w, that is not connected to w_prime + w_prime_neighbs = G[w_prime] # slightly faster declaring this variable + for v in G[w]: + if (v not in w_prime_neighbs) and (v != w_prime): + switch_node = v + break + + # remove edge (w,switch_node), add edge (w_prime,switch_node) and update + # data structures + G.remove_edge(w, switch_node) + G.add_edge(w_prime, switch_node) + h_node_residual[w] += 1 + h_node_residual[w_prime] -= 1 + if h_node_residual[w_prime] == 0: + unsat.remove(w_prime) + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def joint_degree_graph(joint_degrees, seed=None): + """Generates a random simple graph with the given joint degree dictionary. + + Parameters + ---------- + joint_degrees : dictionary of dictionary of integers + A joint degree dictionary in which entry ``joint_degrees[k][l]`` is the + number of edges joining nodes of degree *k* with nodes of degree *l*. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : Graph + A graph with the specified joint degree dictionary. + + Raises + ------ + NetworkXError + If *joint_degrees* dictionary is not realizable. + + Notes + ----- + In each iteration of the "while loop" the algorithm picks two disconnected + nodes *v* and *w*, of degree *k* and *l* correspondingly, for which + ``joint_degrees[k][l]`` has not reached its target yet. It then adds + edge (*v*, *w*) and increases the number of edges in graph G by one. + + The intelligence of the algorithm lies in the fact that it is always + possible to add an edge between such disconnected nodes *v* and *w*, + even if one or both nodes do not have free stubs. That is made possible by + executing a "neighbor switch", an edge rewiring move that releases + a free stub while keeping the joint degree of G the same. + + The algorithm continues for E (number of edges) iterations of + the "while loop", at the which point all entries of the given + ``joint_degrees[k][l]`` have reached their target values and the + construction is complete. + + References + ---------- + .. [1] M. Gjoka, B. Tillman, A. Markopoulou, "Construction of Simple + Graphs with a Target Joint Degree Matrix and Beyond", IEEE Infocom, '15 + + Examples + -------- + >>> joint_degrees = { + ... 1: {4: 1}, + ... 2: {2: 2, 3: 2, 4: 2}, + ... 3: {2: 2, 4: 1}, + ... 4: {1: 1, 2: 2, 3: 1}, + ... } + >>> G = nx.joint_degree_graph(joint_degrees) + >>> + """ + + if not is_valid_joint_degree(joint_degrees): + msg = "Input joint degree dict not realizable as a simple graph" + raise nx.NetworkXError(msg) + + # compute degree count from joint_degrees + degree_count = {k: sum(l.values()) // k for k, l in joint_degrees.items() if k > 0} + + # start with empty N-node graph + N = sum(degree_count.values()) + G = nx.empty_graph(N) + + # for a given degree group, keep the list of all node ids + h_degree_nodelist = {} + + # for a given node, keep track of the remaining stubs + h_node_residual = {} + + # populate h_degree_nodelist and h_node_residual + nodeid = 0 + for degree, num_nodes in degree_count.items(): + h_degree_nodelist[degree] = range(nodeid, nodeid + num_nodes) + for v in h_degree_nodelist[degree]: + h_node_residual[v] = degree + nodeid += int(num_nodes) + + # iterate over every degree pair (k,l) and add the number of edges given + # for each pair + for k in joint_degrees: + for l in joint_degrees[k]: + # n_edges_add is the number of edges to add for the + # degree pair (k,l) + n_edges_add = joint_degrees[k][l] + + if (n_edges_add > 0) and (k >= l): + # number of nodes with degree k and l + k_size = degree_count[k] + l_size = degree_count[l] + + # k_nodes and l_nodes consist of all nodes of degree k and l + k_nodes = h_degree_nodelist[k] + l_nodes = h_degree_nodelist[l] + + # k_unsat and l_unsat consist of nodes of degree k and l that + # are unsaturated (nodes that have at least 1 available stub) + k_unsat = {v for v in k_nodes if h_node_residual[v] > 0} + + if k != l: + l_unsat = {w for w in l_nodes if h_node_residual[w] > 0} + else: + l_unsat = k_unsat + n_edges_add = joint_degrees[k][l] // 2 + + while n_edges_add > 0: + # randomly pick nodes v and w that have degrees k and l + v = k_nodes[seed.randrange(k_size)] + w = l_nodes[seed.randrange(l_size)] + + # if nodes v and w are disconnected then attempt to connect + if not G.has_edge(v, w) and (v != w): + # if node v has no free stubs then do neighbor switch + if h_node_residual[v] == 0: + _neighbor_switch(G, v, k_unsat, h_node_residual) + + # if node w has no free stubs then do neighbor switch + if h_node_residual[w] == 0: + if k != l: + _neighbor_switch(G, w, l_unsat, h_node_residual) + else: + _neighbor_switch( + G, w, l_unsat, h_node_residual, avoid_node_id=v + ) + + # add edge (v, w) and update data structures + G.add_edge(v, w) + h_node_residual[v] -= 1 + h_node_residual[w] -= 1 + n_edges_add -= 1 + + if h_node_residual[v] == 0: + k_unsat.discard(v) + if h_node_residual[w] == 0: + l_unsat.discard(w) + return G + + +@nx._dispatchable(graphs=None) +def is_valid_directed_joint_degree(in_degrees, out_degrees, nkk): + """Checks whether the given directed joint degree input is realizable + + Parameters + ---------- + in_degrees : list of integers + in degree sequence contains the in degrees of nodes. + out_degrees : list of integers + out degree sequence contains the out degrees of nodes. + nkk : dictionary of dictionary of integers + directed joint degree dictionary. for nodes of out degree k (first + level of dict) and nodes of in degree l (second level of dict) + describes the number of edges. + + Returns + ------- + boolean + returns true if given input is realizable, else returns false. + + Notes + ----- + Here is the list of conditions that the inputs (in/out degree sequences, + nkk) need to satisfy for simple directed graph realizability: + + - Condition 0: in_degrees and out_degrees have the same length + - Condition 1: nkk[k][l] is integer for all k,l + - Condition 2: sum(nkk[k])/k = number of nodes with partition id k, is an + integer and matching degree sequence + - Condition 3: number of edges and non-chords between k and l cannot exceed + maximum possible number of edges + + + References + ---------- + [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka, + "Construction of Directed 2K Graphs". In Proc. of KDD 2017. + """ + V = {} # number of nodes with in/out degree. + forbidden = {} + if len(in_degrees) != len(out_degrees): + return False + + for idx in range(len(in_degrees)): + i = in_degrees[idx] + o = out_degrees[idx] + V[(i, 0)] = V.get((i, 0), 0) + 1 + V[(o, 1)] = V.get((o, 1), 0) + 1 + + forbidden[(o, i)] = forbidden.get((o, i), 0) + 1 + + S = {} # number of edges going from in/out degree nodes. + for k in nkk: + for l in nkk[k]: + val = nkk[k][l] + if not float(val).is_integer(): # condition 1 + return False + + if val > 0: + S[(k, 1)] = S.get((k, 1), 0) + val + S[(l, 0)] = S.get((l, 0), 0) + val + # condition 3 + if val + forbidden.get((k, l), 0) > V[(k, 1)] * V[(l, 0)]: + return False + + return all(S[s] / s[0] == V[s] for s in S) + + +def _directed_neighbor_switch( + G, w, unsat, h_node_residual_out, chords, h_partition_in, partition +): + """Releases one free stub for node w, while preserving joint degree in G. + + Parameters + ---------- + G : networkx directed graph + graph within which the edge swap will take place. + w : integer + node id for which we need to perform a neighbor switch. + unsat: set of integers + set of node ids that have the same degree as w and are unsaturated. + h_node_residual_out: dict of integers + for a given node, keeps track of the remaining stubs to be added. + chords: set of tuples + keeps track of available positions to add edges. + h_partition_in: dict of integers + for a given node, keeps track of its partition id (in degree). + partition: integer + partition id to check if chords have to be updated. + + Notes + ----- + First, it selects node w_prime that (1) has the same degree as w and + (2) is unsaturated. Then, it selects node v, a neighbor of w, that is + not connected to w_prime and does an edge swap i.e. removes (w,v) and + adds (w_prime,v). If neighbor switch is not possible for w using + w_prime and v, then return w_prime; in [1] it's proven that + such unsaturated nodes can be used. + + References + ---------- + [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka, + "Construction of Directed 2K Graphs". In Proc. of KDD 2017. + """ + w_prime = unsat.pop() + unsat.add(w_prime) + # select node t, a neighbor of w, that is not connected to w_prime + w_neighbs = list(G.successors(w)) + # slightly faster declaring this variable + w_prime_neighbs = list(G.successors(w_prime)) + + for v in w_neighbs: + if (v not in w_prime_neighbs) and w_prime != v: + # removes (w,v), add (w_prime,v) and update data structures + G.remove_edge(w, v) + G.add_edge(w_prime, v) + + if h_partition_in[v] == partition: + chords.add((w, v)) + chords.discard((w_prime, v)) + + h_node_residual_out[w] += 1 + h_node_residual_out[w_prime] -= 1 + if h_node_residual_out[w_prime] == 0: + unsat.remove(w_prime) + return None + + # If neighbor switch didn't work, use unsaturated node + return w_prime + + +def _directed_neighbor_switch_rev( + G, w, unsat, h_node_residual_in, chords, h_partition_out, partition +): + """The reverse of directed_neighbor_switch. + + Parameters + ---------- + G : networkx directed graph + graph within which the edge swap will take place. + w : integer + node id for which we need to perform a neighbor switch. + unsat: set of integers + set of node ids that have the same degree as w and are unsaturated. + h_node_residual_in: dict of integers + for a given node, keeps track of the remaining stubs to be added. + chords: set of tuples + keeps track of available positions to add edges. + h_partition_out: dict of integers + for a given node, keeps track of its partition id (out degree). + partition: integer + partition id to check if chords have to be updated. + + Notes + ----- + Same operation as directed_neighbor_switch except it handles this operation + for incoming edges instead of outgoing. + """ + w_prime = unsat.pop() + unsat.add(w_prime) + # slightly faster declaring these as variables. + w_neighbs = list(G.predecessors(w)) + w_prime_neighbs = list(G.predecessors(w_prime)) + # select node v, a neighbor of w, that is not connected to w_prime. + for v in w_neighbs: + if (v not in w_prime_neighbs) and w_prime != v: + # removes (v,w), add (v,w_prime) and update data structures. + G.remove_edge(v, w) + G.add_edge(v, w_prime) + if h_partition_out[v] == partition: + chords.add((v, w)) + chords.discard((v, w_prime)) + + h_node_residual_in[w] += 1 + h_node_residual_in[w_prime] -= 1 + if h_node_residual_in[w_prime] == 0: + unsat.remove(w_prime) + return None + + # If neighbor switch didn't work, use the unsaturated node. + return w_prime + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def directed_joint_degree_graph(in_degrees, out_degrees, nkk, seed=None): + """Generates a random simple directed graph with the joint degree. + + Parameters + ---------- + degree_seq : list of tuples (of size 3) + degree sequence contains tuples of nodes with node id, in degree and + out degree. + nkk : dictionary of dictionary of integers + directed joint degree dictionary, for nodes of out degree k (first + level of dict) and nodes of in degree l (second level of dict) + describes the number of edges. + seed : hashable object, optional + Seed for random number generator. + + Returns + ------- + G : Graph + A directed graph with the specified inputs. + + Raises + ------ + NetworkXError + If degree_seq and nkk are not realizable as a simple directed graph. + + + Notes + ----- + Similarly to the undirected version: + In each iteration of the "while loop" the algorithm picks two disconnected + nodes v and w, of degree k and l correspondingly, for which nkk[k][l] has + not reached its target yet i.e. (for given k,l): n_edges_add < nkk[k][l]. + It then adds edge (v,w) and always increases the number of edges in graph G + by one. + + The intelligence of the algorithm lies in the fact that it is always + possible to add an edge between disconnected nodes v and w, for which + nkk[degree(v)][degree(w)] has not reached its target, even if one or both + nodes do not have free stubs. If either node v or w does not have a free + stub, we perform a "neighbor switch", an edge rewiring move that releases a + free stub while keeping nkk the same. + + The difference for the directed version lies in the fact that neighbor + switches might not be able to rewire, but in these cases unsaturated nodes + can be reassigned to use instead, see [1] for detailed description and + proofs. + + The algorithm continues for E (number of edges in the graph) iterations of + the "while loop", at which point all entries of the given nkk[k][l] have + reached their target values and the construction is complete. + + References + ---------- + [1] B. Tillman, A. Markopoulou, C. T. Butts & M. Gjoka, + "Construction of Directed 2K Graphs". In Proc. of KDD 2017. + + Examples + -------- + >>> in_degrees = [0, 1, 1, 2] + >>> out_degrees = [1, 1, 1, 1] + >>> nkk = {1: {1: 2, 2: 2}} + >>> G = nx.directed_joint_degree_graph(in_degrees, out_degrees, nkk) + >>> + """ + if not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk): + msg = "Input is not realizable as a simple graph" + raise nx.NetworkXError(msg) + + # start with an empty directed graph. + G = nx.DiGraph() + + # for a given group, keep the list of all node ids. + h_degree_nodelist_in = {} + h_degree_nodelist_out = {} + # for a given group, keep the list of all unsaturated node ids. + h_degree_nodelist_in_unsat = {} + h_degree_nodelist_out_unsat = {} + # for a given node, keep track of the remaining stubs to be added. + h_node_residual_out = {} + h_node_residual_in = {} + # for a given node, keep track of the partition id. + h_partition_out = {} + h_partition_in = {} + # keep track of non-chords between pairs of partition ids. + non_chords = {} + + # populate data structures + for idx, i in enumerate(in_degrees): + idx = int(idx) + if i > 0: + h_degree_nodelist_in.setdefault(i, []) + h_degree_nodelist_in_unsat.setdefault(i, set()) + h_degree_nodelist_in[i].append(idx) + h_degree_nodelist_in_unsat[i].add(idx) + h_node_residual_in[idx] = i + h_partition_in[idx] = i + + for idx, o in enumerate(out_degrees): + o = out_degrees[idx] + non_chords[(o, in_degrees[idx])] = non_chords.get((o, in_degrees[idx]), 0) + 1 + idx = int(idx) + if o > 0: + h_degree_nodelist_out.setdefault(o, []) + h_degree_nodelist_out_unsat.setdefault(o, set()) + h_degree_nodelist_out[o].append(idx) + h_degree_nodelist_out_unsat[o].add(idx) + h_node_residual_out[idx] = o + h_partition_out[idx] = o + + G.add_node(idx) + + nk_in = {} + nk_out = {} + for p in h_degree_nodelist_in: + nk_in[p] = len(h_degree_nodelist_in[p]) + for p in h_degree_nodelist_out: + nk_out[p] = len(h_degree_nodelist_out[p]) + + # iterate over every degree pair (k,l) and add the number of edges given + # for each pair. + for k in nkk: + for l in nkk[k]: + n_edges_add = nkk[k][l] + + if n_edges_add > 0: + # chords contains a random set of potential edges. + chords = set() + + k_len = nk_out[k] + l_len = nk_in[l] + chords_sample = seed.sample( + range(k_len * l_len), n_edges_add + non_chords.get((k, l), 0) + ) + + num = 0 + while len(chords) < n_edges_add: + i = h_degree_nodelist_out[k][chords_sample[num] % k_len] + j = h_degree_nodelist_in[l][chords_sample[num] // k_len] + num += 1 + if i != j: + chords.add((i, j)) + + # k_unsat and l_unsat consist of nodes of in/out degree k and l + # that are unsaturated i.e. those nodes that have at least one + # available stub + k_unsat = h_degree_nodelist_out_unsat[k] + l_unsat = h_degree_nodelist_in_unsat[l] + + while n_edges_add > 0: + v, w = chords.pop() + chords.add((v, w)) + + # if node v has no free stubs then do neighbor switch. + if h_node_residual_out[v] == 0: + _v = _directed_neighbor_switch( + G, + v, + k_unsat, + h_node_residual_out, + chords, + h_partition_in, + l, + ) + if _v is not None: + v = _v + + # if node w has no free stubs then do neighbor switch. + if h_node_residual_in[w] == 0: + _w = _directed_neighbor_switch_rev( + G, + w, + l_unsat, + h_node_residual_in, + chords, + h_partition_out, + k, + ) + if _w is not None: + w = _w + + # add edge (v,w) and update data structures. + G.add_edge(v, w) + h_node_residual_out[v] -= 1 + h_node_residual_in[w] -= 1 + n_edges_add -= 1 + chords.discard((v, w)) + + if h_node_residual_out[v] == 0: + k_unsat.discard(v) + if h_node_residual_in[w] == 0: + l_unsat.discard(w) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/lattice.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/lattice.py new file mode 100644 index 0000000000000000000000000000000000000000..61721c4317995228209305de245c74d64d8095f4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/lattice.py @@ -0,0 +1,405 @@ +"""Functions for generating grid graphs and lattices + +The :func:`grid_2d_graph`, :func:`triangular_lattice_graph`, and +:func:`hexagonal_lattice_graph` functions correspond to the three +`regular tilings of the plane`_, the square, triangular, and hexagonal +tilings, respectively. :func:`grid_graph` and :func:`hypercube_graph` +are similar for arbitrary dimensions. Useful relevant discussion can +be found about `Triangular Tiling`_, and `Square, Hex and Triangle Grids`_ + +.. _regular tilings of the plane: https://en.wikipedia.org/wiki/List_of_regular_polytopes_and_compounds#Euclidean_tilings +.. _Square, Hex and Triangle Grids: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/ +.. _Triangular Tiling: https://en.wikipedia.org/wiki/Triangular_tiling + +""" + +from itertools import repeat +from math import sqrt + +import networkx as nx +from networkx.classes import set_node_attributes +from networkx.exception import NetworkXError +from networkx.generators.classic import cycle_graph, empty_graph, path_graph +from networkx.relabel import relabel_nodes +from networkx.utils import flatten, nodes_or_number, pairwise + +__all__ = [ + "grid_2d_graph", + "grid_graph", + "hypercube_graph", + "triangular_lattice_graph", + "hexagonal_lattice_graph", +] + + +@nx._dispatchable(graphs=None, returns_graph=True) +@nodes_or_number([0, 1]) +def grid_2d_graph(m, n, periodic=False, create_using=None): + """Returns the two-dimensional grid graph. + + The grid graph has each node connected to its four nearest neighbors. + + Parameters + ---------- + m, n : int or iterable container of nodes + If an integer, nodes are from `range(n)`. + If a container, elements become the coordinate of the nodes. + + periodic : bool or iterable + If `periodic` is True, both dimensions are periodic. If False, none + are periodic. If `periodic` is iterable, it should yield 2 bool + values indicating whether the 1st and 2nd axes, respectively, are + periodic. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + The (possibly periodic) grid graph of the specified dimensions. + + See Also + -------- + triangular_lattice_graph, hexagonal_lattice_graph : + Other 2D lattice graphs + grid_graph, hypercube_graph : + N-dimensional lattice graphs + """ + G = empty_graph(0, create_using) + row_name, rows = m + col_name, cols = n + G.add_nodes_from((i, j) for i in rows for j in cols) + G.add_edges_from(((i, j), (pi, j)) for pi, i in pairwise(rows) for j in cols) + G.add_edges_from(((i, j), (i, pj)) for i in rows for pj, j in pairwise(cols)) + + try: + periodic_r, periodic_c = periodic + except TypeError: + periodic_r = periodic_c = periodic + + if periodic_r and len(rows) > 2: + first = rows[0] + last = rows[-1] + G.add_edges_from(((first, j), (last, j)) for j in cols) + if periodic_c and len(cols) > 2: + first = cols[0] + last = cols[-1] + G.add_edges_from(((i, first), (i, last)) for i in rows) + # both directions for directed + if G.is_directed(): + G.add_edges_from((v, u) for u, v in G.edges()) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def grid_graph(dim, periodic=False): + """Returns the *n*-dimensional grid graph. + + The dimension *n* is the length of the list `dim` and the size in + each dimension is the value of the corresponding list element. + + Parameters + ---------- + dim : list or tuple of numbers or iterables of nodes + 'dim' is a tuple or list with, for each dimension, either a number + that is the size of that dimension or an iterable of nodes for + that dimension. The dimension of the grid_graph is the length + of `dim`. + + periodic : bool or iterable + If `periodic` is True, all dimensions are periodic. If False all + dimensions are not periodic. If `periodic` is iterable, it should + yield `dim` bool values each of which indicates whether the + corresponding axis is periodic. + + Returns + ------- + NetworkX graph + The (possibly periodic) grid graph of the specified dimensions. + + See Also + -------- + grid_2d_graph, triangular_lattice_graph, hexagonal_lattice_graph : + 2D lattice graphs + hypercube_graph : + A special case of `grid_graph` where all elements of `dim` are identical + + Examples + -------- + To produce a 2 by 3 by 4 grid graph, a graph on 24 nodes: + + >>> from networkx import grid_graph + >>> G = grid_graph(dim=(2, 3, 4)) + >>> len(G) + 24 + >>> G = grid_graph(dim=(range(7, 9), range(3, 6))) + >>> len(G) + 6 + """ + from collections.abc import Iterable + + from networkx.algorithms.operators.product import cartesian_product + + if not dim: + return empty_graph(0) + + periodic = repeat(periodic) if not isinstance(periodic, Iterable) else periodic + func = (cycle_graph if p else path_graph for p in periodic) + + G = next(func)(dim[0]) + for current_dim in dim[1:]: + Gnew = next(func)(current_dim) + G = cartesian_product(Gnew, G) + # graph G is done but has labels of the form (1, (2, (3, 1))) so relabel + H = relabel_nodes(G, flatten) + return H + + +@nx._dispatchable(graphs=None, returns_graph=True) +def hypercube_graph(n): + """Returns the *n*-dimensional hypercube graph. + + The *n*-dimensional hypercube graph [1]_ has ``2**n`` nodes, each represented as + a binary integer in the form of a tuple of 0's and 1's. Edges exist between + nodes that differ in exactly one bit. + + Parameters + ---------- + n : int + Dimension of the hypercube, must be a positive integer. + + Returns + ------- + networkx.Graph + The n-dimensional hypercube graph as an undirected graph. + + See Also + -------- + grid_2d_graph, triangular_lattice_graph, hexagonal_lattice_graph : + 2D lattice graphs + grid_graph : + A more general N-dimensional grid + + Examples + -------- + >>> G = nx.hypercube_graph(3) + >>> list(G.neighbors((0, 0, 0))) + [(1, 0, 0), (0, 1, 0), (0, 0, 1)] + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Hypercube_graph + """ + dim = n * [2] + G = grid_graph(dim) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def triangular_lattice_graph( + m, n, periodic=False, with_positions=True, create_using=None +): + r"""Returns the $m$ by $n$ triangular lattice graph. + + The `triangular lattice graph`_ is a two-dimensional `grid graph`_ in + which each square unit has a diagonal edge (each grid unit has a chord). + + The returned graph has $m$ rows and $n$ columns of triangles. Rows and + columns include both triangles pointing up and down. Rows form a strip + of constant height. Columns form a series of diamond shapes, staggered + with the columns on either side. Another way to state the size is that + the nodes form a grid of `m+1` rows and `(n + 1) // 2` columns. + The odd row nodes are shifted horizontally relative to the even rows. + + Directed graph types have edges pointed up or right. + + Positions of nodes are computed by default or `with_positions is True`. + The position of each node (embedded in a euclidean plane) is stored in + the graph using equilateral triangles with sidelength 1. + The height between rows of nodes is thus $\sqrt(3)/2$. + Nodes lie in the first quadrant with the node $(0, 0)$ at the origin. + + .. _triangular lattice graph: http://mathworld.wolfram.com/TriangularGrid.html + .. _grid graph: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/ + .. _Triangular Tiling: https://en.wikipedia.org/wiki/Triangular_tiling + + Parameters + ---------- + m : int + The number of rows in the lattice. + + n : int + The number of columns in the lattice. + + periodic : bool (default: False) + If True, join the boundary vertices of the grid using periodic + boundary conditions. The join between boundaries is the final row + and column of triangles. This means there is one row and one column + fewer nodes for the periodic lattice. Periodic lattices require + `m >= 3`, `n >= 5` and are allowed but misaligned if `m` or `n` are odd + + with_positions : bool (default: True) + Store the coordinates of each node in the graph node attribute 'pos'. + The coordinates provide a lattice with equilateral triangles. + Periodic positions shift the nodes vertically in a nonlinear way so + the edges don't overlap so much. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + NetworkX graph + The *m* by *n* triangular lattice graph. + + See Also + -------- + grid_2d_graph, hexagonal_lattice_graph : + Other 2D lattice graphs + grid_graph, hypercube_graph : + N-dimensional lattice graphs + """ + H = empty_graph(0, create_using) + if n == 0 or m == 0: + return H + if periodic: + if n < 5 or m < 3: + msg = f"m > 2 and n > 4 required for periodic. m={m}, n={n}" + raise NetworkXError(msg) + + N = (n + 1) // 2 # number of nodes in row + rows = range(m + 1) + cols = range(N + 1) + # Make grid + H.add_edges_from(((i, j), (i + 1, j)) for j in rows for i in cols[:N]) + H.add_edges_from(((i, j), (i, j + 1)) for j in rows[:m] for i in cols) + # add diagonals + H.add_edges_from(((i, j), (i + 1, j + 1)) for j in rows[1:m:2] for i in cols[:N]) + H.add_edges_from(((i + 1, j), (i, j + 1)) for j in rows[:m:2] for i in cols[:N]) + + # identify boundary nodes if periodic + if periodic is True: + for i in cols: + H = nx.contracted_nodes(H, (i, 0), (i, m), store_contraction_as=None) + for j in rows[:m]: + H = nx.contracted_nodes(H, (0, j), (N, j), store_contraction_as=None) + elif n % 2: + # remove extra nodes + H.remove_nodes_from((N, j) for j in rows[1::2]) + + # Add position node attributes + if with_positions: + ii = (i for i in cols for j in rows) + jj = (j for i in cols for j in rows) + xx = (0.5 * (j % 2) + i for i in cols for j in rows) + h = sqrt(3) / 2 + if periodic: + yy = (h * j + 0.01 * i * i for i in cols for j in rows) + else: + yy = (h * j for i in cols for j in rows) + pos = {(i, j): (x, y) for i, j, x, y in zip(ii, jj, xx, yy) if (i, j) in H} + set_node_attributes(H, pos, "pos") + return H + + +@nx._dispatchable(graphs=None, returns_graph=True) +def hexagonal_lattice_graph( + m, n, periodic=False, with_positions=True, create_using=None +): + """Returns an `m` by `n` hexagonal lattice graph. + + The *hexagonal lattice graph* is a graph whose nodes and edges are + the `hexagonal tiling`_ of the plane. + + The returned graph will have `m` rows and `n` columns of hexagons. + `Odd numbered columns`_ are shifted up relative to even numbered columns. + + Positions of nodes are computed by default or `with_positions is True`. + Node positions creating the standard embedding in the plane + with sidelength 1 and are stored in the node attribute 'pos'. + `pos = nx.get_node_attributes(G, 'pos')` creates a dict ready for drawing. + + .. _hexagonal tiling: https://en.wikipedia.org/wiki/Hexagonal_tiling + .. _Odd numbered columns: http://www-cs-students.stanford.edu/~amitp/game-programming/grids/ + + Parameters + ---------- + m : int + The number of rows of hexagons in the lattice. + + n : int + The number of columns of hexagons in the lattice. + + periodic : bool + Whether to make a periodic grid by joining the boundary vertices. + For this to work `n` must be even and both `n > 1` and `m > 1`. + The periodic connections create another row and column of hexagons + so these graphs have fewer nodes as boundary nodes are identified. + + with_positions : bool (default: True) + Store the coordinates of each node in the graph node attribute 'pos'. + The coordinates provide a lattice with vertical columns of hexagons + offset to interleave and cover the plane. + Periodic positions shift the nodes vertically in a nonlinear way so + the edges don't overlap so much. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + If graph is directed, edges will point up or right. + + Returns + ------- + NetworkX graph + The *m* by *n* hexagonal lattice graph. + + See Also + -------- + grid_2d_graph, triangular_lattice_graph : + Other 2D lattice graphs + grid_graph, hypercube_graph : + N-dimensional lattice graphs + """ + G = empty_graph(0, create_using) + if m == 0 or n == 0: + return G + if periodic and (n % 2 == 1 or m < 2 or n < 2): + msg = "periodic hexagonal lattice needs m > 1, n > 1 and even n" + raise NetworkXError(msg) + + M = 2 * m # twice as many nodes as hexagons vertically + rows = range(M + 2) + cols = range(n + 1) + # make lattice + col_edges = (((i, j), (i, j + 1)) for i in cols for j in rows[: M + 1]) + row_edges = (((i, j), (i + 1, j)) for i in cols[:n] for j in rows if i % 2 == j % 2) + G.add_edges_from(col_edges) + G.add_edges_from(row_edges) + # Remove corner nodes with one edge + G.remove_node((0, M + 1)) + G.remove_node((n, (M + 1) * (n % 2))) + + # identify boundary nodes if periodic + if periodic: + for i in cols[:n]: + G = nx.contracted_nodes(G, (i, 0), (i, M), store_contraction_as=None) + for i in cols[1:]: + G = nx.contracted_nodes(G, (i, 1), (i, M + 1), store_contraction_as=None) + for j in rows[1:M]: + G = nx.contracted_nodes(G, (0, j), (n, j), store_contraction_as=None) + G.remove_node((n, M)) + + # calc position in embedded space + if with_positions: + ii = (i for i in cols for j in rows) + jj = (j for i in cols for j in rows) + xx = (0.5 + i + i // 2 + (j % 2) * ((i % 2) - 0.5) for i in cols for j in rows) + h = sqrt(3) / 2 + if periodic: + yy = (h * j + 0.01 * i * i for i in cols for j in rows) + else: + yy = (h * j for i in cols for j in rows) + # exclude nodes not in G + pos = {(i, j): (x, y) for i, j, x, y in zip(ii, jj, xx, yy) if (i, j) in G} + set_node_attributes(G, pos, "pos") + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/line.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/line.py new file mode 100644 index 0000000000000000000000000000000000000000..90997146bde4a04b4b53c475e300c4a63b987f38 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/line.py @@ -0,0 +1,501 @@ +"""Functions for generating line graphs.""" + +from collections import defaultdict +from functools import partial +from itertools import combinations + +import networkx as nx +from networkx.utils import arbitrary_element +from networkx.utils.decorators import not_implemented_for + +__all__ = ["line_graph", "inverse_line_graph"] + + +@nx._dispatchable(returns_graph=True) +def line_graph(G, create_using=None): + r"""Returns the line graph of the graph or digraph `G`. + + The line graph of a graph `G` has a node for each edge in `G` and an + edge joining those nodes if the two edges in `G` share a common node. For + directed graphs, nodes are adjacent exactly when the edges they represent + form a directed path of length two. + + The nodes of the line graph are 2-tuples of nodes in the original graph (or + 3-tuples for multigraphs, with the key of the edge as the third element). + + For information about self-loops and more discussion, see the **Notes** + section below. + + Parameters + ---------- + G : graph + A NetworkX Graph, DiGraph, MultiGraph, or MultiDigraph. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + L : graph + The line graph of G. + + Examples + -------- + >>> G = nx.star_graph(3) + >>> L = nx.line_graph(G) + >>> print(sorted(map(sorted, L.edges()))) # makes a 3-clique, K3 + [[(0, 1), (0, 2)], [(0, 1), (0, 3)], [(0, 2), (0, 3)]] + + Edge attributes from `G` are not copied over as node attributes in `L`, but + attributes can be copied manually: + + >>> G = nx.path_graph(4) + >>> G.add_edges_from((u, v, {"tot": u + v}) for u, v in G.edges) + >>> G.edges(data=True) + EdgeDataView([(0, 1, {'tot': 1}), (1, 2, {'tot': 3}), (2, 3, {'tot': 5})]) + >>> H = nx.line_graph(G) + >>> H.add_nodes_from((node, G.edges[node]) for node in H) + >>> H.nodes(data=True) + NodeDataView({(0, 1): {'tot': 1}, (2, 3): {'tot': 5}, (1, 2): {'tot': 3}}) + + Notes + ----- + Graph, node, and edge data are not propagated to the new graph. For + undirected graphs, the nodes in G must be sortable, otherwise the + constructed line graph may not be correct. + + *Self-loops in undirected graphs* + + For an undirected graph `G` without multiple edges, each edge can be + written as a set `\{u, v\}`. Its line graph `L` has the edges of `G` as + its nodes. If `x` and `y` are two nodes in `L`, then `\{x, y\}` is an edge + in `L` if and only if the intersection of `x` and `y` is nonempty. Thus, + the set of all edges is determined by the set of all pairwise intersections + of edges in `G`. + + Trivially, every edge in G would have a nonzero intersection with itself, + and so every node in `L` should have a self-loop. This is not so + interesting, and the original context of line graphs was with simple + graphs, which had no self-loops or multiple edges. The line graph was also + meant to be a simple graph and thus, self-loops in `L` are not part of the + standard definition of a line graph. In a pairwise intersection matrix, + this is analogous to excluding the diagonal entries from the line graph + definition. + + Self-loops and multiple edges in `G` add nodes to `L` in a natural way, and + do not require any fundamental changes to the definition. It might be + argued that the self-loops we excluded before should now be included. + However, the self-loops are still "trivial" in some sense and thus, are + usually excluded. + + *Self-loops in directed graphs* + + For a directed graph `G` without multiple edges, each edge can be written + as a tuple `(u, v)`. Its line graph `L` has the edges of `G` as its + nodes. If `x` and `y` are two nodes in `L`, then `(x, y)` is an edge in `L` + if and only if the tail of `x` matches the head of `y`, for example, if `x + = (a, b)` and `y = (b, c)` for some vertices `a`, `b`, and `c` in `G`. + + Due to the directed nature of the edges, it is no longer the case that + every edge in `G` should have a self-loop in `L`. Now, the only time + self-loops arise is if a node in `G` itself has a self-loop. So such + self-loops are no longer "trivial" but instead, represent essential + features of the topology of `G`. For this reason, the historical + development of line digraphs is such that self-loops are included. When the + graph `G` has multiple edges, once again only superficial changes are + required to the definition. + + References + ---------- + * Harary, Frank, and Norman, Robert Z., "Some properties of line digraphs", + Rend. Circ. Mat. Palermo, II. Ser. 9 (1960), 161--168. + * Hemminger, R. L.; Beineke, L. W. (1978), "Line graphs and line digraphs", + in Beineke, L. W.; Wilson, R. J., Selected Topics in Graph Theory, + Academic Press Inc., pp. 271--305. + + """ + if G.is_directed(): + L = _lg_directed(G, create_using=create_using) + else: + L = _lg_undirected(G, selfloops=False, create_using=create_using) + return L + + +def _lg_directed(G, create_using=None): + """Returns the line graph L of the (multi)digraph G. + + Edges in G appear as nodes in L, represented as tuples of the form (u,v) + or (u,v,key) if G is a multidigraph. A node in L corresponding to the edge + (u,v) is connected to every node corresponding to an edge (v,w). + + Parameters + ---------- + G : digraph + A directed graph or directed multigraph. + create_using : NetworkX graph constructor, optional + Graph type to create. If graph instance, then cleared before populated. + Default is to use the same graph class as `G`. + + """ + L = nx.empty_graph(0, create_using, default=G.__class__) + + # Create a graph specific edge function. + get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges + + for from_node in get_edges(): + # from_node is: (u,v) or (u,v,key) + L.add_node(from_node) + for to_node in get_edges(from_node[1]): + L.add_edge(from_node, to_node) + + return L + + +def _lg_undirected(G, selfloops=False, create_using=None): + """Returns the line graph L of the (multi)graph G. + + Edges in G appear as nodes in L, represented as sorted tuples of the form + (u,v), or (u,v,key) if G is a multigraph. A node in L corresponding to + the edge {u,v} is connected to every node corresponding to an edge that + involves u or v. + + Parameters + ---------- + G : graph + An undirected graph or multigraph. + selfloops : bool + If `True`, then self-loops are included in the line graph. If `False`, + they are excluded. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Notes + ----- + The standard algorithm for line graphs of undirected graphs does not + produce self-loops. + + """ + L = nx.empty_graph(0, create_using, default=G.__class__) + + # Graph specific functions for edges. + get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges + + # Determine if we include self-loops or not. + shift = 0 if selfloops else 1 + + # Introduce numbering of nodes + node_index = {n: i for i, n in enumerate(G)} + + # Lift canonical representation of nodes to edges in line graph + def edge_key_function(edge): + return node_index[edge[0]], node_index[edge[1]] + + edges = set() + for u in G: + # Label nodes as a sorted tuple of nodes in original graph. + # Decide on representation of {u, v} as (u, v) or (v, u) depending on node_index. + # -> This ensures a canonical representation and avoids comparing values of different types. + nodes = [tuple(sorted(x[:2], key=node_index.get)) + x[2:] for x in get_edges(u)] + + if len(nodes) == 1: + # Then the edge will be an isolated node in L. + L.add_node(nodes[0]) + + # Add a clique of `nodes` to graph. To prevent double adding edges, + # especially important for multigraphs, we store the edges in + # canonical form in a set. + for i, a in enumerate(nodes): + edges.update( + [ + tuple(sorted((a, b), key=edge_key_function)) + for b in nodes[i + shift :] + ] + ) + + L.add_edges_from(edges) + return L + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(returns_graph=True) +def inverse_line_graph(G): + """Returns the inverse line graph of graph G. + + If H is a graph, and G is the line graph of H, such that G = L(H). + Then H is the inverse line graph of G. + + Not all graphs are line graphs and these do not have an inverse line graph. + In these cases this function raises a NetworkXError. + + Parameters + ---------- + G : graph + A NetworkX Graph + + Returns + ------- + H : graph + The inverse line graph of G. + + Raises + ------ + NetworkXNotImplemented + If G is directed or a multigraph + + NetworkXError + If G is not a line graph + + Notes + ----- + This is an implementation of the Roussopoulos algorithm[1]_. + + If G consists of multiple components, then the algorithm doesn't work. + You should invert every component separately: + + >>> K5 = nx.complete_graph(5) + >>> P4 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")]) + >>> G = nx.union(K5, P4) + >>> root_graphs = [] + >>> for comp in nx.connected_components(G): + ... root_graphs.append(nx.inverse_line_graph(G.subgraph(comp))) + >>> len(root_graphs) + 2 + + References + ---------- + .. [1] Roussopoulos, N.D. , "A max {m, n} algorithm for determining the graph H from + its line graph G", Information Processing Letters 2, (1973), 108--112, ISSN 0020-0190, + `DOI link `_ + + """ + if G.number_of_nodes() == 0: + return nx.empty_graph(1) + elif G.number_of_nodes() == 1: + v = arbitrary_element(G) + a = (v, 0) + b = (v, 1) + H = nx.Graph([(a, b)]) + return H + elif G.number_of_nodes() > 1 and G.number_of_edges() == 0: + msg = ( + "inverse_line_graph() doesn't work on an edgeless graph. " + "Please use this function on each component separately." + ) + raise nx.NetworkXError(msg) + + if nx.number_of_selfloops(G) != 0: + msg = ( + "A line graph as generated by NetworkX has no selfloops, so G has no " + "inverse line graph. Please remove the selfloops from G and try again." + ) + raise nx.NetworkXError(msg) + + starting_cell = _select_starting_cell(G) + P = _find_partition(G, starting_cell) + # count how many times each vertex appears in the partition set + P_count = {u: 0 for u in G.nodes} + for p in P: + for u in p: + P_count[u] += 1 + + if max(P_count.values()) > 2: + msg = "G is not a line graph (vertex found in more than two partition cells)" + raise nx.NetworkXError(msg) + W = tuple((u,) for u in P_count if P_count[u] == 1) + H = nx.Graph() + H.add_nodes_from(P) + H.add_nodes_from(W) + for a, b in combinations(H.nodes, 2): + if any(a_bit in b for a_bit in a): + H.add_edge(a, b) + return H + + +def _triangles(G, e): + """Return list of all triangles containing edge e""" + u, v = e + if u not in G: + raise nx.NetworkXError(f"Vertex {u} not in graph") + if v not in G[u]: + raise nx.NetworkXError(f"Edge ({u}, {v}) not in graph") + triangle_list = [] + for x in G[u]: + if x in G[v]: + triangle_list.append((u, v, x)) + return triangle_list + + +def _odd_triangle(G, T): + """Test whether T is an odd triangle in G + + Parameters + ---------- + G : NetworkX Graph + T : 3-tuple of vertices forming triangle in G + + Returns + ------- + True is T is an odd triangle + False otherwise + + Raises + ------ + NetworkXError + T is not a triangle in G + + Notes + ----- + An odd triangle is one in which there exists another vertex in G which is + adjacent to either exactly one or exactly all three of the vertices in the + triangle. + + """ + for u in T: + if u not in G.nodes(): + raise nx.NetworkXError(f"Vertex {u} not in graph") + for e in list(combinations(T, 2)): + if e[0] not in G[e[1]]: + raise nx.NetworkXError(f"Edge ({e[0]}, {e[1]}) not in graph") + + T_nbrs = defaultdict(int) + for t in T: + for v in G[t]: + if v not in T: + T_nbrs[v] += 1 + return any(T_nbrs[v] in [1, 3] for v in T_nbrs) + + +def _find_partition(G, starting_cell): + """Find a partition of the vertices of G into cells of complete graphs + + Parameters + ---------- + G : NetworkX Graph + starting_cell : tuple of vertices in G which form a cell + + Returns + ------- + List of tuples of vertices of G + + Raises + ------ + NetworkXError + If a cell is not a complete subgraph then G is not a line graph + """ + G_partition = G.copy() + P = [starting_cell] # partition set + G_partition.remove_edges_from(list(combinations(starting_cell, 2))) + # keep list of partitioned nodes which might have an edge in G_partition + partitioned_vertices = list(starting_cell) + while G_partition.number_of_edges() > 0: + # there are still edges left and so more cells to be made + u = partitioned_vertices.pop() + deg_u = len(G_partition[u]) + if deg_u != 0: + # if u still has edges then we need to find its other cell + # this other cell must be a complete subgraph or else G is + # not a line graph + new_cell = [u] + list(G_partition[u]) + for u in new_cell: + for v in new_cell: + if (u != v) and (v not in G_partition[u]): + msg = ( + "G is not a line graph " + "(partition cell not a complete subgraph)" + ) + raise nx.NetworkXError(msg) + P.append(tuple(new_cell)) + G_partition.remove_edges_from(list(combinations(new_cell, 2))) + partitioned_vertices += new_cell + return P + + +def _select_starting_cell(G, starting_edge=None): + """Select a cell to initiate _find_partition + + Parameters + ---------- + G : NetworkX Graph + starting_edge: an edge to build the starting cell from + + Returns + ------- + Tuple of vertices in G + + Raises + ------ + NetworkXError + If it is determined that G is not a line graph + + Notes + ----- + If starting edge not specified then pick an arbitrary edge - doesn't + matter which. However, this function may call itself requiring a + specific starting edge. Note that the r, s notation for counting + triangles is the same as in the Roussopoulos paper cited above. + """ + if starting_edge is None: + e = arbitrary_element(G.edges()) + else: + e = starting_edge + if e[0] not in G.nodes(): + raise nx.NetworkXError(f"Vertex {e[0]} not in graph") + if e[1] not in G[e[0]]: + msg = f"starting_edge ({e[0]}, {e[1]}) is not in the Graph" + raise nx.NetworkXError(msg) + e_triangles = _triangles(G, e) + r = len(e_triangles) + if r == 0: + # there are no triangles containing e, so the starting cell is just e + starting_cell = e + elif r == 1: + # there is exactly one triangle, T, containing e. If other 2 edges + # of T belong only to this triangle then T is starting cell + T = e_triangles[0] + a, b, c = T + # ab was original edge so check the other 2 edges + ac_edges = len(_triangles(G, (a, c))) + bc_edges = len(_triangles(G, (b, c))) + if ac_edges == 1: + if bc_edges == 1: + starting_cell = T + else: + return _select_starting_cell(G, starting_edge=(b, c)) + else: + return _select_starting_cell(G, starting_edge=(a, c)) + else: + # r >= 2 so we need to count the number of odd triangles, s + s = 0 + odd_triangles = [] + for T in e_triangles: + if _odd_triangle(G, T): + s += 1 + odd_triangles.append(T) + if r == 2 and s == 0: + # in this case either triangle works, so just use T + starting_cell = T + elif r - 1 <= s <= r: + # check if odd triangles containing e form complete subgraph + triangle_nodes = set() + for T in odd_triangles: + for x in T: + triangle_nodes.add(x) + + for u in triangle_nodes: + for v in triangle_nodes: + if u != v and (v not in G[u]): + msg = ( + "G is not a line graph (odd triangles " + "do not form complete subgraph)" + ) + raise nx.NetworkXError(msg) + # otherwise then we can use this as the starting cell + starting_cell = tuple(triangle_nodes) + + else: + msg = ( + "G is not a line graph (incorrect number of " + "odd triangles around starting edge)" + ) + raise nx.NetworkXError(msg) + return starting_cell diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/mycielski.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/mycielski.py new file mode 100644 index 0000000000000000000000000000000000000000..804b903692853d3c45b3b1b20898efeee9b71a5e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/mycielski.py @@ -0,0 +1,110 @@ +"""Functions related to the Mycielski Operation and the Mycielskian family +of graphs. + +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["mycielskian", "mycielski_graph"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(returns_graph=True) +def mycielskian(G, iterations=1): + r"""Returns the Mycielskian of a simple, undirected graph G + + The Mycielskian of graph preserves a graph's triangle free + property while increasing the chromatic number by 1. + + The Mycielski Operation on a graph, :math:`G=(V, E)`, constructs a new + graph with :math:`2|V| + 1` nodes and :math:`3|E| + |V|` edges. + + The construction is as follows: + + Let :math:`V = {0, ..., n-1}`. Construct another vertex set + :math:`U = {n, ..., 2n}` and a vertex, `w`. + Construct a new graph, `M`, with vertices :math:`U \bigcup V \bigcup w`. + For edges, :math:`(u, v) \in E` add edges :math:`(u, v), (u, v + n)`, and + :math:`(u + n, v)` to M. Finally, for all vertices :math:`u \in U`, add + edge :math:`(u, w)` to M. + + The Mycielski Operation can be done multiple times by repeating the above + process iteratively. + + More information can be found at https://en.wikipedia.org/wiki/Mycielskian + + Parameters + ---------- + G : graph + A simple, undirected NetworkX graph + iterations : int + The number of iterations of the Mycielski operation to + perform on G. Defaults to 1. Must be a non-negative integer. + + Returns + ------- + M : graph + The Mycielskian of G after the specified number of iterations. + + Notes + ----- + Graph, node, and edge data are not necessarily propagated to the new graph. + + """ + + M = nx.convert_node_labels_to_integers(G) + + for i in range(iterations): + n = M.number_of_nodes() + M.add_nodes_from(range(n, 2 * n)) + old_edges = list(M.edges()) + M.add_edges_from((u, v + n) for u, v in old_edges) + M.add_edges_from((u + n, v) for u, v in old_edges) + M.add_node(2 * n) + M.add_edges_from((u + n, 2 * n) for u in range(n)) + + return M + + +@nx._dispatchable(graphs=None, returns_graph=True) +def mycielski_graph(n): + """Generator for the n_th Mycielski Graph. + + The Mycielski family of graphs is an infinite set of graphs. + :math:`M_1` is the singleton graph, :math:`M_2` is two vertices with an + edge, and, for :math:`i > 2`, :math:`M_i` is the Mycielskian of + :math:`M_{i-1}`. + + More information can be found at + http://mathworld.wolfram.com/MycielskiGraph.html + + Parameters + ---------- + n : int + The desired Mycielski Graph. + + Returns + ------- + M : graph + The n_th Mycielski Graph + + Notes + ----- + The first graph in the Mycielski sequence is the singleton graph. + The Mycielskian of this graph is not the :math:`P_2` graph, but rather the + :math:`P_2` graph with an extra, isolated vertex. The second Mycielski + graph is the :math:`P_2` graph, so the first two are hard coded. + The remaining graphs are generated using the Mycielski operation. + + """ + + if n < 1: + raise nx.NetworkXError("must satisfy n >= 1") + + if n == 1: + return nx.empty_graph(1) + + else: + return mycielskian(nx.path_graph(2), n - 2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py new file mode 100644 index 0000000000000000000000000000000000000000..6a0f0c996d8915b07b5f87c4e14359bc642b2929 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/nonisomorphic_trees.py @@ -0,0 +1,259 @@ +""" +Implementation of the Wright, Richmond, Odlyzko and McKay (WROM) +algorithm for the enumeration of all non-isomorphic free trees of a +given order. Rooted trees are represented by level sequences, i.e., +lists in which the i-th element specifies the distance of vertex i to +the root. + +""" + +__all__ = ["nonisomorphic_trees", "number_of_nonisomorphic_trees"] + +from functools import lru_cache + +import networkx as nx + + +@nx._dispatchable(graphs=None, returns_graph=True) +def nonisomorphic_trees(order): + """Generate nonisomorphic trees of specified `order`. + + Parameters + ---------- + order : int + order of the desired tree(s) + + Yields + ------ + `networkx.Graph` instances + A tree with `order` number of nodes that is not isomorphic to any other + yielded tree. + + Raises + ------ + ValueError + If `order` is negative. + + Examples + -------- + There are 11 unique (non-isomorphic) trees with 7 nodes. + + >>> n = 7 + >>> nit_list = list(nx.nonisomorphic_trees(n)) + >>> len(nit_list) == nx.number_of_nonisomorphic_trees(n) == 11 + True + + All trees yielded by the generator have the specified order. + + >>> all(len(G) == n for G in nx.nonisomorphic_trees(n)) + True + + Each tree is nonisomorphic to every other tree yielded by the generator. + >>> seen = [] + >>> for G in nx.nonisomorphic_trees(n): + ... assert not any(nx.is_isomorphic(G, H) for H in seen) + ... seen.append(G) + + See Also + -------- + number_of_nonisomorphic_trees + """ + if order < 0: + raise ValueError("order must be non-negative") + if order == 0: + # Idiom for empty generator, i.e. list(nonisomorphic_trees(0)) == [] + return + yield + if order == 1: + yield nx.empty_graph(1) + return + # start at the path graph rooted at its center + layout = list(range(order // 2 + 1)) + list(range(1, (order + 1) // 2)) + + while layout is not None: + layout = _next_tree(layout) + if layout is not None: + yield _layout_to_graph(layout) + layout = _next_rooted_tree(layout) + + +@nx._dispatchable(graphs=None) +def number_of_nonisomorphic_trees(order): + """Returns the number of nonisomorphic trees of the specified `order`. + + Based on an algorithm by Alois P. Heinz in + `OEIS entry A000055 `_. Complexity is ``O(n ** 3)``. + + Parameters + ---------- + order : int + Order of the desired tree(s). + + Returns + ------- + int + Number of nonisomorphic trees with `order` number of nodes. + + Raises + ------ + ValueError + If `order` is negative. + + Examples + -------- + >>> nx.number_of_nonisomorphic_trees(10) + 106 + + See Also + -------- + nonisomorphic_trees + """ + if order < 0: + raise ValueError("order must be non-negative") + return _unlabeled_trees(order) + + +@lru_cache(None) +def _unlabeled_trees(n): + """Implements OEIS A000055 (number of unlabeled trees).""" + + value = 0 + for k in range(n + 1): + value += _rooted_trees(k) * _rooted_trees(n - k) + if n % 2 == 0: + value -= _rooted_trees(n // 2) + return _rooted_trees(n) - value // 2 + + +@lru_cache(None) +def _rooted_trees(n): + """Implements OEIS A000081 (number of unlabeled rooted trees).""" + + if n < 2: + return n + value = 0 + for j in range(1, n): + for d in range(1, n): + if j % d == 0: + value += d * _rooted_trees(d) * _rooted_trees(n - j) + return value // (n - 1) + + +def _next_rooted_tree(predecessor, p=None): + """One iteration of the Beyer-Hedetniemi algorithm.""" + + if p is None: + p = len(predecessor) - 1 + while predecessor[p] == 1: + p -= 1 + if p == 0: + return None + + q = p - 1 + while predecessor[q] != predecessor[p] - 1: + q -= 1 + result = list(predecessor) + for i in range(p, len(result)): + result[i] = result[i - p + q] + return result + + +def _next_tree(candidate): + """One iteration of the Wright, Richmond, Odlyzko and McKay + algorithm.""" + + # valid representation of a free tree if: + # there are at least two vertices at layer 1 + # (this is always the case because we start at the path graph) + left, rest = _split_tree(candidate) + + # and the left subtree of the root + # is less high than the tree with the left subtree removed + left_height = max(left) + rest_height = max(rest) + valid = rest_height >= left_height + + if valid and rest_height == left_height: + # and, if left and rest are of the same height, + # if left does not encompass more vertices + if len(left) > len(rest): + valid = False + # and, if they have the same number or vertices, + # if left does not come after rest lexicographically + elif len(left) == len(rest) and left > rest: + valid = False + + if valid: + return candidate + else: + # jump to the next valid free tree + p = len(left) + new_candidate = _next_rooted_tree(candidate, p) + if candidate[p] > 2: + new_left, new_rest = _split_tree(new_candidate) + new_left_height = max(new_left) + suffix = range(1, new_left_height + 2) + new_candidate[-len(suffix) :] = suffix + return new_candidate + + +def _split_tree(layout): + """Returns a tuple of two layouts, one containing the left + subtree of the root vertex, and one containing the original tree + with the left subtree removed.""" + + one_found = False + m = None + for i in range(len(layout)): + if layout[i] == 1: + if one_found: + m = i + break + else: + one_found = True + + if m is None: + m = len(layout) + + left = [layout[i] - 1 for i in range(1, m)] + rest = [0] + [layout[i] for i in range(m, len(layout))] + return (left, rest) + + +def _layout_to_matrix(layout): + """Create the adjacency matrix for the tree specified by the + given layout (level sequence).""" + + result = [[0] * len(layout) for i in range(len(layout))] + stack = [] + for i in range(len(layout)): + i_level = layout[i] + if stack: + j = stack[-1] + j_level = layout[j] + while j_level >= i_level: + stack.pop() + j = stack[-1] + j_level = layout[j] + result[i][j] = result[j][i] = 1 + stack.append(i) + return result + + +def _layout_to_graph(layout): + """Create a NetworkX Graph for the tree specified by the + given layout(level sequence)""" + G = nx.Graph() + stack = [] + for i in range(len(layout)): + i_level = layout[i] + if stack: + j = stack[-1] + j_level = layout[j] + while j_level >= i_level: + stack.pop() + j = stack[-1] + j_level = layout[j] + G.add_edge(i, j) + stack.append(i) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_clustered.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_clustered.py new file mode 100644 index 0000000000000000000000000000000000000000..8fbf855e672d3c50f1e74952cc2272143fbac57a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_clustered.py @@ -0,0 +1,117 @@ +"""Generate graphs with given degree and triangle sequence.""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = ["random_clustered_graph"] + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_clustered_graph(joint_degree_sequence, create_using=None, seed=None): + r"""Generate a random graph with the given joint independent edge degree and + triangle degree sequence. + + This uses a configuration model-like approach to generate a random graph + (with parallel edges and self-loops) by randomly assigning edges to match + the given joint degree sequence. + + The joint degree sequence is a list of pairs of integers of the form + $[(d_{1,i}, d_{1,t}), \dotsc, (d_{n,i}, d_{n,t})]$. According to this list, + vertex $u$ is a member of $d_{u,t}$ triangles and has $d_{u, i}$ other + edges. The number $d_{u,t}$ is the *triangle degree* of $u$ and the number + $d_{u,i}$ is the *independent edge degree*. + + Parameters + ---------- + joint_degree_sequence : list of integer pairs + Each list entry corresponds to the independent edge degree and + triangle degree of a node. + create_using : NetworkX graph constructor, optional (default MultiGraph) + Graph type to create. If graph instance, then cleared before populated. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + G : MultiGraph + A graph with the specified degree sequence. Nodes are labeled + starting at 0 with an index corresponding to the position in + deg_sequence. + + Raises + ------ + NetworkXError + If the independent edge degree sequence sum is not even + or the triangle degree sequence sum is not divisible by 3. + + Notes + ----- + As described by Miller [1]_ (see also Newman [2]_ for an equivalent + description). + + A non-graphical degree sequence (not realizable by some simple + graph) is allowed since this function returns graphs with self + loops and parallel edges. An exception is raised if the + independent degree sequence does not have an even sum or the + triangle degree sequence sum is not divisible by 3. + + This configuration model-like construction process can lead to + duplicate edges and loops. You can remove the self-loops and + parallel edges (see below) which will likely result in a graph + that doesn't have the exact degree sequence specified. This + "finite-size effect" decreases as the size of the graph increases. + + References + ---------- + .. [1] Joel C. Miller. "Percolation and epidemics in random clustered + networks". In: Physical review. E, Statistical, nonlinear, and soft + matter physics 80 (2 Part 1 August 2009). + .. [2] M. E. J. Newman. "Random Graphs with Clustering". + In: Physical Review Letters 103 (5 July 2009) + + Examples + -------- + >>> deg = [(1, 0), (1, 0), (1, 0), (2, 0), (1, 0), (2, 1), (0, 1), (0, 1)] + >>> G = nx.random_clustered_graph(deg) + + To remove parallel edges: + + >>> G = nx.Graph(G) + + To remove self loops: + + >>> G.remove_edges_from(nx.selfloop_edges(G)) + + """ + # In Python 3, zip() returns an iterator. Make this into a list. + joint_degree_sequence = list(joint_degree_sequence) + + N = len(joint_degree_sequence) + G = nx.empty_graph(N, create_using, default=nx.MultiGraph) + if G.is_directed(): + raise nx.NetworkXError("Directed Graph not supported") + + ilist = [] + tlist = [] + for n in G: + degrees = joint_degree_sequence[n] + for icount in range(degrees[0]): + ilist.append(n) + for tcount in range(degrees[1]): + tlist.append(n) + + if len(ilist) % 2 != 0 or len(tlist) % 3 != 0: + raise nx.NetworkXError("Invalid degree sequence") + + seed.shuffle(ilist) + seed.shuffle(tlist) + while ilist: + G.add_edge(ilist.pop(), ilist.pop()) + while tlist: + n1 = tlist.pop() + n2 = tlist.pop() + n3 = tlist.pop() + G.add_edges_from([(n1, n2), (n1, n3), (n2, n3)]) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_graphs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_graphs.py new file mode 100644 index 0000000000000000000000000000000000000000..d4ab0c9813631319ff866ac585926a88e6d76550 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/random_graphs.py @@ -0,0 +1,1416 @@ +""" +Generators for random graphs. + +""" + +import itertools +import math +from collections import defaultdict + +import networkx as nx +from networkx.utils import py_random_state + +from ..utils.misc import check_create_using +from .classic import complete_graph, empty_graph, path_graph, star_graph +from .degree_seq import degree_sequence_tree + +__all__ = [ + "fast_gnp_random_graph", + "gnp_random_graph", + "dense_gnm_random_graph", + "gnm_random_graph", + "erdos_renyi_graph", + "binomial_graph", + "newman_watts_strogatz_graph", + "watts_strogatz_graph", + "connected_watts_strogatz_graph", + "random_regular_graph", + "barabasi_albert_graph", + "dual_barabasi_albert_graph", + "extended_barabasi_albert_graph", + "powerlaw_cluster_graph", + "random_lobster", + "random_lobster_graph", + "random_shell_graph", + "random_powerlaw_tree", + "random_powerlaw_tree_sequence", + "random_kernel_graph", +] + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def fast_gnp_random_graph(n, p, seed=None, directed=False, *, create_using=None): + """Returns a $G_{n,p}$ random graph, also known as an Erdős-Rényi graph or + a binomial graph. + + Parameters + ---------- + n : int + The number of nodes. + p : float + Probability for edge creation. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + directed : bool, optional (default=False) + If True, this function returns a directed graph. + create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph types are not supported and raise a ``NetworkXError``. + By default NetworkX Graph or DiGraph are used depending on `directed`. + + Notes + ----- + The $G_{n,p}$ graph algorithm chooses each of the $[n (n - 1)] / 2$ + (undirected) or $n (n - 1)$ (directed) possible edges with probability $p$. + + This algorithm [1]_ runs in $O(n + m)$ time, where `m` is the expected number of + edges, which equals $p n (n - 1) / 2$. This should be faster than + :func:`gnp_random_graph` when $p$ is small and the expected number of edges + is small (that is, the graph is sparse). + + See Also + -------- + gnp_random_graph + + References + ---------- + .. [1] Vladimir Batagelj and Ulrik Brandes, + "Efficient generation of large random networks", + Phys. Rev. E, 71, 036113, 2005. + """ + default = nx.DiGraph if directed else nx.Graph + create_using = check_create_using( + create_using, directed=directed, multigraph=False, default=default + ) + if p <= 0 or p >= 1: + return nx.gnp_random_graph( + n, p, seed=seed, directed=directed, create_using=create_using + ) + + G = empty_graph(n, create_using=create_using) + + lp = math.log(1.0 - p) + + if directed: + v = 1 + w = -1 + while v < n: + lr = math.log(1.0 - seed.random()) + w = w + 1 + int(lr / lp) + while w >= v and v < n: + w = w - v + v = v + 1 + if v < n: + G.add_edge(w, v) + + # Nodes in graph are from 0,n-1 (start with v as the second node index). + v = 1 + w = -1 + while v < n: + lr = math.log(1.0 - seed.random()) + w = w + 1 + int(lr / lp) + while w >= v and v < n: + w = w - v + v = v + 1 + if v < n: + G.add_edge(v, w) + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def gnp_random_graph(n, p, seed=None, directed=False, *, create_using=None): + """Returns a $G_{n,p}$ random graph, also known as an Erdős-Rényi graph + or a binomial graph. + + The $G_{n,p}$ model chooses each of the possible edges with probability $p$. + + Parameters + ---------- + n : int + The number of nodes. + p : float + Probability for edge creation. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + directed : bool, optional (default=False) + If True, this function returns a directed graph. + create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph types are not supported and raise a ``NetworkXError``. + By default NetworkX Graph or DiGraph are used depending on `directed`. + + See Also + -------- + fast_gnp_random_graph + + Notes + ----- + This algorithm [2]_ runs in $O(n^2)$ time. For sparse graphs (that is, for + small values of $p$), :func:`fast_gnp_random_graph` is a faster algorithm. + + :func:`binomial_graph` and :func:`erdos_renyi_graph` are + aliases for :func:`gnp_random_graph`. + + >>> nx.binomial_graph is nx.gnp_random_graph + True + >>> nx.erdos_renyi_graph is nx.gnp_random_graph + True + + References + ---------- + .. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959). + .. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959). + """ + default = nx.DiGraph if directed else nx.Graph + create_using = check_create_using( + create_using, directed=directed, multigraph=False, default=default + ) + if p >= 1: + return complete_graph(n, create_using=create_using) + + G = nx.empty_graph(n, create_using=create_using) + if p <= 0: + return G + + edgetool = itertools.permutations if directed else itertools.combinations + for e in edgetool(range(n), 2): + if seed.random() < p: + G.add_edge(*e) + return G + + +# add some aliases to common names +binomial_graph = gnp_random_graph +erdos_renyi_graph = gnp_random_graph + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def dense_gnm_random_graph(n, m, seed=None, *, create_using=None): + """Returns a $G_{n,m}$ random graph. + + In the $G_{n,m}$ model, a graph is chosen uniformly at random from the set + of all graphs with $n$ nodes and $m$ edges. + + This algorithm should be faster than :func:`gnm_random_graph` for dense + graphs. + + Parameters + ---------- + n : int + The number of nodes. + m : int + The number of edges. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + See Also + -------- + gnm_random_graph + + Notes + ----- + Algorithm by Keith M. Briggs Mar 31, 2006. + Inspired by Knuth's Algorithm S (Selection sampling technique), + in section 3.4.2 of [1]_. + + References + ---------- + .. [1] Donald E. Knuth, The Art of Computer Programming, + Volume 2/Seminumerical algorithms, Third Edition, Addison-Wesley, 1997. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + mmax = n * (n - 1) // 2 + if m >= mmax: + return complete_graph(n, create_using) + G = empty_graph(n, create_using) + + if n == 1: + return G + + u = 0 + v = 1 + t = 0 + k = 0 + while True: + if seed.randrange(mmax - t) < m - k: + G.add_edge(u, v) + k += 1 + if k == m: + return G + t += 1 + v += 1 + if v == n: # go to next row of adjacency matrix + u += 1 + v = u + 1 + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def gnm_random_graph(n, m, seed=None, directed=False, *, create_using=None): + """Returns a $G_{n,m}$ random graph. + + In the $G_{n,m}$ model, a graph is chosen uniformly at random from the set + of all graphs with $n$ nodes and $m$ edges. + + This algorithm should be faster than :func:`dense_gnm_random_graph` for + sparse graphs. + + Parameters + ---------- + n : int + The number of nodes. + m : int + The number of edges. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + directed : bool, optional (default=False) + If True return a directed graph + create_using : Graph constructor, optional (default=nx.Graph or nx.DiGraph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph types are not supported and raise a ``NetworkXError``. + By default NetworkX Graph or DiGraph are used depending on `directed`. + + See also + -------- + dense_gnm_random_graph + + """ + default = nx.DiGraph if directed else nx.Graph + create_using = check_create_using( + create_using, directed=directed, multigraph=False, default=default + ) + if n == 1: + return nx.empty_graph(n, create_using=create_using) + max_edges = n * (n - 1) if directed else n * (n - 1) / 2.0 + if m >= max_edges: + return complete_graph(n, create_using=create_using) + + G = nx.empty_graph(n, create_using=create_using) + nlist = list(G) + edge_count = 0 + while edge_count < m: + # generate random edge,u,v + u = seed.choice(nlist) + v = seed.choice(nlist) + if u == v or G.has_edge(u, v): + continue + else: + G.add_edge(u, v) + edge_count = edge_count + 1 + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def newman_watts_strogatz_graph(n, k, p, seed=None, *, create_using=None): + """Returns a Newman–Watts–Strogatz small-world graph. + + Parameters + ---------- + n : int + The number of nodes. + k : int + Each node is joined with its `k` nearest neighbors in a ring + topology. + p : float + The probability of adding a new edge for each edge. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + First create a ring over $n$ nodes [1]_. Then each node in the ring is + connected with its $k$ nearest neighbors (or $k - 1$ neighbors if $k$ + is odd). Then shortcuts are created by adding new edges as follows: for + each edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest + neighbors" with probability $p$ add a new edge $(u, w)$ with + randomly-chosen existing node $w$. In contrast with + :func:`watts_strogatz_graph`, no edges are removed. + + See Also + -------- + watts_strogatz_graph + + References + ---------- + .. [1] M. E. J. Newman and D. J. Watts, + Renormalization group analysis of the small-world network model, + Physics Letters A, 263, 341, 1999. + https://doi.org/10.1016/S0375-9601(99)00757-4 + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if k > n: + raise nx.NetworkXError("k>=n, choose smaller k or larger n") + + # If k == n the graph return is a complete graph + if k == n: + return nx.complete_graph(n, create_using) + + G = empty_graph(n, create_using) + nlist = list(G.nodes()) + fromv = nlist + # connect the k/2 neighbors + for j in range(1, k // 2 + 1): + tov = fromv[j:] + fromv[0:j] # the first j are now last + for i in range(len(fromv)): + G.add_edge(fromv[i], tov[i]) + # for each edge u-v, with probability p, randomly select existing + # node w and add new edge u-w + e = list(G.edges()) + for u, v in e: + if seed.random() < p: + w = seed.choice(nlist) + # no self-loops and reject if edge u-w exists + # is that the correct NWS model? + while w == u or G.has_edge(u, w): + w = seed.choice(nlist) + if G.degree(u) >= n - 1: + break # skip this rewiring + else: + G.add_edge(u, w) + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def watts_strogatz_graph(n, k, p, seed=None, *, create_using=None): + """Returns a Watts–Strogatz small-world graph. + + Parameters + ---------- + n : int + The number of nodes + k : int + Each node is joined with its `k` nearest neighbors in a ring + topology. + p : float + The probability of rewiring each edge + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + See Also + -------- + newman_watts_strogatz_graph + connected_watts_strogatz_graph + + Notes + ----- + First create a ring over $n$ nodes [1]_. Then each node in the ring is joined + to its $k$ nearest neighbors (or $k - 1$ neighbors if $k$ is odd). + Then shortcuts are created by replacing some edges as follows: for each + edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest neighbors" + with probability $p$ replace it with a new edge $(u, w)$ with uniformly + random choice of existing node $w$. + + In contrast with :func:`newman_watts_strogatz_graph`, the random rewiring + does not increase the number of edges. The rewired graph is not guaranteed + to be connected as in :func:`connected_watts_strogatz_graph`. + + References + ---------- + .. [1] Duncan J. Watts and Steven H. Strogatz, + Collective dynamics of small-world networks, + Nature, 393, pp. 440--442, 1998. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if k > n: + raise nx.NetworkXError("k>n, choose smaller k or larger n") + + # If k == n, the graph is complete not Watts-Strogatz + if k == n: + G = nx.complete_graph(n, create_using) + return G + + G = nx.empty_graph(n, create_using=create_using) + nodes = list(range(n)) # nodes are labeled 0 to n-1 + # connect each node to k/2 neighbors + for j in range(1, k // 2 + 1): + targets = nodes[j:] + nodes[0:j] # first j nodes are now last in list + G.add_edges_from(zip(nodes, targets)) + # rewire edges from each node + # loop over all nodes in order (label) and neighbors in order (distance) + # no self loops or multiple edges allowed + for j in range(1, k // 2 + 1): # outer loop is neighbors + targets = nodes[j:] + nodes[0:j] # first j nodes are now last in list + # inner loop in node order + for u, v in zip(nodes, targets): + if seed.random() < p: + w = seed.choice(nodes) + # Enforce no self-loops or multiple edges + while w == u or G.has_edge(u, w): + w = seed.choice(nodes) + if G.degree(u) >= n - 1: + break # skip this rewiring + else: + G.remove_edge(u, v) + G.add_edge(u, w) + return G + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def connected_watts_strogatz_graph(n, k, p, tries=100, seed=None, *, create_using=None): + """Returns a connected Watts–Strogatz small-world graph. + + Attempts to generate a connected graph by repeated generation of + Watts–Strogatz small-world graphs. An exception is raised if the maximum + number of tries is exceeded. + + Parameters + ---------- + n : int + The number of nodes + k : int + Each node is joined with its `k` nearest neighbors in a ring + topology. + p : float + The probability of rewiring each edge + tries : int + Number of attempts to generate a connected graph. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + First create a ring over $n$ nodes [1]_. Then each node in the ring is joined + to its $k$ nearest neighbors (or $k - 1$ neighbors if $k$ is odd). + Then shortcuts are created by replacing some edges as follows: for each + edge $(u, v)$ in the underlying "$n$-ring with $k$ nearest neighbors" + with probability $p$ replace it with a new edge $(u, w)$ with uniformly + random choice of existing node $w$. + The entire process is repeated until a connected graph results. + + See Also + -------- + newman_watts_strogatz_graph + watts_strogatz_graph + + References + ---------- + .. [1] Duncan J. Watts and Steven H. Strogatz, + Collective dynamics of small-world networks, + Nature, 393, pp. 440--442, 1998. + """ + for i in range(tries): + # seed is an RNG so should change sequence each call + G = watts_strogatz_graph(n, k, p, seed, create_using=create_using) + if nx.is_connected(G): + return G + raise nx.NetworkXError("Maximum number of tries exceeded") + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_regular_graph(d, n, seed=None, *, create_using=None): + r"""Returns a random $d$-regular graph on $n$ nodes. + + A regular graph is a graph where each node has the same number of neighbors. + + The resulting graph has no self-loops or parallel edges. + + Parameters + ---------- + d : int + The degree of each node. + n : integer + The number of nodes. The value of $n \times d$ must be even. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + The nodes are numbered from $0$ to $n - 1$. + + Kim and Vu's paper [2]_ shows that this algorithm samples in an + asymptotically uniform way from the space of random graphs when + $d = O(n^{1 / 3 - \epsilon})$. + + Raises + ------ + + NetworkXError + If $n \times d$ is odd or $d$ is greater than or equal to $n$. + + References + ---------- + .. [1] A. Steger and N. Wormald, + Generating random regular graphs quickly, + Probability and Computing 8 (1999), 377-396, 1999. + https://doi.org/10.1017/S0963548399003867 + + .. [2] Jeong Han Kim and Van H. Vu, + Generating random regular graphs, + Proceedings of the thirty-fifth ACM symposium on Theory of computing, + San Diego, CA, USA, pp 213--222, 2003. + http://portal.acm.org/citation.cfm?id=780542.780576 + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if (n * d) % 2 != 0: + raise nx.NetworkXError("n * d must be even") + + if not 0 <= d < n: + raise nx.NetworkXError("the 0 <= d < n inequality must be satisfied") + + G = nx.empty_graph(n, create_using=create_using) + + if d == 0: + return G + + def _suitable(edges, potential_edges): + # Helper subroutine to check if there are suitable edges remaining + # If False, the generation of the graph has failed + if not potential_edges: + return True + for s1 in potential_edges: + for s2 in potential_edges: + # Two iterators on the same dictionary are guaranteed + # to visit it in the same order if there are no + # intervening modifications. + if s1 == s2: + # Only need to consider s1-s2 pair one time + break + if s1 > s2: + s1, s2 = s2, s1 + if (s1, s2) not in edges: + return True + return False + + def _try_creation(): + # Attempt to create an edge set + + edges = set() + stubs = list(range(n)) * d + + while stubs: + potential_edges = defaultdict(lambda: 0) + seed.shuffle(stubs) + stubiter = iter(stubs) + for s1, s2 in zip(stubiter, stubiter): + if s1 > s2: + s1, s2 = s2, s1 + if s1 != s2 and ((s1, s2) not in edges): + edges.add((s1, s2)) + else: + potential_edges[s1] += 1 + potential_edges[s2] += 1 + + if not _suitable(edges, potential_edges): + return None # failed to find suitable edge set + + stubs = [ + node + for node, potential in potential_edges.items() + for _ in range(potential) + ] + return edges + + # Even though a suitable edge set exists, + # the generation of such a set is not guaranteed. + # Try repeatedly to find one. + edges = _try_creation() + while edges is None: + edges = _try_creation() + G.add_edges_from(edges) + + return G + + +def _random_subset(seq, m, rng): + """Return m unique elements from seq. + + This differs from random.sample which can return repeated + elements if seq holds repeated elements. + + Note: rng is a random.Random or numpy.random.RandomState instance. + """ + targets = set() + while len(targets) < m: + x = rng.choice(seq) + targets.add(x) + return targets + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def barabasi_albert_graph(n, m, seed=None, initial_graph=None, *, create_using=None): + """Returns a random graph using Barabási–Albert preferential attachment + + A graph of $n$ nodes is grown by attaching new nodes each with $m$ + edges that are preferentially attached to existing nodes with high degree. + + Parameters + ---------- + n : int + Number of nodes + m : int + Number of edges to attach from a new node to existing nodes + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + initial_graph : Graph or None (default) + Initial network for Barabási–Albert algorithm. + It should be a connected graph for most use cases. + A copy of `initial_graph` is used. + If None, starts from a star graph on (m+1) nodes. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If `m` does not satisfy ``1 <= m < n``, or + the initial graph number of nodes m0 does not satisfy ``m <= m0 <= n``. + + References + ---------- + .. [1] A. L. Barabási and R. Albert "Emergence of scaling in + random networks", Science 286, pp 509-512, 1999. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if m < 1 or m >= n: + raise nx.NetworkXError( + f"Barabási–Albert network must have m >= 1 and m < n, m = {m}, n = {n}" + ) + + if initial_graph is None: + # Default initial graph : star graph on (m + 1) nodes + G = star_graph(m, create_using) + else: + if len(initial_graph) < m or len(initial_graph) > n: + raise nx.NetworkXError( + f"Barabási–Albert initial graph needs between m={m} and n={n} nodes" + ) + G = initial_graph.copy() + + # List of existing nodes, with nodes repeated once for each adjacent edge + repeated_nodes = [n for n, d in G.degree() for _ in range(d)] + # Start adding the other n - m0 nodes. + source = len(G) + while source < n: + # Now choose m unique nodes from the existing nodes + # Pick uniformly from repeated_nodes (preferential attachment) + targets = _random_subset(repeated_nodes, m, seed) + # Add edges to m nodes from the source. + G.add_edges_from(zip([source] * m, targets)) + # Add one node to the list for each new edge just created. + repeated_nodes.extend(targets) + # And the new node "source" has m edges to add to the list. + repeated_nodes.extend([source] * m) + + source += 1 + return G + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def dual_barabasi_albert_graph( + n, m1, m2, p, seed=None, initial_graph=None, *, create_using=None +): + """Returns a random graph using dual Barabási–Albert preferential attachment + + A graph of $n$ nodes is grown by attaching new nodes each with either $m_1$ + edges (with probability $p$) or $m_2$ edges (with probability $1-p$) that + are preferentially attached to existing nodes with high degree. + + Parameters + ---------- + n : int + Number of nodes + m1 : int + Number of edges to link each new node to existing nodes with probability $p$ + m2 : int + Number of edges to link each new node to existing nodes with probability $1-p$ + p : float + The probability of attaching $m_1$ edges (as opposed to $m_2$ edges) + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + initial_graph : Graph or None (default) + Initial network for Barabási–Albert algorithm. + A copy of `initial_graph` is used. + It should be connected for most use cases. + If None, starts from an star graph on max(m1, m2) + 1 nodes. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If `m1` and `m2` do not satisfy ``1 <= m1,m2 < n``, or + `p` does not satisfy ``0 <= p <= 1``, or + the initial graph number of nodes m0 does not satisfy m1, m2 <= m0 <= n. + + References + ---------- + .. [1] N. Moshiri "The dual-Barabasi-Albert model", arXiv:1810.10538. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if m1 < 1 or m1 >= n: + raise nx.NetworkXError( + f"Dual Barabási–Albert must have m1 >= 1 and m1 < n, m1 = {m1}, n = {n}" + ) + if m2 < 1 or m2 >= n: + raise nx.NetworkXError( + f"Dual Barabási–Albert must have m2 >= 1 and m2 < n, m2 = {m2}, n = {n}" + ) + if p < 0 or p > 1: + raise nx.NetworkXError( + f"Dual Barabási–Albert network must have 0 <= p <= 1, p = {p}" + ) + + # For simplicity, if p == 0 or 1, just return BA + if p == 1: + return barabasi_albert_graph(n, m1, seed, create_using=create_using) + elif p == 0: + return barabasi_albert_graph(n, m2, seed, create_using=create_using) + + if initial_graph is None: + # Default initial graph : star graph on max(m1, m2) nodes + G = star_graph(max(m1, m2), create_using) + else: + if len(initial_graph) < max(m1, m2) or len(initial_graph) > n: + raise nx.NetworkXError( + f"Barabási–Albert initial graph must have between " + f"max(m1, m2) = {max(m1, m2)} and n = {n} nodes" + ) + G = initial_graph.copy() + + # Target nodes for new edges + targets = list(G) + # List of existing nodes, with nodes repeated once for each adjacent edge + repeated_nodes = [n for n, d in G.degree() for _ in range(d)] + # Start adding the remaining nodes. + source = len(G) + while source < n: + # Pick which m to use (m1 or m2) + if seed.random() < p: + m = m1 + else: + m = m2 + # Now choose m unique nodes from the existing nodes + # Pick uniformly from repeated_nodes (preferential attachment) + targets = _random_subset(repeated_nodes, m, seed) + # Add edges to m nodes from the source. + G.add_edges_from(zip([source] * m, targets)) + # Add one node to the list for each new edge just created. + repeated_nodes.extend(targets) + # And the new node "source" has m edges to add to the list. + repeated_nodes.extend([source] * m) + + source += 1 + return G + + +@py_random_state(4) +@nx._dispatchable(graphs=None, returns_graph=True) +def extended_barabasi_albert_graph(n, m, p, q, seed=None, *, create_using=None): + """Returns an extended Barabási–Albert model graph. + + An extended Barabási–Albert model graph is a random graph constructed + using preferential attachment. The extended model allows new edges, + rewired edges or new nodes. Based on the probabilities $p$ and $q$ + with $p + q < 1$, the growing behavior of the graph is determined as: + + 1) With $p$ probability, $m$ new edges are added to the graph, + starting from randomly chosen existing nodes and attached preferentially at the + other end. + + 2) With $q$ probability, $m$ existing edges are rewired + by randomly choosing an edge and rewiring one end to a preferentially chosen node. + + 3) With $(1 - p - q)$ probability, $m$ new nodes are added to the graph + with edges attached preferentially. + + When $p = q = 0$, the model behaves just like the Barabási–Alber model. + + Parameters + ---------- + n : int + Number of nodes + m : int + Number of edges with which a new node attaches to existing nodes + p : float + Probability value for adding an edge between existing nodes. p + q < 1 + q : float + Probability value of rewiring of existing edges. p + q < 1 + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If `m` does not satisfy ``1 <= m < n`` or ``1 >= p + q`` + + References + ---------- + .. [1] Albert, R., & Barabási, A. L. (2000) + Topology of evolving networks: local events and universality + Physical review letters, 85(24), 5234. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if m < 1 or m >= n: + msg = f"Extended Barabasi-Albert network needs m>=1 and m= 1: + msg = f"Extended Barabasi-Albert network needs p + q <= 1, p={p}, q={q}" + raise nx.NetworkXError(msg) + + # Add m initial nodes (m0 in barabasi-speak) + G = empty_graph(m, create_using) + + # List of nodes to represent the preferential attachment random selection. + # At the creation of the graph, all nodes are added to the list + # so that even nodes that are not connected have a chance to get selected, + # for rewiring and adding of edges. + # With each new edge, nodes at the ends of the edge are added to the list. + attachment_preference = [] + attachment_preference.extend(range(m)) + + # Start adding the other n-m nodes. The first node is m. + new_node = m + while new_node < n: + a_probability = seed.random() + + # Total number of edges of a Clique of all the nodes + clique_degree = len(G) - 1 + clique_size = (len(G) * clique_degree) / 2 + + # Adding m new edges, if there is room to add them + if a_probability < p and G.size() <= clique_size - m: + # Select the nodes where an edge can be added + eligible_nodes = [nd for nd, deg in G.degree() if deg < clique_degree] + for i in range(m): + # Choosing a random source node from eligible_nodes + src_node = seed.choice(eligible_nodes) + + # Picking a possible node that is not 'src_node' or + # neighbor with 'src_node', with preferential attachment + prohibited_nodes = list(G[src_node]) + prohibited_nodes.append(src_node) + # This will raise an exception if the sequence is empty + dest_node = seed.choice( + [nd for nd in attachment_preference if nd not in prohibited_nodes] + ) + # Adding the new edge + G.add_edge(src_node, dest_node) + + # Appending both nodes to add to their preferential attachment + attachment_preference.append(src_node) + attachment_preference.append(dest_node) + + # Adjusting the eligible nodes. Degree may be saturated. + if G.degree(src_node) == clique_degree: + eligible_nodes.remove(src_node) + if G.degree(dest_node) == clique_degree and dest_node in eligible_nodes: + eligible_nodes.remove(dest_node) + + # Rewiring m edges, if there are enough edges + elif p <= a_probability < (p + q) and m <= G.size() < clique_size: + # Selecting nodes that have at least 1 edge but that are not + # fully connected to ALL other nodes (center of star). + # These nodes are the pivot nodes of the edges to rewire + eligible_nodes = [nd for nd, deg in G.degree() if 0 < deg < clique_degree] + for i in range(m): + # Choosing a random source node + node = seed.choice(eligible_nodes) + + # The available nodes do have a neighbor at least. + nbr_nodes = list(G[node]) + + # Choosing the other end that will get detached + src_node = seed.choice(nbr_nodes) + + # Picking a target node that is not 'node' or + # neighbor with 'node', with preferential attachment + nbr_nodes.append(node) + dest_node = seed.choice( + [nd for nd in attachment_preference if nd not in nbr_nodes] + ) + # Rewire + G.remove_edge(node, src_node) + G.add_edge(node, dest_node) + + # Adjusting the preferential attachment list + attachment_preference.remove(src_node) + attachment_preference.append(dest_node) + + # Adjusting the eligible nodes. + # nodes may be saturated or isolated. + if G.degree(src_node) == 0 and src_node in eligible_nodes: + eligible_nodes.remove(src_node) + if dest_node in eligible_nodes: + if G.degree(dest_node) == clique_degree: + eligible_nodes.remove(dest_node) + else: + if G.degree(dest_node) == 1: + eligible_nodes.append(dest_node) + + # Adding new node with m edges + else: + # Select the edges' nodes by preferential attachment + targets = _random_subset(attachment_preference, m, seed) + G.add_edges_from(zip([new_node] * m, targets)) + + # Add one node to the list for each new edge just created. + attachment_preference.extend(targets) + # The new node has m edges to it, plus itself: m + 1 + attachment_preference.extend([new_node] * (m + 1)) + new_node += 1 + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def powerlaw_cluster_graph(n, m, p, seed=None, *, create_using=None): + """Holme and Kim algorithm for growing graphs with powerlaw + degree distribution and approximate average clustering. + + Parameters + ---------- + n : int + the number of nodes + m : int + the number of random edges to add for each new node + p : float, + Probability of adding a triangle after adding a random edge + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + The average clustering has a hard time getting above a certain + cutoff that depends on `m`. This cutoff is often quite low. The + transitivity (fraction of triangles to possible triangles) seems to + decrease with network size. + + It is essentially the Barabási–Albert (BA) growth model with an + extra step that each random edge is followed by a chance of + making an edge to one of its neighbors too (and thus a triangle). + + This algorithm improves on BA in the sense that it enables a + higher average clustering to be attained if desired. + + It seems possible to have a disconnected graph with this algorithm + since the initial `m` nodes may not be all linked to a new node + on the first iteration like the BA model. + + Raises + ------ + NetworkXError + If `m` does not satisfy ``1 <= m <= n`` or `p` does not + satisfy ``0 <= p <= 1``. + + References + ---------- + .. [1] P. Holme and B. J. Kim, + "Growing scale-free networks with tunable clustering", + Phys. Rev. E, 65, 026107, 2002. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if m < 1 or n < m: + raise nx.NetworkXError(f"NetworkXError must have m>1 and m 1 or p < 0: + raise nx.NetworkXError(f"NetworkXError p must be in [0,1], p={p}") + + G = empty_graph(m, create_using) # add m initial nodes (m0 in barabasi-speak) + repeated_nodes = list(G) # list of existing nodes to sample from + # with nodes repeated once for each adjacent edge + source = m # next node is m + while source < n: # Now add the other n-1 nodes + possible_targets = _random_subset(repeated_nodes, m, seed) + # do one preferential attachment for new node + target = possible_targets.pop() + G.add_edge(source, target) + repeated_nodes.append(target) # add one node to list for each new link + count = 1 + while count < m: # add m-1 more new links + if seed.random() < p: # clustering step: add triangle + neighborhood = [ + nbr + for nbr in G.neighbors(target) + if not G.has_edge(source, nbr) and nbr != source + ] + if neighborhood: # if there is a neighbor without a link + nbr = seed.choice(neighborhood) + G.add_edge(source, nbr) # add triangle + repeated_nodes.append(nbr) + count = count + 1 + continue # go to top of while loop + # else do preferential attachment step if above fails + target = possible_targets.pop() + G.add_edge(source, target) + repeated_nodes.append(target) + count = count + 1 + + repeated_nodes.extend([source] * m) # add source node to list m times + source += 1 + return G + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_lobster_graph(n, p1, p2, seed=None, *, create_using=None): + """Returns a random lobster graph. + + A lobster is a tree that reduces to a caterpillar when pruning all + leaf nodes. A caterpillar is a tree that reduces to a path graph + when pruning all leaf nodes; setting `p2` to zero produces a caterpillar. + + This implementation iterates on the probabilities `p1` and `p2` to add + edges at levels 1 and 2, respectively. Graphs are therefore constructed + iteratively with uniform randomness at each level rather than being selected + uniformly at random from the set of all possible lobsters. + + Parameters + ---------- + n : int + The expected number of nodes in the backbone + p1 : float + Probability of adding an edge to the backbone + p2 : float + Probability of adding an edge one level beyond backbone + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Raises + ------ + NetworkXError + If `p1` or `p2` parameters are >= 1 because the while loops would never finish. + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + p1, p2 = abs(p1), abs(p2) + if any(p >= 1 for p in [p1, p2]): + raise nx.NetworkXError("Probability values for `p1` and `p2` must both be < 1.") + + # a necessary ingredient in any self-respecting graph library + llen = int(2 * seed.random() * n + 0.5) + L = path_graph(llen, create_using) + # build caterpillar: add edges to path graph with probability p1 + current_node = llen - 1 + for n in range(llen): + while seed.random() < p1: # add fuzzy caterpillar parts + current_node += 1 + L.add_edge(n, current_node) + cat_node = current_node + while seed.random() < p2: # add crunchy lobster bits + current_node += 1 + L.add_edge(cat_node, current_node) + return L # voila, un lobster! + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_lobster(n, p1, p2, seed=None, *, create_using=None): + """ + .. deprecated:: 3.5 + `random_lobster` is a deprecated alias + for `random_lobster_graph`. + Use `random_lobster_graph` instead. + """ + import warnings + + warnings.warn( + "`random_lobster` is deprecated, use `random_lobster_graph` instead.", + category=DeprecationWarning, + stacklevel=2, + ) + return random_lobster_graph(n, p1, p2, seed=seed, create_using=create_using) + + +@py_random_state(1) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_shell_graph(constructor, seed=None, *, create_using=None): + """Returns a random shell graph for the constructor given. + + Parameters + ---------- + constructor : list of three-tuples + Represents the parameters for a shell, starting at the center + shell. Each element of the list must be of the form `(n, m, + d)`, where `n` is the number of nodes in the shell, `m` is + the number of edges in the shell, and `d` is the ratio of + inter-shell (next) edges to intra-shell edges. If `d` is zero, + there will be no intra-shell edges, and if `d` is one there + will be all possible intra-shell edges. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. Graph instances are not supported. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Examples + -------- + >>> constructor = [(10, 20, 0.8), (20, 40, 0.8)] + >>> G = nx.random_shell_graph(constructor) + + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + G = empty_graph(0, create_using) + + glist = [] + intra_edges = [] + nnodes = 0 + # create gnm graphs for each shell + for n, m, d in constructor: + inter_edges = int(m * d) + intra_edges.append(m - inter_edges) + g = nx.convert_node_labels_to_integers( + gnm_random_graph(n, inter_edges, seed=seed, create_using=G.__class__), + first_label=nnodes, + ) + glist.append(g) + nnodes += n + G = nx.operators.union(G, g) + + # connect the shells randomly + for gi in range(len(glist) - 1): + nlist1 = list(glist[gi]) + nlist2 = list(glist[gi + 1]) + total_edges = intra_edges[gi] + edge_count = 0 + while edge_count < total_edges: + u = seed.choice(nlist1) + v = seed.choice(nlist2) + if u == v or G.has_edge(u, v): + continue + else: + G.add_edge(u, v) + edge_count = edge_count + 1 + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_powerlaw_tree(n, gamma=3, seed=None, tries=100, *, create_using=None): + """Returns a tree with a power law degree distribution. + + Parameters + ---------- + n : int + The number of nodes. + gamma : float + Exponent of the power law. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + tries : int + Number of attempts to adjust the sequence to make it a tree. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Raises + ------ + NetworkXError + If no valid sequence is found within the maximum number of + attempts. + + Notes + ----- + A trial power law degree sequence is chosen and then elements are + swapped with new elements from a powerlaw distribution until the + sequence makes a tree (by checking, for example, that the number of + edges is one smaller than the number of nodes). + + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + # This call may raise a NetworkXError if the number of tries is succeeded. + seq = random_powerlaw_tree_sequence(n, gamma=gamma, seed=seed, tries=tries) + G = degree_sequence_tree(seq, create_using) + return G + + +@py_random_state(2) +@nx._dispatchable(graphs=None) +def random_powerlaw_tree_sequence(n, gamma=3, seed=None, tries=100): + """Returns a degree sequence for a tree with a power law distribution. + + Parameters + ---------- + n : int, + The number of nodes. + gamma : float + Exponent of the power law. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + tries : int + Number of attempts to adjust the sequence to make it a tree. + + Raises + ------ + NetworkXError + If no valid sequence is found within the maximum number of + attempts. + + Notes + ----- + A trial power law degree sequence is chosen and then elements are + swapped with new elements from a power law distribution until + the sequence makes a tree (by checking, for example, that the number of + edges is one smaller than the number of nodes). + + """ + # get trial sequence + z = nx.utils.powerlaw_sequence(n, exponent=gamma, seed=seed) + # round to integer values in the range [0,n] + zseq = [min(n, max(round(s), 0)) for s in z] + + # another sequence to swap values from + z = nx.utils.powerlaw_sequence(tries, exponent=gamma, seed=seed) + # round to integer values in the range [0,n] + swap = [min(n, max(round(s), 0)) for s in z] + + for _ in swap: + valid, _ = nx.utils.is_valid_tree_degree_sequence(zseq) + if valid: + return zseq + index = seed.randint(0, n - 1) + zseq[index] = swap.pop() + + raise nx.NetworkXError( + f"Exceeded max ({tries}) attempts for a valid tree sequence." + ) + + +@py_random_state(3) +@nx._dispatchable(graphs=None, returns_graph=True) +def random_kernel_graph( + n, kernel_integral, kernel_root=None, seed=None, *, create_using=None +): + r"""Returns an random graph based on the specified kernel. + + The algorithm chooses each of the $[n(n-1)]/2$ possible edges with + probability specified by a kernel $\kappa(x,y)$ [1]_. The kernel + $\kappa(x,y)$ must be a symmetric (in $x,y$), non-negative, + bounded function. + + Parameters + ---------- + n : int + The number of nodes + kernel_integral : function + Function that returns the definite integral of the kernel $\kappa(x,y)$, + $F(y,a,b) := \int_a^b \kappa(x,y)dx$ + kernel_root: function (optional) + Function that returns the root $b$ of the equation $F(y,a,b) = r$. + If None, the root is found using :func:`scipy.optimize.brentq` + (this requires SciPy). + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + create_using : Graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + Multigraph and directed types are not supported and raise a ``NetworkXError``. + + Notes + ----- + The kernel is specified through its definite integral which must be + provided as one of the arguments. If the integral and root of the + kernel integral can be found in $O(1)$ time then this algorithm runs in + time $O(n+m)$ where m is the expected number of edges [2]_. + + The nodes are set to integers from $0$ to $n-1$. + + Examples + -------- + Generate an Erdős–Rényi random graph $G(n,c/n)$, with kernel + $\kappa(x,y)=c$ where $c$ is the mean expected degree. + + >>> def integral(u, w, z): + ... return c * (z - w) + >>> def root(u, w, r): + ... return r / c + w + >>> c = 1 + >>> graph = nx.random_kernel_graph(1000, integral, root) + + See Also + -------- + gnp_random_graph + expected_degree_graph + + References + ---------- + .. [1] Bollobás, Béla, Janson, S. and Riordan, O. + "The phase transition in inhomogeneous random graphs", + *Random Structures Algorithms*, 31, 3--122, 2007. + + .. [2] Hagberg A, Lemons N (2015), + "Fast Generation of Sparse Random Kernel Graphs". + PLoS ONE 10(9): e0135177, 2015. doi:10.1371/journal.pone.0135177 + """ + create_using = check_create_using(create_using, directed=False, multigraph=False) + if kernel_root is None: + import scipy as sp + + def kernel_root(y, a, r): + def my_function(b): + return kernel_integral(y, a, b) - r + + return sp.optimize.brentq(my_function, a, 1) + + graph = nx.empty_graph(create_using=create_using) + graph.add_nodes_from(range(n)) + (i, j) = (1, 1) + while i < n: + r = -math.log(1 - seed.random()) # (1-seed.random()) in (0, 1] + if kernel_integral(i / n, j / n, 1) <= r: + i, j = i + 1, i + 1 + else: + j = math.ceil(n * kernel_root(i / n, j / n, r)) + graph.add_edge(i - 1, j - 1) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/small.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/small.py new file mode 100644 index 0000000000000000000000000000000000000000..12fc2a3c99c893609ba43ce0af9af5404be19957 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/small.py @@ -0,0 +1,1070 @@ +""" +Various small and named graphs, together with some compact generators. + +""" + +__all__ = [ + "LCF_graph", + "bull_graph", + "chvatal_graph", + "cubical_graph", + "desargues_graph", + "diamond_graph", + "dodecahedral_graph", + "frucht_graph", + "generalized_petersen_graph", + "heawood_graph", + "hoffman_singleton_graph", + "house_graph", + "house_x_graph", + "icosahedral_graph", + "krackhardt_kite_graph", + "moebius_kantor_graph", + "octahedral_graph", + "pappus_graph", + "petersen_graph", + "sedgewick_maze_graph", + "tetrahedral_graph", + "truncated_cube_graph", + "truncated_tetrahedron_graph", + "tutte_graph", +] + +from functools import wraps + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.generators.classic import ( + complete_graph, + cycle_graph, + empty_graph, + path_graph, +) + + +def _raise_on_directed(func): + """ + A decorator which inspects the `create_using` argument and raises a + NetworkX exception when `create_using` is a DiGraph (class or instance) for + graph generators that do not support directed outputs. + + `create_using` may be a keyword argument or the first positional argument. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + create_using = args[0] if args else kwargs.get("create_using") + if create_using is not None: + G = nx.empty_graph(create_using=create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported in create_using") + return func(*args, **kwargs) + + return wrapper + + +@nx._dispatchable(graphs=None, returns_graph=True) +def LCF_graph(n, shift_list, repeats, create_using=None): + """ + Return the cubic graph specified in LCF notation. + + LCF (Lederberg-Coxeter-Fruchte) notation[1]_ is a compressed + notation used in the generation of various cubic Hamiltonian + graphs of high symmetry. See, for example, `dodecahedral_graph`, + `desargues_graph`, `heawood_graph` and `pappus_graph`. + + Nodes are drawn from ``range(n)``. Each node ``n_i`` is connected with + node ``n_i + shift % n`` where ``shift`` is given by cycling through + the input `shift_list` `repeat` s times. + + Parameters + ---------- + n : int + The starting graph is the `n`-cycle with nodes ``0, ..., n-1``. + The null graph is returned if `n` < 1. + + shift_list : list + A list of integer shifts mod `n`, ``[s1, s2, .., sk]`` + + repeats : int + Integer specifying the number of times that shifts in `shift_list` + are successively applied to each current node in the n-cycle + to generate an edge between ``n_current`` and ``n_current + shift mod n``. + + Returns + ------- + G : Graph + A graph instance created from the specified LCF notation. + + Examples + -------- + The utility graph $K_{3,3}$ + + >>> G = nx.LCF_graph(6, [3, -3], 3) + >>> G.edges() + EdgeView([(0, 1), (0, 5), (0, 3), (1, 2), (1, 4), (2, 3), (2, 5), (3, 4), (4, 5)]) + + The Heawood graph: + + >>> G = nx.LCF_graph(14, [5, -5], 7) + >>> nx.is_isomorphic(G, nx.heawood_graph()) + True + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/LCF_notation + + """ + if n <= 0: + return empty_graph(0, create_using) + + # start with the n-cycle + G = cycle_graph(n, create_using) + if G.is_directed(): + raise NetworkXError("Directed Graph not supported") + G.name = "LCF_graph" + nodes = sorted(G) + + n_extra_edges = repeats * len(shift_list) + # edges are added n_extra_edges times + # (not all of these need be new) + if n_extra_edges < 1: + return G + + for i in range(n_extra_edges): + shift = shift_list[i % len(shift_list)] # cycle through shift_list + v1 = nodes[i % n] # cycle repeatedly through nodes + v2 = nodes[(i + shift) % n] + G.add_edge(v1, v2) + return G + + +# ------------------------------------------------------------------------------- +# Various small and named graphs +# ------------------------------------------------------------------------------- + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def bull_graph(create_using=None): + """ + Returns the Bull Graph + + The Bull Graph has 5 nodes and 5 edges. It is a planar undirected + graph in the form of a triangle with two disjoint pendant edges [1]_ + The name comes from the triangle and pendant edges representing + respectively the body and legs of a bull. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + A bull graph with 5 nodes + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Bull_graph. + + """ + G = nx.from_dict_of_lists( + {0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 4], 3: [1], 4: [2]}, + create_using=create_using, + ) + G.name = "Bull Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def chvatal_graph(create_using=None): + """ + Returns the Chvátal Graph + + The Chvátal Graph is an undirected graph with 12 nodes and 24 edges [1]_. + It has 370 distinct (directed) Hamiltonian cycles, giving a unique generalized + LCF notation of order 4, two of order 6 , and 43 of order 1 [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + The Chvátal graph with 12 nodes and 24 edges + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Chv%C3%A1tal_graph + .. [2] https://mathworld.wolfram.com/ChvatalGraph.html + + """ + G = nx.from_dict_of_lists( + { + 0: [1, 4, 6, 9], + 1: [2, 5, 7], + 2: [3, 6, 8], + 3: [4, 7, 9], + 4: [5, 8], + 5: [10, 11], + 6: [10, 11], + 7: [8, 11], + 8: [10], + 9: [10, 11], + }, + create_using=create_using, + ) + G.name = "Chvatal Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def cubical_graph(create_using=None): + """ + Returns the 3-regular Platonic Cubical Graph + + The skeleton of the cube (the nodes and edges) form a graph, with 8 + nodes, and 12 edges. It is a special case of the hypercube graph. + It is one of 5 Platonic graphs, each a skeleton of its + Platonic solid [1]_. + Such graphs arise in parallel processing in computers. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + A cubical graph with 8 nodes and 12 edges + + See Also + -------- + tetrahedral_graph, octahedral_graph, dodecahedral_graph, icosahedral_graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Cube#Cubical_graph + + """ + G = nx.from_dict_of_lists( + { + 0: [1, 3, 4], + 1: [0, 2, 7], + 2: [1, 3, 6], + 3: [0, 2, 5], + 4: [0, 5, 7], + 5: [3, 4, 6], + 6: [2, 5, 7], + 7: [1, 4, 6], + }, + create_using=create_using, + ) + G.name = "Platonic Cubical Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def desargues_graph(create_using=None): + """ + Returns the Desargues Graph + + The Desargues Graph is a non-planar, distance-transitive cubic graph + with 20 nodes and 30 edges [1]_. It is isomorphic to the Generalized + Petersen Graph GP(10, 3). It is a symmetric graph. It can be represented + in LCF notation as [5,-5,9,-9]^5 [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Desargues Graph with 20 nodes and 30 edges + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Desargues_graph + .. [2] https://mathworld.wolfram.com/DesarguesGraph.html + """ + G = LCF_graph(20, [5, -5, 9, -9], 5, create_using) + G.name = "Desargues Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def diamond_graph(create_using=None): + """ + Returns the Diamond graph + + The Diamond Graph is planar undirected graph with 4 nodes and 5 edges. + It is also sometimes known as the double triangle graph or kite graph [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Diamond Graph with 4 nodes and 5 edges + + References + ---------- + .. [1] https://mathworld.wolfram.com/DiamondGraph.html + """ + G = nx.from_dict_of_lists( + {0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 3], 3: [1, 2]}, create_using=create_using + ) + G.name = "Diamond Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def dodecahedral_graph(create_using=None): + """ + Returns the Platonic Dodecahedral graph. + + The dodecahedral graph has 20 nodes and 30 edges. The skeleton of the + dodecahedron forms a graph. It is one of 5 Platonic graphs [1]_. + It can be described in LCF notation as: + ``[10, 7, 4, -4, -7, 10, -4, 7, -7, 4]^2`` [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Dodecahedral Graph with 20 nodes and 30 edges + + See Also + -------- + tetrahedral_graph, cubical_graph, octahedral_graph, icosahedral_graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Regular_dodecahedron#Dodecahedral_graph + .. [2] https://mathworld.wolfram.com/DodecahedralGraph.html + + """ + G = LCF_graph(20, [10, 7, 4, -4, -7, 10, -4, 7, -7, 4], 2, create_using) + G.name = "Dodecahedral Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def frucht_graph(create_using=None): + """ + Returns the Frucht Graph. + + The Frucht Graph is the smallest cubical graph whose + automorphism group consists only of the identity element [1]_. + It has 12 nodes and 18 edges and no nontrivial symmetries. + It is planar and Hamiltonian [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Frucht Graph with 12 nodes and 18 edges + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Frucht_graph + .. [2] https://mathworld.wolfram.com/FruchtGraph.html + + """ + G = cycle_graph(7, create_using) + G.add_edges_from( + [ + [0, 7], + [1, 7], + [2, 8], + [3, 9], + [4, 9], + [5, 10], + [6, 10], + [7, 11], + [8, 11], + [8, 9], + [10, 11], + ] + ) + + G.name = "Frucht Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def heawood_graph(create_using=None): + """ + Returns the Heawood Graph, a (3,6) cage. + + The Heawood Graph is an undirected graph with 14 nodes and 21 edges, + named after Percy John Heawood [1]_. + It is cubic symmetric, nonplanar, Hamiltonian, and can be represented + in LCF notation as ``[5,-5]^7`` [2]_. + It is the unique (3,6)-cage: the regular cubic graph of girth 6 with + minimal number of vertices [3]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Heawood Graph with 14 nodes and 21 edges + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Heawood_graph + .. [2] https://mathworld.wolfram.com/HeawoodGraph.html + .. [3] https://www.win.tue.nl/~aeb/graphs/Heawood.html + + """ + G = LCF_graph(14, [5, -5], 7, create_using) + G.name = "Heawood Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def hoffman_singleton_graph(): + """ + Returns the Hoffman-Singleton Graph. + + The Hoffman–Singleton graph is a symmetrical undirected graph + with 50 nodes and 175 edges. + All indices lie in ``Z % 5``: that is, the integers mod 5 [1]_. + It is the only regular graph of vertex degree 7, diameter 2, and girth 5. + It is the unique (7,5)-cage graph and Moore graph, and contains many + copies of the Petersen Graph [2]_. + + Returns + ------- + G : networkx Graph + Hoffman–Singleton Graph with 50 nodes and 175 edges + + Notes + ----- + Constructed from pentagon and pentagram as follows: Take five pentagons $P_h$ + and five pentagrams $Q_i$ . Join vertex $j$ of $P_h$ to vertex $h·i+j$ of $Q_i$ [3]_. + + References + ---------- + .. [1] https://blogs.ams.org/visualinsight/2016/02/01/hoffman-singleton-graph/ + .. [2] https://mathworld.wolfram.com/Hoffman-SingletonGraph.html + .. [3] https://en.wikipedia.org/wiki/Hoffman%E2%80%93Singleton_graph + + """ + G = nx.Graph() + for i in range(5): + for j in range(5): + G.add_edge(("pentagon", i, j), ("pentagon", i, (j - 1) % 5)) + G.add_edge(("pentagon", i, j), ("pentagon", i, (j + 1) % 5)) + G.add_edge(("pentagram", i, j), ("pentagram", i, (j - 2) % 5)) + G.add_edge(("pentagram", i, j), ("pentagram", i, (j + 2) % 5)) + for k in range(5): + G.add_edge(("pentagon", i, j), ("pentagram", k, (i * k + j) % 5)) + G = nx.convert_node_labels_to_integers(G) + G.name = "Hoffman-Singleton Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def house_graph(create_using=None): + """ + Returns the House graph (square with triangle on top) + + The house graph is a simple undirected graph with + 5 nodes and 6 edges [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + House graph in the form of a square with a triangle on top + + References + ---------- + .. [1] https://mathworld.wolfram.com/HouseGraph.html + """ + G = nx.from_dict_of_lists( + {0: [1, 2], 1: [0, 3], 2: [0, 3, 4], 3: [1, 2, 4], 4: [2, 3]}, + create_using=create_using, + ) + G.name = "House Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def house_x_graph(create_using=None): + """ + Returns the House graph with a cross inside the house square. + + The House X-graph is the House graph plus the two edges connecting diagonally + opposite vertices of the square base. It is also one of the two graphs + obtained by removing two edges from the pentatope graph [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + House graph with diagonal vertices connected + + References + ---------- + .. [1] https://mathworld.wolfram.com/HouseGraph.html + """ + G = house_graph(create_using) + G.add_edges_from([(0, 3), (1, 2)]) + G.name = "House-with-X-inside Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def icosahedral_graph(create_using=None): + """ + Returns the Platonic Icosahedral graph. + + The icosahedral graph has 12 nodes and 30 edges. It is a Platonic graph + whose nodes have the connectivity of the icosahedron. It is undirected, + regular and Hamiltonian [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Icosahedral graph with 12 nodes and 30 edges. + + See Also + -------- + tetrahedral_graph, cubical_graph, octahedral_graph, dodecahedral_graph + + References + ---------- + .. [1] https://mathworld.wolfram.com/IcosahedralGraph.html + """ + G = nx.from_dict_of_lists( + { + 0: [1, 5, 7, 8, 11], + 1: [2, 5, 6, 8], + 2: [3, 6, 8, 9], + 3: [4, 6, 9, 10], + 4: [5, 6, 10, 11], + 5: [6, 11], + 7: [8, 9, 10, 11], + 8: [9], + 9: [10], + 10: [11], + }, + create_using=create_using, + ) + G.name = "Platonic Icosahedral Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def krackhardt_kite_graph(create_using=None): + """ + Returns the Krackhardt Kite Social Network. + + A 10 actor social network introduced by David Krackhardt + to illustrate different centrality measures [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Krackhardt Kite graph with 10 nodes and 18 edges + + Notes + ----- + The traditional labeling is: + Andre=1, Beverley=2, Carol=3, Diane=4, + Ed=5, Fernando=6, Garth=7, Heather=8, Ike=9, Jane=10. + + References + ---------- + .. [1] Krackhardt, David. "Assessing the Political Landscape: Structure, + Cognition, and Power in Organizations". Administrative Science Quarterly. + 35 (2): 342–369. doi:10.2307/2393394. JSTOR 2393394. June 1990. + + """ + G = nx.from_dict_of_lists( + { + 0: [1, 2, 3, 5], + 1: [0, 3, 4, 6], + 2: [0, 3, 5], + 3: [0, 1, 2, 4, 5, 6], + 4: [1, 3, 6], + 5: [0, 2, 3, 6, 7], + 6: [1, 3, 4, 5, 7], + 7: [5, 6, 8], + 8: [7, 9], + 9: [8], + }, + create_using=create_using, + ) + G.name = "Krackhardt Kite Social Network" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def moebius_kantor_graph(create_using=None): + """ + Returns the Moebius-Kantor graph. + + The Möbius-Kantor graph is the cubic symmetric graph on 16 nodes. + Its LCF notation is [5,-5]^8, and it is isomorphic to the generalized + Petersen Graph GP(8, 3) [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Moebius-Kantor graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/M%C3%B6bius%E2%80%93Kantor_graph + + """ + G = LCF_graph(16, [5, -5], 8, create_using) + G.name = "Moebius-Kantor Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def octahedral_graph(create_using=None): + """ + Returns the Platonic Octahedral graph. + + The octahedral graph is the 6-node 12-edge Platonic graph having the + connectivity of the octahedron [1]_. If 6 couples go to a party, + and each person shakes hands with every person except his or her partner, + then this graph describes the set of handshakes that take place; + for this reason it is also called the cocktail party graph [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Octahedral graph + + See Also + -------- + tetrahedral_graph, cubical_graph, dodecahedral_graph, icosahedral_graph + + References + ---------- + .. [1] https://mathworld.wolfram.com/OctahedralGraph.html + .. [2] https://en.wikipedia.org/wiki/Tur%C3%A1n_graph#Special_cases + + """ + G = nx.from_dict_of_lists( + {0: [1, 2, 3, 4], 1: [2, 3, 5], 2: [4, 5], 3: [4, 5], 4: [5]}, + create_using=create_using, + ) + G.name = "Platonic Octahedral Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def pappus_graph(): + """ + Returns the Pappus graph. + + The Pappus graph is a cubic symmetric distance-regular graph with 18 nodes + and 27 edges. It is Hamiltonian and can be represented in LCF notation as + [5,7,-7,7,-7,-5]^3 [1]_. + + Returns + ------- + G : networkx Graph + Pappus graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Pappus_graph + """ + G = LCF_graph(18, [5, 7, -7, 7, -7, -5], 3) + G.name = "Pappus Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def petersen_graph(create_using=None): + """ + Returns the Petersen Graph. + + The Peterson Graph is a cubic, undirected graph with 10 nodes and 15 edges [1]_. + Julius Petersen constructed the graph as the smallest counterexample + against the claim that a connected bridgeless cubic graph + has an edge colouring with three colours [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Petersen Graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Petersen_graph + .. [2] https://www.win.tue.nl/~aeb/drg/graphs/Petersen.html + """ + G = nx.from_dict_of_lists( + { + 0: [1, 4, 5], + 1: [0, 2, 6], + 2: [1, 3, 7], + 3: [2, 4, 8], + 4: [3, 0, 9], + 5: [0, 7, 8], + 6: [1, 8, 9], + 7: [2, 5, 9], + 8: [3, 5, 6], + 9: [4, 6, 7], + }, + create_using=create_using, + ) + G.name = "Petersen Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def generalized_petersen_graph(n, k, *, create_using=None): + """ + Returns the Generalized Petersen Graph GP(n,k). + + The Generalized Peterson Graph consists of an outer cycle of n nodes + connected to an inner circulant graph of n nodes, where nodes in the + inner circulant are connected to their kth nearest neighbor [1]_ [2]_. + A Generalized Petersen Graph is cubic with 2n nodes and 3n edges. + + Some well known graphs are examples of Generalized Petersen Graphs such + as the Petersen Graph GP(5, 2), the Desargues graph GP(10, 3), the + Moebius-Kantor graph GP(8, 3), and the dodecahedron graph GP(10, 2). + + Parameters + ---------- + n : int + Number of nodes in the outer cycle and inner circulant. ``n >= 3`` is required. + + k : int + Neighbor to connect in the inner circulant. ``1 <= k <= n/2``. + Note that some people require ``k < n/2`` but we and others allow equality. + Also, ``k < n/2`` is equivalent to ``k <= floor((n-1)/2)`` + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Generalized Petersen Graph n k + + References + ---------- + .. [1] https://mathworld.wolfram.com/GeneralizedPetersenGraph.html + .. [2] https://en.wikipedia.org/wiki/Generalized_Petersen_graph + """ + if n <= 2: + raise NetworkXError(f"n >= 3 required. Got {n=}") + if k < 1 or k > n / 2: + raise NetworkXError(f" Got {n=} {k=}. Need 1 <= k <= n/2") + + G = nx.cycle_graph(range(n), create_using=create_using) # u-nodes + if G.is_directed(): + raise NetworkXError("Directed Graph not supported in create_using") + for i in range(n): + G.add_edge(i, n + i) # add v-nodes and u to v edges + G.add_edge(n + i, n + (i + k) % n) # edge from v_i to v_(i+k)%n + + G.name = f"Generalized Petersen Graph GP({n}, {k})" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def sedgewick_maze_graph(create_using=None): + """ + Return a small maze with a cycle. + + This is the maze used in Sedgewick, 3rd Edition, Part 5, Graph + Algorithms, Chapter 18, e.g. Figure 18.2 and following [1]_. + Nodes are numbered 0,..,7 + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Small maze with a cycle + + References + ---------- + .. [1] Figure 18.2, Chapter 18, Graph Algorithms (3rd Ed), Sedgewick + """ + G = empty_graph(0, create_using) + G.add_nodes_from(range(8)) + G.add_edges_from([[0, 2], [0, 7], [0, 5]]) + G.add_edges_from([[1, 7], [2, 6]]) + G.add_edges_from([[3, 4], [3, 5]]) + G.add_edges_from([[4, 5], [4, 7], [4, 6]]) + G.name = "Sedgewick Maze" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def tetrahedral_graph(create_using=None): + """ + Returns the 3-regular Platonic Tetrahedral graph. + + Tetrahedral graph has 4 nodes and 6 edges. It is a + special case of the complete graph, K4, and wheel graph, W4. + It is one of the 5 platonic graphs [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Tetrahedral Graph + + See Also + -------- + cubical_graph, octahedral_graph, dodecahedral_graph, icosahedral_graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Tetrahedron#Tetrahedral_graph + + """ + G = complete_graph(4, create_using) + G.name = "Platonic Tetrahedral Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def truncated_cube_graph(create_using=None): + """ + Returns the skeleton of the truncated cube. + + The truncated cube is an Archimedean solid with 14 regular + faces (6 octagonal and 8 triangular), 36 edges and 24 nodes [1]_. + The truncated cube is created by truncating (cutting off) the tips + of the cube one third of the way into each edge [2]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Skeleton of the truncated cube + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Truncated_cube + .. [2] https://www.coolmath.com/reference/polyhedra-truncated-cube + + """ + G = nx.from_dict_of_lists( + { + 0: [1, 2, 4], + 1: [11, 14], + 2: [3, 4], + 3: [6, 8], + 4: [5], + 5: [16, 18], + 6: [7, 8], + 7: [10, 12], + 8: [9], + 9: [17, 20], + 10: [11, 12], + 11: [14], + 12: [13], + 13: [21, 22], + 14: [15], + 15: [19, 23], + 16: [17, 18], + 17: [20], + 18: [19], + 19: [23], + 20: [21], + 21: [22], + 22: [23], + }, + create_using=create_using, + ) + G.name = "Truncated Cube Graph" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def truncated_tetrahedron_graph(create_using=None): + """ + Returns the skeleton of the truncated Platonic tetrahedron. + + The truncated tetrahedron is an Archimedean solid with 4 regular hexagonal faces, + 4 equilateral triangle faces, 12 nodes and 18 edges. It can be constructed by truncating + all 4 vertices of a regular tetrahedron at one third of the original edge length [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Skeleton of the truncated tetrahedron + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Truncated_tetrahedron + + """ + G = path_graph(12, create_using) + G.add_edges_from([(0, 2), (0, 9), (1, 6), (3, 11), (4, 11), (5, 7), (8, 10)]) + G.name = "Truncated Tetrahedron Graph" + return G + + +@_raise_on_directed +@nx._dispatchable(graphs=None, returns_graph=True) +def tutte_graph(create_using=None): + """ + Returns the Tutte graph. + + The Tutte graph is a cubic polyhedral, non-Hamiltonian graph. It has + 46 nodes and 69 edges. + It is a counterexample to Tait's conjecture that every 3-regular polyhedron + has a Hamiltonian cycle. + It can be realized geometrically from a tetrahedron by multiply truncating + three of its vertices [1]_. + + Parameters + ---------- + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + Returns + ------- + G : networkx Graph + Tutte graph + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Tutte_graph + """ + G = nx.from_dict_of_lists( + { + 0: [1, 2, 3], + 1: [4, 26], + 2: [10, 11], + 3: [18, 19], + 4: [5, 33], + 5: [6, 29], + 6: [7, 27], + 7: [8, 14], + 8: [9, 38], + 9: [10, 37], + 10: [39], + 11: [12, 39], + 12: [13, 35], + 13: [14, 15], + 14: [34], + 15: [16, 22], + 16: [17, 44], + 17: [18, 43], + 18: [45], + 19: [20, 45], + 20: [21, 41], + 21: [22, 23], + 22: [40], + 23: [24, 27], + 24: [25, 32], + 25: [26, 31], + 26: [33], + 27: [28], + 28: [29, 32], + 29: [30], + 30: [31, 33], + 31: [32], + 34: [35, 38], + 35: [36], + 36: [37, 39], + 37: [38], + 40: [41, 44], + 41: [42], + 42: [43, 45], + 43: [44], + }, + create_using=create_using, + ) + G.name = "Tutte's Graph" + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/social.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/social.py new file mode 100644 index 0000000000000000000000000000000000000000..d06ce00531fb840e0f2a6238aba97f7a40a3b8fc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/social.py @@ -0,0 +1,554 @@ +""" +Famous social networks. +""" + +import networkx as nx + +__all__ = [ + "karate_club_graph", + "davis_southern_women_graph", + "florentine_families_graph", + "les_miserables_graph", +] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def karate_club_graph(): + """Returns Zachary's Karate Club graph. + + Each node in the returned graph has a node attribute 'club' that + indicates the name of the club to which the member represented by that node + belongs, either 'Mr. Hi' or 'Officer'. Each edge has a weight based on the + number of contexts in which that edge's incident node members interacted. + + The dataset is derived from the 'Club After Split From Data' column of Table 3 in [1]_. + This was in turn derived from the 'Club After Fission' column of Table 1 in the + same paper. Note that the nodes are 0-indexed in NetworkX, but 1-indexed in the + paper (the 'Individual Number in Matrix C' column of Table 3 starts at 1). This + means, for example, that ``G.nodes[9]["club"]`` returns 'Officer', which + corresponds to row 10 of Table 3 in the paper. + + Examples + -------- + To get the name of the club to which a node belongs: + + >>> G = nx.karate_club_graph() + >>> G.nodes[5]["club"] + 'Mr. Hi' + >>> G.nodes[9]["club"] + 'Officer' + + References + ---------- + .. [1] Zachary, Wayne W. + "An Information Flow Model for Conflict and Fission in Small Groups." + *Journal of Anthropological Research*, 33, 452--473, (1977). + """ + # Create the set of all members, and the members of each club. + all_members = set(range(34)) + club1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 16, 17, 19, 21} + # club2 = all_members - club1 + + G = nx.Graph() + G.add_nodes_from(all_members) + G.name = "Zachary's Karate Club" + + zacharydat = """\ +0 4 5 3 3 3 3 2 2 0 2 3 2 3 0 0 0 2 0 2 0 2 0 0 0 0 0 0 0 0 0 2 0 0 +4 0 6 3 0 0 0 4 0 0 0 0 0 5 0 0 0 1 0 2 0 2 0 0 0 0 0 0 0 0 2 0 0 0 +5 6 0 3 0 0 0 4 5 1 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 0 0 0 3 0 +3 3 3 0 0 0 0 3 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 2 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 0 0 5 0 0 0 3 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +3 0 0 0 2 5 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 4 4 3 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 +2 0 5 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 3 0 4 3 +0 0 1 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 2 +2 0 0 0 3 3 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 +3 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 0 0 3 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 +3 5 3 3 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 3 +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 3 2 +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 3 4 +0 0 0 0 0 3 3 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 +2 1 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 +2 2 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 +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 3 1 +2 2 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 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 5 0 4 0 2 0 0 5 4 +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 2 0 3 0 0 0 2 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 5 2 0 0 0 0 0 0 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 4 0 0 0 2 +0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 3 0 0 0 0 0 0 0 0 4 +0 0 2 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 2 0 2 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 4 0 0 0 0 0 3 2 +0 2 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 3 +2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 7 0 0 2 0 0 0 4 4 +0 0 2 0 0 0 0 0 3 0 0 0 0 0 3 3 0 0 1 0 3 0 2 5 0 0 0 0 0 4 3 4 0 5 +0 0 0 0 0 0 0 0 4 2 0 0 0 3 2 4 0 0 2 1 1 0 3 4 0 0 2 4 2 2 3 4 5 0""" + + for row, line in enumerate(zacharydat.split("\n")): + thisrow = [int(b) for b in line.split()] + for col, entry in enumerate(thisrow): + if entry >= 1: + G.add_edge(row, col, weight=entry) + + # Add the name of each member's club as a node attribute. + for v in G: + G.nodes[v]["club"] = "Mr. Hi" if v in club1 else "Officer" + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def davis_southern_women_graph(): + """Returns Davis Southern women social network. + + This is a bipartite graph. + + References + ---------- + .. [1] A. Davis, Gardner, B. B., Gardner, M. R., 1941. Deep South. + University of Chicago Press, Chicago, IL. + """ + G = nx.Graph() + # Top nodes + women = [ + "Evelyn Jefferson", + "Laura Mandeville", + "Theresa Anderson", + "Brenda Rogers", + "Charlotte McDowd", + "Frances Anderson", + "Eleanor Nye", + "Pearl Oglethorpe", + "Ruth DeSand", + "Verne Sanderson", + "Myra Liddel", + "Katherina Rogers", + "Sylvia Avondale", + "Nora Fayette", + "Helen Lloyd", + "Dorothy Murchison", + "Olivia Carleton", + "Flora Price", + ] + G.add_nodes_from(women, bipartite=0) + # Bottom nodes + events = [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12", + "E13", + "E14", + ] + G.add_nodes_from(events, bipartite=1) + + G.add_edges_from( + [ + ("Evelyn Jefferson", "E1"), + ("Evelyn Jefferson", "E2"), + ("Evelyn Jefferson", "E3"), + ("Evelyn Jefferson", "E4"), + ("Evelyn Jefferson", "E5"), + ("Evelyn Jefferson", "E6"), + ("Evelyn Jefferson", "E8"), + ("Evelyn Jefferson", "E9"), + ("Laura Mandeville", "E1"), + ("Laura Mandeville", "E2"), + ("Laura Mandeville", "E3"), + ("Laura Mandeville", "E5"), + ("Laura Mandeville", "E6"), + ("Laura Mandeville", "E7"), + ("Laura Mandeville", "E8"), + ("Theresa Anderson", "E2"), + ("Theresa Anderson", "E3"), + ("Theresa Anderson", "E4"), + ("Theresa Anderson", "E5"), + ("Theresa Anderson", "E6"), + ("Theresa Anderson", "E7"), + ("Theresa Anderson", "E8"), + ("Theresa Anderson", "E9"), + ("Brenda Rogers", "E1"), + ("Brenda Rogers", "E3"), + ("Brenda Rogers", "E4"), + ("Brenda Rogers", "E5"), + ("Brenda Rogers", "E6"), + ("Brenda Rogers", "E7"), + ("Brenda Rogers", "E8"), + ("Charlotte McDowd", "E3"), + ("Charlotte McDowd", "E4"), + ("Charlotte McDowd", "E5"), + ("Charlotte McDowd", "E7"), + ("Frances Anderson", "E3"), + ("Frances Anderson", "E5"), + ("Frances Anderson", "E6"), + ("Frances Anderson", "E8"), + ("Eleanor Nye", "E5"), + ("Eleanor Nye", "E6"), + ("Eleanor Nye", "E7"), + ("Eleanor Nye", "E8"), + ("Pearl Oglethorpe", "E6"), + ("Pearl Oglethorpe", "E8"), + ("Pearl Oglethorpe", "E9"), + ("Ruth DeSand", "E5"), + ("Ruth DeSand", "E7"), + ("Ruth DeSand", "E8"), + ("Ruth DeSand", "E9"), + ("Verne Sanderson", "E7"), + ("Verne Sanderson", "E8"), + ("Verne Sanderson", "E9"), + ("Verne Sanderson", "E12"), + ("Myra Liddel", "E8"), + ("Myra Liddel", "E9"), + ("Myra Liddel", "E10"), + ("Myra Liddel", "E12"), + ("Katherina Rogers", "E8"), + ("Katherina Rogers", "E9"), + ("Katherina Rogers", "E10"), + ("Katherina Rogers", "E12"), + ("Katherina Rogers", "E13"), + ("Katherina Rogers", "E14"), + ("Sylvia Avondale", "E7"), + ("Sylvia Avondale", "E8"), + ("Sylvia Avondale", "E9"), + ("Sylvia Avondale", "E10"), + ("Sylvia Avondale", "E12"), + ("Sylvia Avondale", "E13"), + ("Sylvia Avondale", "E14"), + ("Nora Fayette", "E6"), + ("Nora Fayette", "E7"), + ("Nora Fayette", "E9"), + ("Nora Fayette", "E10"), + ("Nora Fayette", "E11"), + ("Nora Fayette", "E12"), + ("Nora Fayette", "E13"), + ("Nora Fayette", "E14"), + ("Helen Lloyd", "E7"), + ("Helen Lloyd", "E8"), + ("Helen Lloyd", "E10"), + ("Helen Lloyd", "E11"), + ("Helen Lloyd", "E12"), + ("Dorothy Murchison", "E8"), + ("Dorothy Murchison", "E9"), + ("Olivia Carleton", "E9"), + ("Olivia Carleton", "E11"), + ("Flora Price", "E9"), + ("Flora Price", "E11"), + ] + ) + G.graph["top"] = women + G.graph["bottom"] = events + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def florentine_families_graph(): + """Returns Florentine families graph. + + References + ---------- + .. [1] Ronald L. Breiger and Philippa E. Pattison + Cumulated social roles: The duality of persons and their algebras,1 + Social Networks, Volume 8, Issue 3, September 1986, Pages 215-256 + """ + G = nx.Graph() + G.add_edge("Acciaiuoli", "Medici") + G.add_edge("Castellani", "Peruzzi") + G.add_edge("Castellani", "Strozzi") + G.add_edge("Castellani", "Barbadori") + G.add_edge("Medici", "Barbadori") + G.add_edge("Medici", "Ridolfi") + G.add_edge("Medici", "Tornabuoni") + G.add_edge("Medici", "Albizzi") + G.add_edge("Medici", "Salviati") + G.add_edge("Salviati", "Pazzi") + G.add_edge("Peruzzi", "Strozzi") + G.add_edge("Peruzzi", "Bischeri") + G.add_edge("Strozzi", "Ridolfi") + G.add_edge("Strozzi", "Bischeri") + G.add_edge("Ridolfi", "Tornabuoni") + G.add_edge("Tornabuoni", "Guadagni") + G.add_edge("Albizzi", "Ginori") + G.add_edge("Albizzi", "Guadagni") + G.add_edge("Bischeri", "Guadagni") + G.add_edge("Guadagni", "Lamberteschi") + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def les_miserables_graph(): + """Returns coappearance network of characters in the novel Les Miserables. + + References + ---------- + .. [1] D. E. Knuth, 1993. + The Stanford GraphBase: a platform for combinatorial computing, + pp. 74-87. New York: AcM Press. + """ + G = nx.Graph() + G.add_edge("Napoleon", "Myriel", weight=1) + G.add_edge("MlleBaptistine", "Myriel", weight=8) + G.add_edge("MmeMagloire", "Myriel", weight=10) + G.add_edge("MmeMagloire", "MlleBaptistine", weight=6) + G.add_edge("CountessDeLo", "Myriel", weight=1) + G.add_edge("Geborand", "Myriel", weight=1) + G.add_edge("Champtercier", "Myriel", weight=1) + G.add_edge("Cravatte", "Myriel", weight=1) + G.add_edge("Count", "Myriel", weight=2) + G.add_edge("OldMan", "Myriel", weight=1) + G.add_edge("Valjean", "Labarre", weight=1) + G.add_edge("Valjean", "MmeMagloire", weight=3) + G.add_edge("Valjean", "MlleBaptistine", weight=3) + G.add_edge("Valjean", "Myriel", weight=5) + G.add_edge("Marguerite", "Valjean", weight=1) + G.add_edge("MmeDeR", "Valjean", weight=1) + G.add_edge("Isabeau", "Valjean", weight=1) + G.add_edge("Gervais", "Valjean", weight=1) + G.add_edge("Listolier", "Tholomyes", weight=4) + G.add_edge("Fameuil", "Tholomyes", weight=4) + G.add_edge("Fameuil", "Listolier", weight=4) + G.add_edge("Blacheville", "Tholomyes", weight=4) + G.add_edge("Blacheville", "Listolier", weight=4) + G.add_edge("Blacheville", "Fameuil", weight=4) + G.add_edge("Favourite", "Tholomyes", weight=3) + G.add_edge("Favourite", "Listolier", weight=3) + G.add_edge("Favourite", "Fameuil", weight=3) + G.add_edge("Favourite", "Blacheville", weight=4) + G.add_edge("Dahlia", "Tholomyes", weight=3) + G.add_edge("Dahlia", "Listolier", weight=3) + G.add_edge("Dahlia", "Fameuil", weight=3) + G.add_edge("Dahlia", "Blacheville", weight=3) + G.add_edge("Dahlia", "Favourite", weight=5) + G.add_edge("Zephine", "Tholomyes", weight=3) + G.add_edge("Zephine", "Listolier", weight=3) + G.add_edge("Zephine", "Fameuil", weight=3) + G.add_edge("Zephine", "Blacheville", weight=3) + G.add_edge("Zephine", "Favourite", weight=4) + G.add_edge("Zephine", "Dahlia", weight=4) + G.add_edge("Fantine", "Tholomyes", weight=3) + G.add_edge("Fantine", "Listolier", weight=3) + G.add_edge("Fantine", "Fameuil", weight=3) + G.add_edge("Fantine", "Blacheville", weight=3) + G.add_edge("Fantine", "Favourite", weight=4) + G.add_edge("Fantine", "Dahlia", weight=4) + G.add_edge("Fantine", "Zephine", weight=4) + G.add_edge("Fantine", "Marguerite", weight=2) + G.add_edge("Fantine", "Valjean", weight=9) + G.add_edge("MmeThenardier", "Fantine", weight=2) + G.add_edge("MmeThenardier", "Valjean", weight=7) + G.add_edge("Thenardier", "MmeThenardier", weight=13) + G.add_edge("Thenardier", "Fantine", weight=1) + G.add_edge("Thenardier", "Valjean", weight=12) + G.add_edge("Cosette", "MmeThenardier", weight=4) + G.add_edge("Cosette", "Valjean", weight=31) + G.add_edge("Cosette", "Tholomyes", weight=1) + G.add_edge("Cosette", "Thenardier", weight=1) + G.add_edge("Javert", "Valjean", weight=17) + G.add_edge("Javert", "Fantine", weight=5) + G.add_edge("Javert", "Thenardier", weight=5) + G.add_edge("Javert", "MmeThenardier", weight=1) + G.add_edge("Javert", "Cosette", weight=1) + G.add_edge("Fauchelevent", "Valjean", weight=8) + G.add_edge("Fauchelevent", "Javert", weight=1) + G.add_edge("Bamatabois", "Fantine", weight=1) + G.add_edge("Bamatabois", "Javert", weight=1) + G.add_edge("Bamatabois", "Valjean", weight=2) + G.add_edge("Perpetue", "Fantine", weight=1) + G.add_edge("Simplice", "Perpetue", weight=2) + G.add_edge("Simplice", "Valjean", weight=3) + G.add_edge("Simplice", "Fantine", weight=2) + G.add_edge("Simplice", "Javert", weight=1) + G.add_edge("Scaufflaire", "Valjean", weight=1) + G.add_edge("Woman1", "Valjean", weight=2) + G.add_edge("Woman1", "Javert", weight=1) + G.add_edge("Judge", "Valjean", weight=3) + G.add_edge("Judge", "Bamatabois", weight=2) + G.add_edge("Champmathieu", "Valjean", weight=3) + G.add_edge("Champmathieu", "Judge", weight=3) + G.add_edge("Champmathieu", "Bamatabois", weight=2) + G.add_edge("Brevet", "Judge", weight=2) + G.add_edge("Brevet", "Champmathieu", weight=2) + G.add_edge("Brevet", "Valjean", weight=2) + G.add_edge("Brevet", "Bamatabois", weight=1) + G.add_edge("Chenildieu", "Judge", weight=2) + G.add_edge("Chenildieu", "Champmathieu", weight=2) + G.add_edge("Chenildieu", "Brevet", weight=2) + G.add_edge("Chenildieu", "Valjean", weight=2) + G.add_edge("Chenildieu", "Bamatabois", weight=1) + G.add_edge("Cochepaille", "Judge", weight=2) + G.add_edge("Cochepaille", "Champmathieu", weight=2) + G.add_edge("Cochepaille", "Brevet", weight=2) + G.add_edge("Cochepaille", "Chenildieu", weight=2) + G.add_edge("Cochepaille", "Valjean", weight=2) + G.add_edge("Cochepaille", "Bamatabois", weight=1) + G.add_edge("Pontmercy", "Thenardier", weight=1) + G.add_edge("Boulatruelle", "Thenardier", weight=1) + G.add_edge("Eponine", "MmeThenardier", weight=2) + G.add_edge("Eponine", "Thenardier", weight=3) + G.add_edge("Anzelma", "Eponine", weight=2) + G.add_edge("Anzelma", "Thenardier", weight=2) + G.add_edge("Anzelma", "MmeThenardier", weight=1) + G.add_edge("Woman2", "Valjean", weight=3) + G.add_edge("Woman2", "Cosette", weight=1) + G.add_edge("Woman2", "Javert", weight=1) + G.add_edge("MotherInnocent", "Fauchelevent", weight=3) + G.add_edge("MotherInnocent", "Valjean", weight=1) + G.add_edge("Gribier", "Fauchelevent", weight=2) + G.add_edge("MmeBurgon", "Jondrette", weight=1) + G.add_edge("Gavroche", "MmeBurgon", weight=2) + G.add_edge("Gavroche", "Thenardier", weight=1) + G.add_edge("Gavroche", "Javert", weight=1) + G.add_edge("Gavroche", "Valjean", weight=1) + G.add_edge("Gillenormand", "Cosette", weight=3) + G.add_edge("Gillenormand", "Valjean", weight=2) + G.add_edge("Magnon", "Gillenormand", weight=1) + G.add_edge("Magnon", "MmeThenardier", weight=1) + G.add_edge("MlleGillenormand", "Gillenormand", weight=9) + G.add_edge("MlleGillenormand", "Cosette", weight=2) + G.add_edge("MlleGillenormand", "Valjean", weight=2) + G.add_edge("MmePontmercy", "MlleGillenormand", weight=1) + G.add_edge("MmePontmercy", "Pontmercy", weight=1) + G.add_edge("MlleVaubois", "MlleGillenormand", weight=1) + G.add_edge("LtGillenormand", "MlleGillenormand", weight=2) + G.add_edge("LtGillenormand", "Gillenormand", weight=1) + G.add_edge("LtGillenormand", "Cosette", weight=1) + G.add_edge("Marius", "MlleGillenormand", weight=6) + G.add_edge("Marius", "Gillenormand", weight=12) + G.add_edge("Marius", "Pontmercy", weight=1) + G.add_edge("Marius", "LtGillenormand", weight=1) + G.add_edge("Marius", "Cosette", weight=21) + G.add_edge("Marius", "Valjean", weight=19) + G.add_edge("Marius", "Tholomyes", weight=1) + G.add_edge("Marius", "Thenardier", weight=2) + G.add_edge("Marius", "Eponine", weight=5) + G.add_edge("Marius", "Gavroche", weight=4) + G.add_edge("BaronessT", "Gillenormand", weight=1) + G.add_edge("BaronessT", "Marius", weight=1) + G.add_edge("Mabeuf", "Marius", weight=1) + G.add_edge("Mabeuf", "Eponine", weight=1) + G.add_edge("Mabeuf", "Gavroche", weight=1) + G.add_edge("Enjolras", "Marius", weight=7) + G.add_edge("Enjolras", "Gavroche", weight=7) + G.add_edge("Enjolras", "Javert", weight=6) + G.add_edge("Enjolras", "Mabeuf", weight=1) + G.add_edge("Enjolras", "Valjean", weight=4) + G.add_edge("Combeferre", "Enjolras", weight=15) + G.add_edge("Combeferre", "Marius", weight=5) + G.add_edge("Combeferre", "Gavroche", weight=6) + G.add_edge("Combeferre", "Mabeuf", weight=2) + G.add_edge("Prouvaire", "Gavroche", weight=1) + G.add_edge("Prouvaire", "Enjolras", weight=4) + G.add_edge("Prouvaire", "Combeferre", weight=2) + G.add_edge("Feuilly", "Gavroche", weight=2) + G.add_edge("Feuilly", "Enjolras", weight=6) + G.add_edge("Feuilly", "Prouvaire", weight=2) + G.add_edge("Feuilly", "Combeferre", weight=5) + G.add_edge("Feuilly", "Mabeuf", weight=1) + G.add_edge("Feuilly", "Marius", weight=1) + G.add_edge("Courfeyrac", "Marius", weight=9) + G.add_edge("Courfeyrac", "Enjolras", weight=17) + G.add_edge("Courfeyrac", "Combeferre", weight=13) + G.add_edge("Courfeyrac", "Gavroche", weight=7) + G.add_edge("Courfeyrac", "Mabeuf", weight=2) + G.add_edge("Courfeyrac", "Eponine", weight=1) + G.add_edge("Courfeyrac", "Feuilly", weight=6) + G.add_edge("Courfeyrac", "Prouvaire", weight=3) + G.add_edge("Bahorel", "Combeferre", weight=5) + G.add_edge("Bahorel", "Gavroche", weight=5) + G.add_edge("Bahorel", "Courfeyrac", weight=6) + G.add_edge("Bahorel", "Mabeuf", weight=2) + G.add_edge("Bahorel", "Enjolras", weight=4) + G.add_edge("Bahorel", "Feuilly", weight=3) + G.add_edge("Bahorel", "Prouvaire", weight=2) + G.add_edge("Bahorel", "Marius", weight=1) + G.add_edge("Bossuet", "Marius", weight=5) + G.add_edge("Bossuet", "Courfeyrac", weight=12) + G.add_edge("Bossuet", "Gavroche", weight=5) + G.add_edge("Bossuet", "Bahorel", weight=4) + G.add_edge("Bossuet", "Enjolras", weight=10) + G.add_edge("Bossuet", "Feuilly", weight=6) + G.add_edge("Bossuet", "Prouvaire", weight=2) + G.add_edge("Bossuet", "Combeferre", weight=9) + G.add_edge("Bossuet", "Mabeuf", weight=1) + G.add_edge("Bossuet", "Valjean", weight=1) + G.add_edge("Joly", "Bahorel", weight=5) + G.add_edge("Joly", "Bossuet", weight=7) + G.add_edge("Joly", "Gavroche", weight=3) + G.add_edge("Joly", "Courfeyrac", weight=5) + G.add_edge("Joly", "Enjolras", weight=5) + G.add_edge("Joly", "Feuilly", weight=5) + G.add_edge("Joly", "Prouvaire", weight=2) + G.add_edge("Joly", "Combeferre", weight=5) + G.add_edge("Joly", "Mabeuf", weight=1) + G.add_edge("Joly", "Marius", weight=2) + G.add_edge("Grantaire", "Bossuet", weight=3) + G.add_edge("Grantaire", "Enjolras", weight=3) + G.add_edge("Grantaire", "Combeferre", weight=1) + G.add_edge("Grantaire", "Courfeyrac", weight=2) + G.add_edge("Grantaire", "Joly", weight=2) + G.add_edge("Grantaire", "Gavroche", weight=1) + G.add_edge("Grantaire", "Bahorel", weight=1) + G.add_edge("Grantaire", "Feuilly", weight=1) + G.add_edge("Grantaire", "Prouvaire", weight=1) + G.add_edge("MotherPlutarch", "Mabeuf", weight=3) + G.add_edge("Gueulemer", "Thenardier", weight=5) + G.add_edge("Gueulemer", "Valjean", weight=1) + G.add_edge("Gueulemer", "MmeThenardier", weight=1) + G.add_edge("Gueulemer", "Javert", weight=1) + G.add_edge("Gueulemer", "Gavroche", weight=1) + G.add_edge("Gueulemer", "Eponine", weight=1) + G.add_edge("Babet", "Thenardier", weight=6) + G.add_edge("Babet", "Gueulemer", weight=6) + G.add_edge("Babet", "Valjean", weight=1) + G.add_edge("Babet", "MmeThenardier", weight=1) + G.add_edge("Babet", "Javert", weight=2) + G.add_edge("Babet", "Gavroche", weight=1) + G.add_edge("Babet", "Eponine", weight=1) + G.add_edge("Claquesous", "Thenardier", weight=4) + G.add_edge("Claquesous", "Babet", weight=4) + G.add_edge("Claquesous", "Gueulemer", weight=4) + G.add_edge("Claquesous", "Valjean", weight=1) + G.add_edge("Claquesous", "MmeThenardier", weight=1) + G.add_edge("Claquesous", "Javert", weight=1) + G.add_edge("Claquesous", "Eponine", weight=1) + G.add_edge("Claquesous", "Enjolras", weight=1) + G.add_edge("Montparnasse", "Javert", weight=1) + G.add_edge("Montparnasse", "Babet", weight=2) + G.add_edge("Montparnasse", "Gueulemer", weight=2) + G.add_edge("Montparnasse", "Claquesous", weight=2) + G.add_edge("Montparnasse", "Valjean", weight=1) + G.add_edge("Montparnasse", "Gavroche", weight=1) + G.add_edge("Montparnasse", "Eponine", weight=1) + G.add_edge("Montparnasse", "Thenardier", weight=1) + G.add_edge("Toussaint", "Cosette", weight=2) + G.add_edge("Toussaint", "Javert", weight=1) + G.add_edge("Toussaint", "Valjean", weight=1) + G.add_edge("Child1", "Gavroche", weight=2) + G.add_edge("Child2", "Gavroche", weight=2) + G.add_edge("Child2", "Child1", weight=3) + G.add_edge("Brujon", "Babet", weight=3) + G.add_edge("Brujon", "Gueulemer", weight=3) + G.add_edge("Brujon", "Thenardier", weight=3) + G.add_edge("Brujon", "Gavroche", weight=1) + G.add_edge("Brujon", "Eponine", weight=1) + G.add_edge("Brujon", "Claquesous", weight=1) + G.add_edge("Brujon", "Montparnasse", weight=1) + G.add_edge("MmeHucheloup", "Bossuet", weight=1) + G.add_edge("MmeHucheloup", "Joly", weight=1) + G.add_edge("MmeHucheloup", "Grantaire", weight=1) + G.add_edge("MmeHucheloup", "Bahorel", weight=1) + G.add_edge("MmeHucheloup", "Courfeyrac", weight=1) + G.add_edge("MmeHucheloup", "Gavroche", weight=1) + G.add_edge("MmeHucheloup", "Enjolras", weight=1) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py new file mode 100644 index 0000000000000000000000000000000000000000..aa8c9194bb31ce15434c728c607bfe0172402406 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/spectral_graph_forge.py @@ -0,0 +1,120 @@ +"""Generates graphs with a given eigenvector structure""" + +import networkx as nx +from networkx.utils import np_random_state + +__all__ = ["spectral_graph_forge"] + + +@np_random_state(3) +@nx._dispatchable(preserve_edge_attrs={"G": {"weight": 1}}, returns_graph=True) +def spectral_graph_forge(G, alpha, transformation="identity", seed=None): + """Returns a random simple graph with spectrum resembling that of `G` + + This algorithm, called Spectral Graph Forge (SGF), computes the + eigenvectors of a given graph adjacency matrix, filters them and + builds a random graph with a similar eigenstructure. + SGF has been proved to be particularly useful for synthesizing + realistic social networks and it can also be used to anonymize + graph sensitive data. + + Parameters + ---------- + G : Graph + alpha : float + Ratio representing the percentage of eigenvectors of G to consider, + values in [0,1]. + transformation : string, optional + Represents the intended matrix linear transformation, possible values + are 'identity' and 'modularity' + seed : integer, random_state, or None (default) + Indicator of numpy random number generation state. + See :ref:`Randomness`. + + Returns + ------- + H : Graph + A graph with a similar eigenvector structure of the input one. + + Raises + ------ + NetworkXError + If transformation has a value different from 'identity' or 'modularity' + + Notes + ----- + Spectral Graph Forge (SGF) generates a random simple graph resembling the + global properties of the given one. + It leverages the low-rank approximation of the associated adjacency matrix + driven by the *alpha* precision parameter. + SGF preserves the number of nodes of the input graph and their ordering. + This way, nodes of output graphs resemble the properties of the input one + and attributes can be directly mapped. + + It considers the graph adjacency matrices which can optionally be + transformed to other symmetric real matrices (currently transformation + options include *identity* and *modularity*). + The *modularity* transformation, in the sense of Newman's modularity matrix + allows the focusing on community structure related properties of the graph. + + SGF applies a low-rank approximation whose fixed rank is computed from the + ratio *alpha* of the input graph adjacency matrix dimension. + This step performs a filtering on the input eigenvectors similar to the low + pass filtering common in telecommunications. + + The filtered values (after truncation) are used as input to a Bernoulli + sampling for constructing a random adjacency matrix. + + References + ---------- + .. [1] L. Baldesi, C. T. Butts, A. Markopoulou, "Spectral Graph Forge: + Graph Generation Targeting Modularity", IEEE Infocom, '18. + https://arxiv.org/abs/1801.01715 + .. [2] M. Newman, "Networks: an introduction", Oxford university press, + 2010 + + Examples + -------- + >>> G = nx.karate_club_graph() + >>> H = nx.spectral_graph_forge(G, 0.3) + >>> + """ + import numpy as np + import scipy as sp + + available_transformations = ["identity", "modularity"] + alpha = np.clip(alpha, 0, 1) + A = nx.to_numpy_array(G) + n = A.shape[1] + level = round(n * alpha) + + if transformation not in available_transformations: + msg = f"{transformation!r} is not a valid transformation. " + msg += f"Transformations: {available_transformations}" + raise nx.NetworkXError(msg) + + K = np.ones((1, n)) @ A + + B = A + if transformation == "modularity": + B -= K.T @ K / K.sum() + + # Compute low-rank approximation of B + evals, evecs = np.linalg.eigh(B) + k = np.argsort(np.abs(evals))[::-1] # indices of evals in descending order + evecs[:, k[np.arange(level, n)]] = 0 # set smallest eigenvectors to 0 + B = evecs @ np.diag(evals) @ evecs.T + + if transformation == "modularity": + B += K.T @ K / K.sum() + + B = np.clip(B, 0, 1) + np.fill_diagonal(B, 0) + + for i in range(n - 1): + B[i, i + 1 :] = sp.stats.bernoulli.rvs(B[i, i + 1 :], random_state=seed) + B[i + 1 :, i] = np.transpose(B[i, i + 1 :]) + + H = nx.from_numpy_array(B) + + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/stochastic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/stochastic.py new file mode 100644 index 0000000000000000000000000000000000000000..f53e2315470f8ffcdea0380026a933e06ddf6ea7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/stochastic.py @@ -0,0 +1,54 @@ +"""Functions for generating stochastic graphs from a given weighted directed +graph. + +""" + +import networkx as nx +from networkx.classes import DiGraph, MultiDiGraph +from networkx.utils import not_implemented_for + +__all__ = ["stochastic_graph"] + + +@not_implemented_for("undirected") +@nx._dispatchable( + edge_attrs="weight", mutates_input={"not copy": 1}, returns_graph=True +) +def stochastic_graph(G, copy=True, weight="weight"): + """Returns a right-stochastic representation of directed graph `G`. + + A right-stochastic graph is a weighted digraph in which for each + node, the sum of the weights of all the out-edges of that node is + 1. If the graph is already weighted (for example, via a 'weight' + edge attribute), the reweighting takes that into account. + + Parameters + ---------- + G : directed graph + A :class:`~networkx.DiGraph` or :class:`~networkx.MultiDiGraph`. + + copy : boolean, optional + If this is True, then this function returns a new graph with + the stochastic reweighting. Otherwise, the original graph is + modified in-place (and also returned, for convenience). + + weight : edge attribute key (optional, default='weight') + Edge attribute key used for reading the existing weight and + setting the new weight. If no attribute with this key is found + for an edge, then the edge weight is assumed to be 1. If an edge + has a weight, it must be a positive number. + + """ + if copy: + G = MultiDiGraph(G) if G.is_multigraph() else DiGraph(G) + # There is a tradeoff here: the dictionary of node degrees may + # require a lot of memory, whereas making a call to `G.out_degree` + # inside the loop may be costly in computation time. + degree = dict(G.out_degree(weight=weight)) + for u, v, d in G.edges(data=True): + if degree[u] == 0: + d[weight] = 0 + else: + d[weight] = d.get(weight, 1) / degree[u] + nx._clear_cache(G) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/sudoku.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/sudoku.py new file mode 100644 index 0000000000000000000000000000000000000000..f288ed24d1f189588de7e1e0bba61f50bbad0003 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/sudoku.py @@ -0,0 +1,131 @@ +"""Generator for Sudoku graphs + +This module gives a generator for n-Sudoku graphs. It can be used to develop +algorithms for solving or generating Sudoku puzzles. + +A completed Sudoku grid is a 9x9 array of integers between 1 and 9, with no +number appearing twice in the same row, column, or 3x3 box. + ++---------+---------+---------+ +| | 8 6 4 | | 3 7 1 | | 2 5 9 | +| | 3 2 5 | | 8 4 9 | | 7 6 1 | +| | 9 7 1 | | 2 6 5 | | 8 4 3 | ++---------+---------+---------+ +| | 4 3 6 | | 1 9 2 | | 5 8 7 | +| | 1 9 8 | | 6 5 7 | | 4 3 2 | +| | 2 5 7 | | 4 8 3 | | 9 1 6 | ++---------+---------+---------+ +| | 6 8 9 | | 7 3 4 | | 1 2 5 | +| | 7 1 3 | | 5 2 8 | | 6 9 4 | +| | 5 4 2 | | 9 1 6 | | 3 7 8 | ++---------+---------+---------+ + + +The Sudoku graph is an undirected graph with 81 vertices, corresponding to +the cells of a Sudoku grid. It is a regular graph of degree 20. Two distinct +vertices are adjacent if and only if the corresponding cells belong to the +same row, column, or box. A completed Sudoku grid corresponds to a vertex +coloring of the Sudoku graph with nine colors. + +More generally, the n-Sudoku graph is a graph with n^4 vertices, corresponding +to the cells of an n^2 by n^2 grid. Two distinct vertices are adjacent if and +only if they belong to the same row, column, or n by n box. + +References +---------- +.. [1] Herzberg, A. M., & Murty, M. R. (2007). Sudoku squares and chromatic + polynomials. Notices of the AMS, 54(6), 708-717. +.. [2] Sander, Torsten (2009), "Sudoku graphs are integral", + Electronic Journal of Combinatorics, 16 (1): Note 25, 7pp, MR 2529816 +.. [3] Wikipedia contributors. "Glossary of Sudoku." Wikipedia, The Free + Encyclopedia, 3 Dec. 2019. Web. 22 Dec. 2019. +""" + +import networkx as nx +from networkx.exception import NetworkXError + +__all__ = ["sudoku_graph"] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def sudoku_graph(n=3): + """Returns the n-Sudoku graph. The default value of n is 3. + + The n-Sudoku graph is a graph with n^4 vertices, corresponding to the + cells of an n^2 by n^2 grid. Two distinct vertices are adjacent if and + only if they belong to the same row, column, or n-by-n box. + + Parameters + ---------- + n: integer + The order of the Sudoku graph, equal to the square root of the + number of rows. The default is 3. + + Returns + ------- + NetworkX graph + The n-Sudoku graph Sud(n). + + Examples + -------- + >>> G = nx.sudoku_graph() + >>> G.number_of_nodes() + 81 + >>> G.number_of_edges() + 810 + >>> sorted(G.neighbors(42)) + [6, 15, 24, 33, 34, 35, 36, 37, 38, 39, 40, 41, 43, 44, 51, 52, 53, 60, 69, 78] + >>> G = nx.sudoku_graph(2) + >>> G.number_of_nodes() + 16 + >>> G.number_of_edges() + 56 + + References + ---------- + .. [1] Herzberg, A. M., & Murty, M. R. (2007). Sudoku squares and chromatic + polynomials. Notices of the AMS, 54(6), 708-717. + .. [2] Sander, Torsten (2009), "Sudoku graphs are integral", + Electronic Journal of Combinatorics, 16 (1): Note 25, 7pp, MR 2529816 + .. [3] Wikipedia contributors. "Glossary of Sudoku." Wikipedia, The Free + Encyclopedia, 3 Dec. 2019. Web. 22 Dec. 2019. + """ + + if n < 0: + raise NetworkXError("The order must be greater than or equal to zero.") + + n2 = n * n + n3 = n2 * n + n4 = n3 * n + + # Construct an empty graph with n^4 nodes + G = nx.empty_graph(n4) + + # A Sudoku graph of order 0 or 1 has no edges + if n < 2: + return G + + # Add edges for cells in the same row + for row_no in range(n2): + row_start = row_no * n2 + for j in range(1, n2): + for i in range(j): + G.add_edge(row_start + i, row_start + j) + + # Add edges for cells in the same column + for col_no in range(n2): + for j in range(col_no, n4, n2): + for i in range(col_no, j, n2): + G.add_edge(i, j) + + # Add edges for cells in the same box + for band_no in range(n): + for stack_no in range(n): + box_start = n3 * band_no + n * stack_no + for j in range(1, n2): + for i in range(j): + u = box_start + (i % n) + n2 * (i // n) + v = box_start + (j % n) + n2 * (j // n) + G.add_edge(u, v) + + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a7451a0d6c614e52883bec7d2dd68d056e62fbc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_atlas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_atlas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c84ea633a688be9dd4a6072383515b45040f52b6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_atlas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_classic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_classic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3be7a471d8ec5a3a088c968b289c579dec07490e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_classic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_cographs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_cographs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a1e8e71469b66e2202d8fb45c55272470f7b416 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_cographs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_community.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_community.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2580fca18950bfd293c5f78c8451fdfdbc6ddef5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_community.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_degree_seq.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_degree_seq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e51cf00f3d42d0fc98e48c8f17d8ddc9bb14db41 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_degree_seq.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_directed.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_directed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f353ef20d4e656e6b3bffd13fa69e607dfa56661 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_directed.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_duplication.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_duplication.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8a56b6d5502aef38619d0571e6bba9581a26fab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_duplication.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_ego.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_ego.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ab860134b30fd51cf68207abb285b30070dcb07 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_ego.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_expanders.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_expanders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60da25cc36d6fd127602c84c324b10c6bd8a7352 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_expanders.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_geometric.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_geometric.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eac12bdd7abe1e8f0367b47df142e12a6ea2ed11 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_geometric.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_harary_graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_harary_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33fb0786ef34bb18c9aa3b8a3fec0bcdd956144b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_harary_graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_internet_as_graphs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_internet_as_graphs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15b4b521076669719432a142b9b440977e8456fb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_internet_as_graphs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_intersection.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_intersection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e48986fad1e7df3a44561f77dc72b090e860188b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_intersection.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_interval_graph.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_interval_graph.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88331686ac6caba184a6793f700e604e92431e08 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_interval_graph.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_joint_degree_seq.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_joint_degree_seq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e87ccd2bbeace67e9b443037892046da9a567425 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_joint_degree_seq.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_lattice.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_lattice.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5e01b716e240e4b331566cbb18f3f1f11836413 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_lattice.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_line.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_line.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3654d891338b7fd8aec644ede76b686ec287769e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_line.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_mycielski.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_mycielski.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bdcc87a7abfce08c3ce017ebb1b70564f7ad38f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_mycielski.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_nonisomorphic_trees.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_nonisomorphic_trees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca701fde058083df9e77894744b9b0ee8e92be8b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_nonisomorphic_trees.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_clustered.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_clustered.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ba6b5171cb2d023afc72a644dd8f0b3db5479a7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_clustered.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_graphs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_graphs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b966a47a840a4b32d9d7864843dfacf611256b19 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_random_graphs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_small.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_small.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4df498694f302454869e1b963e1c9a01c0aa9db6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_small.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_spectral_graph_forge.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_spectral_graph_forge.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95eae1fbf6c30fd26d0a8a857cadeb57a60cde98 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_spectral_graph_forge.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_stochastic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_stochastic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13c667b6c0c1efd16b92a54aa83655cd94c690c6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_stochastic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_sudoku.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_sudoku.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d65628f2d5dd9068e0a997a95c60d051d9b42b6f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_sudoku.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_time_series.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_time_series.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d97ae7e83504f06f38020cd0320ba53d9baacf9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_time_series.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_trees.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_trees.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b79a97f99ecdfe4023c2149de4bf705e43cfddbf Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_trees.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_triads.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_triads.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7eaeff09c49f192bfa12e5dfc8252142ab6d70af Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/__pycache__/test_triads.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py new file mode 100644 index 0000000000000000000000000000000000000000..add4741c00e8d8aefe4fcf3a2a86815a15aab29c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_atlas.py @@ -0,0 +1,75 @@ +from itertools import groupby + +import pytest + +import networkx as nx +from networkx import graph_atlas, graph_atlas_g +from networkx.generators.atlas import NUM_GRAPHS +from networkx.utils import edges_equal, nodes_equal, pairwise + + +class TestAtlasGraph: + """Unit tests for the :func:`~networkx.graph_atlas` function.""" + + def test_index_too_small(self): + with pytest.raises(ValueError): + graph_atlas(-1) + + def test_index_too_large(self): + with pytest.raises(ValueError): + graph_atlas(NUM_GRAPHS) + + def test_graph(self): + G = graph_atlas(6) + assert nodes_equal(G.nodes(), range(3)) + assert edges_equal(G.edges(), [(0, 1), (0, 2)]) + + +class TestAtlasGraphG: + """Unit tests for the :func:`~networkx.graph_atlas_g` function.""" + + @classmethod + def setup_class(cls): + cls.GAG = graph_atlas_g() + + def test_sizes(self): + G = self.GAG[0] + assert G.number_of_nodes() == 0 + assert G.number_of_edges() == 0 + + G = self.GAG[7] + assert G.number_of_nodes() == 3 + assert G.number_of_edges() == 3 + + def test_names(self): + for i, G in enumerate(self.GAG): + assert int(G.name[1:]) == i + + def test_nondecreasing_nodes(self): + # check for nondecreasing number of nodes + for n1, n2 in pairwise(map(len, self.GAG)): + assert n2 <= n1 + 1 + + def test_nondecreasing_edges(self): + # check for nondecreasing number of edges (for fixed number of + # nodes) + for n, group in groupby(self.GAG, key=nx.number_of_nodes): + for m1, m2 in pairwise(map(nx.number_of_edges, group)): + assert m2 <= m1 + 1 + + def test_nondecreasing_degree_sequence(self): + # Check for lexicographically nondecreasing degree sequences + # (for fixed number of nodes and edges). + # + # There are three exceptions to this rule in the order given in + # the "Atlas of Graphs" book, so we need to manually exclude + # those. + exceptions = [("G55", "G56"), ("G1007", "G1008"), ("G1012", "G1013")] + for n, group in groupby(self.GAG, key=nx.number_of_nodes): + for m, group in groupby(group, key=nx.number_of_edges): + for G1, G2 in pairwise(group): + if (G1.name, G2.name) in exceptions: + continue + d1 = sorted(d for v, d in G1.degree()) + d2 = sorted(d for v, d in G2.degree()) + assert d1 <= d2 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py new file mode 100644 index 0000000000000000000000000000000000000000..bec6e69e99141561bbb0c86dd54810c9f57c75b8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_classic.py @@ -0,0 +1,642 @@ +""" +==================== +Generators - Classic +==================== + +Unit tests for various classic graph generators in generators/classic.py +""" + +import itertools +import typing + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + + +class TestGeneratorClassic: + def test_balanced_tree(self): + # balanced_tree(r,h) is a tree with (r**(h+1)-1)/(r-1) edges + for r, h in [(2, 2), (3, 3), (6, 2)]: + t = nx.balanced_tree(r, h) + order = t.order() + assert order == (r ** (h + 1) - 1) / (r - 1) + assert nx.is_connected(t) + assert t.size() == order - 1 + dh = nx.degree_histogram(t) + assert dh[0] == 0 # no nodes of 0 + assert dh[1] == r**h # nodes of degree 1 are leaves + assert dh[r] == 1 # root is degree r + assert dh[r + 1] == order - r**h - 1 # everyone else is degree r+1 + assert len(dh) == r + 2 + + def test_balanced_tree_star(self): + # balanced_tree(r,1) is the r-star + t = nx.balanced_tree(r=2, h=1) + assert nx.could_be_isomorphic(t, nx.star_graph(2)) + t = nx.balanced_tree(r=5, h=1) + assert nx.could_be_isomorphic(t, nx.star_graph(5)) + t = nx.balanced_tree(r=10, h=1) + assert nx.could_be_isomorphic(t, nx.star_graph(10)) + + def test_balanced_tree_path(self): + """Tests that the balanced tree with branching factor one is the + path graph. + + """ + # A tree of height four has five levels. + T = nx.balanced_tree(1, 4) + P = nx.path_graph(5) + assert nx.could_be_isomorphic(T, P) + + def test_full_rary_tree(self): + r = 2 + n = 9 + t = nx.full_rary_tree(r, n) + assert t.order() == n + assert nx.is_connected(t) + dh = nx.degree_histogram(t) + assert dh[0] == 0 # no nodes of 0 + assert dh[1] == 5 # nodes of degree 1 are leaves + assert dh[r] == 1 # root is degree r + assert dh[r + 1] == 9 - 5 - 1 # everyone else is degree r+1 + assert len(dh) == r + 2 + + def test_full_rary_tree_balanced(self): + t = nx.full_rary_tree(2, 15) + th = nx.balanced_tree(2, 3) + assert nx.could_be_isomorphic(t, th) + + def test_full_rary_tree_path(self): + t = nx.full_rary_tree(1, 10) + assert nx.could_be_isomorphic(t, nx.path_graph(10)) + + def test_full_rary_tree_empty(self): + t = nx.full_rary_tree(0, 10) + assert nx.could_be_isomorphic(t, nx.empty_graph(10)) + t = nx.full_rary_tree(3, 0) + assert nx.could_be_isomorphic(t, nx.empty_graph(0)) + + def test_full_rary_tree_3_20(self): + t = nx.full_rary_tree(3, 20) + assert t.order() == 20 + + def test_barbell_graph(self): + # number of nodes = 2*m1 + m2 (2 m1-complete graphs + m2-path + 2 edges) + # number of edges = 2*(nx.number_of_edges(m1-complete graph) + m2 + 1 + m1 = 3 + m2 = 5 + b = nx.barbell_graph(m1, m2) + assert nx.number_of_nodes(b) == 2 * m1 + m2 + assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1 + + m1 = 4 + m2 = 10 + b = nx.barbell_graph(m1, m2) + assert nx.number_of_nodes(b) == 2 * m1 + m2 + assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1 + + m1 = 3 + m2 = 20 + b = nx.barbell_graph(m1, m2) + assert nx.number_of_nodes(b) == 2 * m1 + m2 + assert nx.number_of_edges(b) == m1 * (m1 - 1) + m2 + 1 + + # Raise NetworkXError if m1<2 + m1 = 1 + m2 = 20 + pytest.raises(nx.NetworkXError, nx.barbell_graph, m1, m2) + + # Raise NetworkXError if m2<0 + m1 = 5 + m2 = -2 + pytest.raises(nx.NetworkXError, nx.barbell_graph, m1, m2) + + # nx.barbell_graph(2,m) = nx.path_graph(m+4) + m1 = 2 + m2 = 5 + b = nx.barbell_graph(m1, m2) + assert nx.could_be_isomorphic(b, nx.path_graph(m2 + 4)) + + m1 = 2 + m2 = 10 + b = nx.barbell_graph(m1, m2) + assert nx.could_be_isomorphic(b, nx.path_graph(m2 + 4)) + + m1 = 2 + m2 = 20 + b = nx.barbell_graph(m1, m2) + assert nx.could_be_isomorphic(b, nx.path_graph(m2 + 4)) + + pytest.raises( + nx.NetworkXError, nx.barbell_graph, m1, m2, create_using=nx.DiGraph() + ) + + mb = nx.barbell_graph(m1, m2, create_using=nx.MultiGraph()) + assert edges_equal(mb.edges(), b.edges()) + + def test_binomial_tree(self): + graphs = (None, nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph) + for create_using in graphs: + for n in range(4): + b = nx.binomial_tree(n, create_using) + assert nx.number_of_nodes(b) == 2**n + assert nx.number_of_edges(b) == (2**n - 1) + + def test_complete_graph(self): + # complete_graph(m) is a connected graph with + # m nodes and m*(m+1)/2 edges + for m in [0, 1, 3, 5]: + g = nx.complete_graph(m) + assert nx.number_of_nodes(g) == m + assert nx.number_of_edges(g) == m * (m - 1) // 2 + + mg = nx.complete_graph(m, create_using=nx.MultiGraph) + assert edges_equal(mg.edges(), g.edges()) + + g = nx.complete_graph("abc") + assert nodes_equal(g.nodes(), ["a", "b", "c"]) + assert g.size() == 3 + + # creates a self-loop... should it? + g = nx.complete_graph("abcb") + assert nodes_equal(g.nodes(), ["a", "b", "c"]) + assert g.size() == 4 + + g = nx.complete_graph("abcb", create_using=nx.MultiGraph) + assert nodes_equal(g.nodes(), ["a", "b", "c"]) + assert g.size() == 6 + + def test_complete_digraph(self): + # complete_graph(m) is a connected graph with + # m nodes and m*(m+1)/2 edges + for m in [0, 1, 3, 5]: + g = nx.complete_graph(m, create_using=nx.DiGraph) + assert nx.number_of_nodes(g) == m + assert nx.number_of_edges(g) == m * (m - 1) + + g = nx.complete_graph("abc", create_using=nx.DiGraph) + assert len(g) == 3 + assert g.size() == 6 + assert g.is_directed() + + def test_circular_ladder_graph(self): + G = nx.circular_ladder_graph(5) + pytest.raises( + nx.NetworkXError, nx.circular_ladder_graph, 5, create_using=nx.DiGraph + ) + mG = nx.circular_ladder_graph(5, create_using=nx.MultiGraph) + assert edges_equal(mG.edges(), G.edges()) + + def test_circulant_graph(self): + # Ci_n(1) is the cycle graph for all n + Ci6_1 = nx.circulant_graph(6, [1]) + C6 = nx.cycle_graph(6) + assert edges_equal(Ci6_1.edges(), C6.edges()) + + # Ci_n(1, 2, ..., n div 2) is the complete graph for all n + Ci7 = nx.circulant_graph(7, [1, 2, 3]) + K7 = nx.complete_graph(7) + assert edges_equal(Ci7.edges(), K7.edges()) + + # Ci_6(1, 3) is K_3,3 i.e. the utility graph + Ci6_1_3 = nx.circulant_graph(6, [1, 3]) + K3_3 = nx.complete_bipartite_graph(3, 3) + assert nx.could_be_isomorphic(Ci6_1_3, K3_3) + + def test_cycle_graph(self): + G = nx.cycle_graph(4) + assert edges_equal(G.edges(), [(0, 1), (0, 3), (1, 2), (2, 3)]) + mG = nx.cycle_graph(4, create_using=nx.MultiGraph) + assert edges_equal(mG.edges(), [(0, 1), (0, 3), (1, 2), (2, 3)]) + G = nx.cycle_graph(4, create_using=nx.DiGraph) + assert not G.has_edge(2, 1) + assert G.has_edge(1, 2) + assert G.is_directed() + + G = nx.cycle_graph("abc") + assert len(G) == 3 + assert G.size() == 3 + G = nx.cycle_graph("abcb") + assert len(G) == 3 + assert G.size() == 2 + g = nx.cycle_graph("abc", nx.DiGraph) + assert len(g) == 3 + assert g.size() == 3 + assert g.is_directed() + g = nx.cycle_graph("abcb", nx.DiGraph) + assert len(g) == 3 + assert g.size() == 4 + + def test_dorogovtsev_goltsev_mendes_graph(self): + G = nx.dorogovtsev_goltsev_mendes_graph(0) + assert edges_equal(G.edges(), [(0, 1)]) + assert nodes_equal(list(G), [0, 1]) + G = nx.dorogovtsev_goltsev_mendes_graph(1) + assert edges_equal(G.edges(), [(0, 1), (0, 2), (1, 2)]) + assert nx.average_clustering(G) == 1.0 + assert nx.average_shortest_path_length(G) == 1.0 + assert sorted(nx.triangles(G).values()) == [1, 1, 1] + assert nx.is_planar(G) + G = nx.dorogovtsev_goltsev_mendes_graph(2) + assert nx.number_of_nodes(G) == 6 + assert nx.number_of_edges(G) == 9 + assert nx.average_clustering(G) == 0.75 + assert nx.average_shortest_path_length(G) == 1.4 + assert nx.is_planar(G) + G = nx.dorogovtsev_goltsev_mendes_graph(10) + assert nx.number_of_nodes(G) == 29526 + assert nx.number_of_edges(G) == 59049 + assert G.degree(0) == 1024 + assert G.degree(1) == 1024 + assert G.degree(2) == 1024 + + with pytest.raises(nx.NetworkXError, match=r"n must be greater than"): + nx.dorogovtsev_goltsev_mendes_graph(-1) + with pytest.raises(nx.NetworkXError, match=r"directed graph not supported"): + nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError, match=r"multigraph not supported"): + nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.MultiGraph) + with pytest.raises(nx.NetworkXError): + nx.dorogovtsev_goltsev_mendes_graph(7, create_using=nx.MultiDiGraph) + + def test_create_using(self): + G = nx.empty_graph() + assert isinstance(G, nx.Graph) + pytest.raises(TypeError, nx.empty_graph, create_using=0.0) + pytest.raises(TypeError, nx.empty_graph, create_using="Graph") + + G = nx.empty_graph(create_using=nx.MultiGraph) + assert isinstance(G, nx.MultiGraph) + G = nx.empty_graph(create_using=nx.DiGraph) + assert isinstance(G, nx.DiGraph) + + G = nx.empty_graph(create_using=nx.DiGraph, default=nx.MultiGraph) + assert isinstance(G, nx.DiGraph) + G = nx.empty_graph(create_using=None, default=nx.MultiGraph) + assert isinstance(G, nx.MultiGraph) + G = nx.empty_graph(default=nx.MultiGraph) + assert isinstance(G, nx.MultiGraph) + + G = nx.path_graph(5) + H = nx.empty_graph(create_using=G) + assert not H.is_multigraph() + assert not H.is_directed() + assert len(H) == 0 + assert G is H + + H = nx.empty_graph(create_using=nx.MultiGraph()) + assert H.is_multigraph() + assert not H.is_directed() + assert G is not H + + # test for subclasses that also use typing.Protocol. See gh-6243 + class Mixin(typing.Protocol): + pass + + class MyGraph(Mixin, nx.DiGraph): + pass + + G = nx.empty_graph(create_using=MyGraph) + + def test_empty_graph(self): + G = nx.empty_graph() + assert nx.number_of_nodes(G) == 0 + G = nx.empty_graph(42) + assert nx.number_of_nodes(G) == 42 + assert nx.number_of_edges(G) == 0 + + G = nx.empty_graph("abc") + assert len(G) == 3 + assert G.size() == 0 + + # create empty digraph + G = nx.empty_graph(42, create_using=nx.DiGraph(name="duh")) + assert nx.number_of_nodes(G) == 42 + assert nx.number_of_edges(G) == 0 + assert isinstance(G, nx.DiGraph) + + # create empty multigraph + G = nx.empty_graph(42, create_using=nx.MultiGraph(name="duh")) + assert nx.number_of_nodes(G) == 42 + assert nx.number_of_edges(G) == 0 + assert isinstance(G, nx.MultiGraph) + + # create empty graph from another + pete = nx.petersen_graph() + G = nx.empty_graph(42, create_using=pete) + assert nx.number_of_nodes(G) == 42 + assert nx.number_of_edges(G) == 0 + assert isinstance(G, nx.Graph) + + def test_ladder_graph(self): + for i, G in [ + (0, nx.empty_graph(0)), + (1, nx.path_graph(2)), + (2, nx.hypercube_graph(2)), + (10, nx.grid_graph([2, 10])), + ]: + assert nx.could_be_isomorphic(nx.ladder_graph(i), G) + + pytest.raises(nx.NetworkXError, nx.ladder_graph, 2, create_using=nx.DiGraph) + + g = nx.ladder_graph(2) + mg = nx.ladder_graph(2, create_using=nx.MultiGraph) + assert edges_equal(mg.edges(), g.edges()) + + @pytest.mark.parametrize(("m", "n"), [(3, 5), (4, 10), (3, 20)]) + def test_lollipop_graph_right_sizes(self, m, n): + G = nx.lollipop_graph(m, n) + assert nx.number_of_nodes(G) == m + n + assert nx.number_of_edges(G) == m * (m - 1) / 2 + n + + @pytest.mark.parametrize(("m", "n"), [("ab", ""), ("abc", "defg")]) + def test_lollipop_graph_size_node_sequence(self, m, n): + G = nx.lollipop_graph(m, n) + assert nx.number_of_nodes(G) == len(m) + len(n) + assert nx.number_of_edges(G) == len(m) * (len(m) - 1) / 2 + len(n) + + def test_lollipop_graph_exceptions(self): + # Raise NetworkXError if m<2 + pytest.raises(nx.NetworkXError, nx.lollipop_graph, -1, 2) + pytest.raises(nx.NetworkXError, nx.lollipop_graph, 1, 20) + pytest.raises(nx.NetworkXError, nx.lollipop_graph, "", 20) + pytest.raises(nx.NetworkXError, nx.lollipop_graph, "a", 20) + + # Raise NetworkXError if n<0 + pytest.raises(nx.NetworkXError, nx.lollipop_graph, 5, -2) + + # raise NetworkXError if create_using is directed + with pytest.raises(nx.NetworkXError): + nx.lollipop_graph(2, 20, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError): + nx.lollipop_graph(2, 20, create_using=nx.MultiDiGraph) + + @pytest.mark.parametrize(("m", "n"), [(2, 0), (2, 5), (2, 10), ("ab", 20)]) + def test_lollipop_graph_same_as_path_when_m1_is_2(self, m, n): + G = nx.lollipop_graph(m, n) + assert nx.could_be_isomorphic(G, nx.path_graph(n + 2)) + + def test_lollipop_graph_for_multigraph(self): + G = nx.lollipop_graph(5, 20) + MG = nx.lollipop_graph(5, 20, create_using=nx.MultiGraph) + assert edges_equal(MG.edges(), G.edges()) + + @pytest.mark.parametrize( + ("m", "n"), + [(4, "abc"), ("abcd", 3), ([1, 2, 3, 4], "abc"), ("abcd", [1, 2, 3])], + ) + def test_lollipop_graph_mixing_input_types(self, m, n): + expected = nx.compose(nx.complete_graph(4), nx.path_graph(range(100, 103))) + expected.add_edge(0, 100) # Connect complete graph and path graph + assert nx.could_be_isomorphic(nx.lollipop_graph(m, n), expected) + + def test_lollipop_graph_non_builtin_ints(self): + np = pytest.importorskip("numpy") + G = nx.lollipop_graph(np.int32(4), np.int64(3)) + expected = nx.compose(nx.complete_graph(4), nx.path_graph(range(100, 103))) + expected.add_edge(0, 100) # Connect complete graph and path graph + assert nx.could_be_isomorphic(G, expected) + + def test_null_graph(self): + assert nx.number_of_nodes(nx.null_graph()) == 0 + + def test_path_graph(self): + p = nx.path_graph(0) + assert nx.could_be_isomorphic(p, nx.null_graph()) + + p = nx.path_graph(1) + assert nx.could_be_isomorphic(p, nx.empty_graph(1)) + + p = nx.path_graph(10) + assert nx.is_connected(p) + assert sorted(d for n, d in p.degree()) == [1, 1, 2, 2, 2, 2, 2, 2, 2, 2] + assert p.order() - 1 == p.size() + + dp = nx.path_graph(3, create_using=nx.DiGraph) + assert dp.has_edge(0, 1) + assert not dp.has_edge(1, 0) + + mp = nx.path_graph(10, create_using=nx.MultiGraph) + assert edges_equal(mp.edges(), p.edges()) + + G = nx.path_graph("abc") + assert len(G) == 3 + assert G.size() == 2 + G = nx.path_graph("abcb") + assert len(G) == 3 + assert G.size() == 2 + g = nx.path_graph("abc", nx.DiGraph) + assert len(g) == 3 + assert g.size() == 2 + assert g.is_directed() + g = nx.path_graph("abcb", nx.DiGraph) + assert len(g) == 3 + assert g.size() == 3 + + G = nx.path_graph((1, 2, 3, 2, 4)) + assert G.has_edge(2, 4) + + def test_star_graph(self): + assert nx.could_be_isomorphic(nx.star_graph(""), nx.empty_graph(0)) + assert nx.could_be_isomorphic(nx.star_graph([]), nx.empty_graph(0)) + assert nx.could_be_isomorphic(nx.star_graph(0), nx.empty_graph(1)) + assert nx.could_be_isomorphic(nx.star_graph(1), nx.path_graph(2)) + assert nx.could_be_isomorphic(nx.star_graph(2), nx.path_graph(3)) + assert nx.could_be_isomorphic( + nx.star_graph(5), nx.complete_bipartite_graph(1, 5) + ) + + s = nx.star_graph(10) + assert sorted(d for n, d in s.degree()) == [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10] + + ms = nx.star_graph(10, create_using=nx.MultiGraph) + assert edges_equal(ms.edges(), s.edges()) + + G = nx.star_graph("abc") + assert len(G) == 3 + assert G.size() == 2 + + G = nx.star_graph("abcb") + assert len(G) == 3 + assert G.size() == 2 + G = nx.star_graph("abcb", create_using=nx.MultiGraph) + assert len(G) == 3 + assert G.size() == 3 + + G = nx.star_graph("abcdefg") + assert len(G) == 7 + assert G.size() == 6 + + @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiDiGraph)) + def test_star_graph_directed(self, graph_type): + dg = nx.star_graph(3, create_using=graph_type) + assert sorted([(u, v) for u, v, *d in dg.edges]) == [(0, 1), (0, 2), (0, 3)] + + def test_non_int_integers_for_star_graph(self): + np = pytest.importorskip("numpy") + G = nx.star_graph(np.int32(3)) + assert len(G) == 4 + assert G.size() == 3 + + @pytest.mark.parametrize(("m", "n"), [(3, 0), (3, 5), (4, 10), (3, 20)]) + def test_tadpole_graph_right_sizes(self, m, n): + G = nx.tadpole_graph(m, n) + assert nx.number_of_nodes(G) == m + n + assert nx.number_of_edges(G) == m + n - (m == 2) + + @pytest.mark.parametrize(("m", "n"), [("ab", ""), ("ab", "c"), ("abc", "defg")]) + def test_tadpole_graph_size_node_sequences(self, m, n): + G = nx.tadpole_graph(m, n) + assert nx.number_of_nodes(G) == len(m) + len(n) + assert nx.number_of_edges(G) == len(m) + len(n) - (len(m) == 2) + + def test_tadpole_graph_exceptions(self): + # Raise NetworkXError if m<2 + pytest.raises(nx.NetworkXError, nx.tadpole_graph, -1, 3) + pytest.raises(nx.NetworkXError, nx.tadpole_graph, 0, 3) + pytest.raises(nx.NetworkXError, nx.tadpole_graph, 1, 3) + + # Raise NetworkXError if n<0 + pytest.raises(nx.NetworkXError, nx.tadpole_graph, 5, -2) + + # Raise NetworkXError for digraphs + with pytest.raises(nx.NetworkXError): + nx.tadpole_graph(2, 20, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError): + nx.tadpole_graph(2, 20, create_using=nx.MultiDiGraph) + + @pytest.mark.parametrize(("m", "n"), [(2, 0), (2, 5), (2, 10), ("ab", 20)]) + def test_tadpole_graph_same_as_path_when_m_is_2(self, m, n): + G = nx.tadpole_graph(m, n) + assert nx.could_be_isomorphic(G, nx.path_graph(n + 2)) + + @pytest.mark.parametrize("m", [4, 7]) + def test_tadpole_graph_same_as_cycle_when_m2_is_0(self, m): + G = nx.tadpole_graph(m, 0) + assert nx.could_be_isomorphic(G, nx.cycle_graph(m)) + + def test_tadpole_graph_for_multigraph(self): + G = nx.tadpole_graph(5, 20) + MG = nx.tadpole_graph(5, 20, create_using=nx.MultiGraph) + assert edges_equal(MG.edges(), G.edges()) + + @pytest.mark.parametrize( + ("m", "n"), + [(4, "abc"), ("abcd", 3), ([1, 2, 3, 4], "abc"), ("abcd", [1, 2, 3])], + ) + def test_tadpole_graph_mixing_input_types(self, m, n): + expected = nx.compose(nx.cycle_graph(4), nx.path_graph(range(100, 103))) + expected.add_edge(0, 100) # Connect cycle and path + assert nx.could_be_isomorphic(nx.tadpole_graph(m, n), expected) + + def test_tadpole_graph_non_builtin_integers(self): + np = pytest.importorskip("numpy") + G = nx.tadpole_graph(np.int32(4), np.int64(3)) + expected = nx.compose(nx.cycle_graph(4), nx.path_graph(range(100, 103))) + expected.add_edge(0, 100) # Connect cycle and path + assert nx.could_be_isomorphic(G, expected) + + def test_trivial_graph(self): + assert nx.number_of_nodes(nx.trivial_graph()) == 1 + + def test_turan_graph(self): + assert nx.number_of_edges(nx.turan_graph(13, 4)) == 63 + assert nx.could_be_isomorphic( + nx.turan_graph(13, 4), nx.complete_multipartite_graph(3, 4, 3, 3) + ) + + def test_wheel_graph(self): + for n, G in [ + ("", nx.null_graph()), + (0, nx.null_graph()), + (1, nx.empty_graph(1)), + (2, nx.path_graph(2)), + (3, nx.complete_graph(3)), + (4, nx.complete_graph(4)), + ]: + g = nx.wheel_graph(n) + assert nx.could_be_isomorphic(g, G) + + g = nx.wheel_graph(10) + assert sorted(d for n, d in g.degree()) == [3, 3, 3, 3, 3, 3, 3, 3, 3, 9] + + pytest.raises(nx.NetworkXError, nx.wheel_graph, 10, create_using=nx.DiGraph) + + mg = nx.wheel_graph(10, create_using=nx.MultiGraph()) + assert edges_equal(mg.edges(), g.edges()) + + G = nx.wheel_graph("abc") + assert len(G) == 3 + assert G.size() == 3 + + G = nx.wheel_graph("abcb") + assert len(G) == 3 + assert G.size() == 4 + G = nx.wheel_graph("abcb", nx.MultiGraph) + assert len(G) == 3 + assert G.size() == 6 + + def test_non_int_integers_for_wheel_graph(self): + np = pytest.importorskip("numpy") + G = nx.wheel_graph(np.int32(3)) + assert len(G) == 3 + assert G.size() == 3 + + def test_complete_0_partite_graph(self): + """Tests that the complete 0-partite graph is the null graph.""" + G = nx.complete_multipartite_graph() + H = nx.null_graph() + assert nodes_equal(G, H) + assert edges_equal(G.edges(), H.edges()) + + def test_complete_1_partite_graph(self): + """Tests that the complete 1-partite graph is the empty graph.""" + G = nx.complete_multipartite_graph(3) + H = nx.empty_graph(3) + assert nodes_equal(G, H) + assert edges_equal(G.edges(), H.edges()) + + def test_complete_2_partite_graph(self): + """Tests that the complete 2-partite graph is the complete bipartite + graph. + + """ + G = nx.complete_multipartite_graph(2, 3) + H = nx.complete_bipartite_graph(2, 3) + assert nodes_equal(G, H) + assert edges_equal(G.edges(), H.edges()) + + def test_complete_multipartite_graph(self): + """Tests for generating the complete multipartite graph.""" + G = nx.complete_multipartite_graph(2, 3, 4) + blocks = [(0, 1), (2, 3, 4), (5, 6, 7, 8)] + # Within each block, no two vertices should be adjacent. + for block in blocks: + for u, v in itertools.combinations_with_replacement(block, 2): + assert v not in G[u] + assert G.nodes[u] == G.nodes[v] + # Across blocks, all vertices should be adjacent. + for block1, block2 in itertools.combinations(blocks, 2): + for u, v in itertools.product(block1, block2): + assert v in G[u] + assert G.nodes[u] != G.nodes[v] + with pytest.raises(nx.NetworkXError, match="Negative number of nodes"): + nx.complete_multipartite_graph(2, -3, 4) + + def test_kneser_graph(self): + # the petersen graph is a special case of the kneser graph when n=5 and k=2 + assert nx.could_be_isomorphic(nx.kneser_graph(5, 2), nx.petersen_graph()) + + # when k is 1, the kneser graph returns a complete graph with n vertices + for i in range(1, 7): + assert nx.could_be_isomorphic(nx.kneser_graph(i, 1), nx.complete_graph(i)) + + # the kneser graph of n and n-1 is the empty graph with n vertices + for j in range(3, 7): + assert nx.could_be_isomorphic(nx.kneser_graph(j, j - 1), nx.empty_graph(j)) + + # in general the number of edges of the kneser graph is equal to + # (n choose k) times (n-k choose k) divided by 2 + assert nx.number_of_edges(nx.kneser_graph(8, 3)) == 280 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py new file mode 100644 index 0000000000000000000000000000000000000000..65ac3250fd34c5972534504184839a929289e8a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_cographs.py @@ -0,0 +1,20 @@ +"""Unit tests for the :mod:`networkx.generators.cographs` module.""" + +import pytest + +import networkx as nx + + +@pytest.mark.parametrize("n", [3, 4, 5]) +@pytest.mark.parametrize("seed", [42, 43]) +def test_random_cograph(n, seed): + """Test the generation of random cographs. + + Parametrized on `seed` to ensure we hit all code branches. + """ + G = nx.random_cograph(n, seed=seed) + + assert len(G) == 2**n + + # Every connected subgraph of G has diameter <= 2. + assert all(nx.diameter(G.subgraph(c)) <= 2 for c in nx.connected_components(G)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_community.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_community.py new file mode 100644 index 0000000000000000000000000000000000000000..2fa107f6dde9f280123796f81b919c99f92ee20c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_community.py @@ -0,0 +1,362 @@ +import pytest + +import networkx as nx + + +def test_random_partition_graph(): + G = nx.random_partition_graph([3, 3, 3], 1, 0, seed=42) + C = G.graph["partition"] + assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}] + assert len(G) == 9 + assert len(list(G.edges())) == 9 + + G = nx.random_partition_graph([3, 3, 3], 0, 1) + C = G.graph["partition"] + assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}] + assert len(G) == 9 + assert len(list(G.edges())) == 27 + + G = nx.random_partition_graph([3, 3, 3], 1, 0, directed=True) + C = G.graph["partition"] + assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}] + assert len(G) == 9 + assert len(list(G.edges())) == 18 + + G = nx.random_partition_graph([3, 3, 3], 0, 1, directed=True) + C = G.graph["partition"] + assert C == [{0, 1, 2}, {3, 4, 5}, {6, 7, 8}] + assert len(G) == 9 + assert len(list(G.edges())) == 54 + + G = nx.random_partition_graph([1, 2, 3, 4, 5], 0.5, 0.1) + C = G.graph["partition"] + assert C == [{0}, {1, 2}, {3, 4, 5}, {6, 7, 8, 9}, {10, 11, 12, 13, 14}] + assert len(G) == 15 + + rpg = nx.random_partition_graph + pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 1.1, 0.1) + pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], -0.1, 0.1) + pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 0.1, 1.1) + pytest.raises(nx.NetworkXError, rpg, [1, 2, 3], 0.1, -0.1) + + +def test_planted_partition_graph(): + G = nx.planted_partition_graph(4, 3, 1, 0, seed=42) + C = G.graph["partition"] + assert len(C) == 4 + assert len(G) == 12 + assert len(list(G.edges())) == 12 + + G = nx.planted_partition_graph(4, 3, 0, 1) + C = G.graph["partition"] + assert len(C) == 4 + assert len(G) == 12 + assert len(list(G.edges())) == 54 + + G = nx.planted_partition_graph(10, 4, 0.5, 0.1, seed=42) + C = G.graph["partition"] + assert len(C) == 10 + assert len(G) == 40 + + G = nx.planted_partition_graph(4, 3, 1, 0, directed=True) + C = G.graph["partition"] + assert len(C) == 4 + assert len(G) == 12 + assert len(list(G.edges())) == 24 + + G = nx.planted_partition_graph(4, 3, 0, 1, directed=True) + C = G.graph["partition"] + assert len(C) == 4 + assert len(G) == 12 + assert len(list(G.edges())) == 108 + + G = nx.planted_partition_graph(10, 4, 0.5, 0.1, seed=42, directed=True) + C = G.graph["partition"] + assert len(C) == 10 + assert len(G) == 40 + + ppg = nx.planted_partition_graph + pytest.raises(nx.NetworkXError, ppg, 3, 3, 1.1, 0.1) + pytest.raises(nx.NetworkXError, ppg, 3, 3, -0.1, 0.1) + pytest.raises(nx.NetworkXError, ppg, 3, 3, 0.1, 1.1) + pytest.raises(nx.NetworkXError, ppg, 3, 3, 0.1, -0.1) + + +def test_relaxed_caveman_graph(): + G = nx.relaxed_caveman_graph(4, 3, 0) + assert len(G) == 12 + G = nx.relaxed_caveman_graph(4, 3, 1) + assert len(G) == 12 + G = nx.relaxed_caveman_graph(4, 3, 0.5) + assert len(G) == 12 + G = nx.relaxed_caveman_graph(4, 3, 0.5, seed=42) + assert len(G) == 12 + + +def test_connected_caveman_graph(): + G = nx.connected_caveman_graph(4, 3) + assert len(G) == 12 + + G = nx.connected_caveman_graph(1, 5) + K5 = nx.complete_graph(5) + K5.remove_edge(3, 4) + assert nx.is_isomorphic(G, K5) + + # need at least 2 nodes in each clique + pytest.raises(nx.NetworkXError, nx.connected_caveman_graph, 4, 1) + + +def test_caveman_graph(): + G = nx.caveman_graph(4, 3) + assert len(G) == 12 + + G = nx.caveman_graph(5, 1) + E5 = nx.empty_graph(5) + assert nx.is_isomorphic(G, E5) + + G = nx.caveman_graph(1, 5) + K5 = nx.complete_graph(5) + assert nx.is_isomorphic(G, K5) + + +def test_gaussian_random_partition_graph(): + G = nx.gaussian_random_partition_graph(100, 10, 10, 0.3, 0.01) + assert len(G) == 100 + G = nx.gaussian_random_partition_graph(100, 10, 10, 0.3, 0.01, directed=True) + assert len(G) == 100 + G = nx.gaussian_random_partition_graph( + 100, 10, 10, 0.3, 0.01, directed=False, seed=42 + ) + assert len(G) == 100 + assert not isinstance(G, nx.DiGraph) + G = nx.gaussian_random_partition_graph( + 100, 10, 10, 0.3, 0.01, directed=True, seed=42 + ) + assert len(G) == 100 + assert isinstance(G, nx.DiGraph) + pytest.raises( + nx.NetworkXError, nx.gaussian_random_partition_graph, 100, 101, 10, 1, 0 + ) + # Test when clusters are likely less than 1 + G = nx.gaussian_random_partition_graph(10, 0.5, 0.5, 0.5, 0.5, seed=1) + assert len(G) == 10 + + +def test_ring_of_cliques(): + for i in range(2, 20, 3): + for j in range(2, 20, 3): + G = nx.ring_of_cliques(i, j) + assert G.number_of_nodes() == i * j + if i != 2 or j != 1: + expected_num_edges = i * (((j * (j - 1)) // 2) + 1) + else: + # the edge that already exists cannot be duplicated + expected_num_edges = i * (((j * (j - 1)) // 2) + 1) - 1 + assert G.number_of_edges() == expected_num_edges + with pytest.raises( + nx.NetworkXError, match="A ring of cliques must have at least two cliques" + ): + nx.ring_of_cliques(1, 5) + with pytest.raises( + nx.NetworkXError, match="The cliques must have at least two nodes" + ): + nx.ring_of_cliques(3, 0) + + +def test_windmill_graph(): + for n in range(2, 20, 3): + for k in range(2, 20, 3): + G = nx.windmill_graph(n, k) + assert G.number_of_nodes() == (k - 1) * n + 1 + assert G.number_of_edges() == n * k * (k - 1) / 2 + assert G.degree(0) == G.number_of_nodes() - 1 + for i in range(1, G.number_of_nodes()): + assert G.degree(i) == k - 1 + with pytest.raises( + nx.NetworkXError, match="A windmill graph must have at least two cliques" + ): + nx.windmill_graph(1, 3) + with pytest.raises( + nx.NetworkXError, match="The cliques must have at least two nodes" + ): + nx.windmill_graph(3, 0) + + +def test_stochastic_block_model(): + sizes = [75, 75, 300] + probs = [[0.25, 0.05, 0.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]] + G = nx.stochastic_block_model(sizes, probs, seed=0) + C = G.graph["partition"] + assert len(C) == 3 + assert len(G) == 450 + assert G.size() == 22160 + + GG = nx.stochastic_block_model(sizes, probs, range(450), seed=0) + assert G.nodes == GG.nodes + + # Test Exceptions + sbm = nx.stochastic_block_model + badnodelist = list(range(400)) # not enough nodes to match sizes + badprobs1 = [[0.25, 0.05, 1.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]] + badprobs2 = [[0.25, 0.05, 0.02], [0.05, -0.35, 0.07], [0.02, 0.07, 0.40]] + probs_rect1 = [[0.25, 0.05, 0.02], [0.05, -0.35, 0.07]] + probs_rect2 = [[0.25, 0.05], [0.05, -0.35], [0.02, 0.07]] + asymprobs = [[0.25, 0.05, 0.01], [0.05, -0.35, 0.07], [0.02, 0.07, 0.40]] + pytest.raises(nx.NetworkXException, sbm, sizes, badprobs1) + pytest.raises(nx.NetworkXException, sbm, sizes, badprobs2) + pytest.raises(nx.NetworkXException, sbm, sizes, probs_rect1, directed=True) + pytest.raises(nx.NetworkXException, sbm, sizes, probs_rect2, directed=True) + pytest.raises(nx.NetworkXException, sbm, sizes, asymprobs, directed=False) + pytest.raises(nx.NetworkXException, sbm, sizes, probs, badnodelist) + nodelist = [0] + list(range(449)) # repeated node name in nodelist + pytest.raises(nx.NetworkXException, sbm, sizes, probs, nodelist) + + # Extra keyword arguments test + GG = nx.stochastic_block_model(sizes, probs, seed=0, selfloops=True) + assert G.nodes == GG.nodes + GG = nx.stochastic_block_model(sizes, probs, selfloops=True, directed=True) + assert G.nodes == GG.nodes + GG = nx.stochastic_block_model(sizes, probs, seed=0, sparse=False) + assert G.nodes == GG.nodes + + +def test_generator(): + n = 250 + tau1 = 3 + tau2 = 1.5 + mu = 0.1 + G = nx.LFR_benchmark_graph( + n, tau1, tau2, mu, average_degree=5, min_community=20, seed=10 + ) + assert len(G) == 250 + C = {frozenset(G.nodes[v]["community"]) for v in G} + assert nx.community.is_partition(G.nodes(), C) + + +def test_invalid_tau1(): + with pytest.raises(nx.NetworkXError, match="tau2 must be greater than one"): + n = 100 + tau1 = 2 + tau2 = 1 + mu = 0.1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2) + + +def test_invalid_tau2(): + with pytest.raises(nx.NetworkXError, match="tau1 must be greater than one"): + n = 100 + tau1 = 1 + tau2 = 2 + mu = 0.1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2) + + +def test_mu_too_large(): + with pytest.raises(nx.NetworkXError, match="mu must be in the interval \\[0, 1\\]"): + n = 100 + tau1 = 2 + tau2 = 2 + mu = 1.1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2) + + +def test_mu_too_small(): + with pytest.raises(nx.NetworkXError, match="mu must be in the interval \\[0, 1\\]"): + n = 100 + tau1 = 2 + tau2 = 2 + mu = -1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2) + + +def test_both_degrees_none(): + with pytest.raises( + nx.NetworkXError, + match="Must assign exactly one of min_degree and average_degree", + ): + n = 100 + tau1 = 2 + tau2 = 2 + mu = 1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu) + + +def test_neither_degrees_none(): + with pytest.raises( + nx.NetworkXError, + match="Must assign exactly one of min_degree and average_degree", + ): + n = 100 + tau1 = 2 + tau2 = 2 + mu = 1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, average_degree=5) + + +def test_max_iters_exceeded(): + with pytest.raises( + nx.ExceededMaxIterations, + match="Could not assign communities; try increasing min_community", + ): + n = 10 + tau1 = 2 + tau2 = 2 + mu = 0.1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, max_iters=10, seed=1) + + +def test_max_deg_out_of_range(): + with pytest.raises( + nx.NetworkXError, match="max_degree must be in the interval \\(0, n\\]" + ): + n = 10 + tau1 = 2 + tau2 = 2 + mu = 0.1 + nx.LFR_benchmark_graph( + n, tau1, tau2, mu, max_degree=n + 1, max_iters=10, seed=1 + ) + + +def test_max_community(): + n = 250 + tau1 = 3 + tau2 = 1.5 + mu = 0.1 + G = nx.LFR_benchmark_graph( + n, + tau1, + tau2, + mu, + average_degree=5, + max_degree=100, + min_community=50, + max_community=200, + seed=10, + ) + assert len(G) == 250 + C = {frozenset(G.nodes[v]["community"]) for v in G} + assert nx.community.is_partition(G.nodes(), C) + + +def test_powerlaw_iterations_exceeded(): + with pytest.raises( + nx.ExceededMaxIterations, match="Could not create power law sequence" + ): + n = 100 + tau1 = 2 + tau2 = 2 + mu = 1 + nx.LFR_benchmark_graph(n, tau1, tau2, mu, min_degree=2, max_iters=0) + + +def test_no_scipy_zeta(): + zeta2 = 1.6449340668482264 + assert abs(zeta2 - nx.generators.community._hurwitz_zeta(2, 1, 0.0001)) < 0.01 + + +def test_generate_min_degree_itr(): + with pytest.raises( + nx.ExceededMaxIterations, match="Could not match average_degree" + ): + nx.generators.community._generate_min_degree(2, 2, 1, 0.01, 0) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py new file mode 100644 index 0000000000000000000000000000000000000000..c7317cd564ed30430bd267e0bb355913cc69353d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_degree_seq.py @@ -0,0 +1,224 @@ +import pytest + +import networkx as nx + + +class TestConfigurationModel: + """Unit tests for the :func:`~networkx.configuration_model` + function. + + """ + + def test_empty_degree_sequence(self): + """Tests that an empty degree sequence yields the null graph.""" + G = nx.configuration_model([]) + assert len(G) == 0 + + def test_degree_zero(self): + """Tests that a degree sequence of all zeros yields the empty + graph. + + """ + G = nx.configuration_model([0, 0, 0]) + assert len(G) == 3 + assert G.number_of_edges() == 0 + + def test_degree_sequence(self): + """Tests that the degree sequence of the generated graph matches + the input degree sequence. + + """ + deg_seq = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + G = nx.configuration_model(deg_seq, seed=12345678) + assert sorted(dict(G.degree).values()) == sorted(deg_seq) + assert sorted(dict(G.degree(range(len(deg_seq)))).values()) == sorted(deg_seq) + + @pytest.mark.parametrize("seed", [10, 1000]) + def test_random_seed(self, seed): + """Tests that each call with the same random seed generates the + same graph. + + """ + deg_seq = [3] * 12 + G1 = nx.configuration_model(deg_seq, seed=seed) + G2 = nx.configuration_model(deg_seq, seed=seed) + assert nx.is_isomorphic(G1, G2) + + def test_directed_disallowed(self): + """Tests that attempting to create a configuration model graph + using a directed graph yields an exception. + + """ + with pytest.raises(nx.NetworkXNotImplemented): + nx.configuration_model([], create_using=nx.DiGraph()) + + def test_odd_degree_sum(self): + """Tests that a degree sequence whose sum is odd yields an + exception. + + """ + with pytest.raises(nx.NetworkXError): + nx.configuration_model([1, 2]) + + +def test_directed_configuration_raise_unequal(): + with pytest.raises(nx.NetworkXError): + zin = [5, 3, 3, 3, 3, 2, 2, 2, 1, 1] + zout = [5, 3, 3, 3, 3, 2, 2, 2, 1, 2] + nx.directed_configuration_model(zin, zout) + + +def test_directed_configuration_model(): + G = nx.directed_configuration_model([], [], seed=0) + assert len(G) == 0 + + +def test_simple_directed_configuration_model(): + G = nx.directed_configuration_model([1, 1], [1, 1], seed=0) + assert len(G) == 2 + + +def test_expected_degree_graph_empty(): + # empty graph has empty degree sequence + deg_seq = [] + G = nx.expected_degree_graph(deg_seq) + assert dict(G.degree()) == {} + + +@pytest.mark.parametrize("seed", [10, 42, 1000]) +@pytest.mark.parametrize("deg_seq", [[3] * 12, [2, 0], [10, 2, 2, 2, 2]]) +def test_expected_degree_graph(seed, deg_seq): + G1 = nx.expected_degree_graph(deg_seq, seed=seed) + G2 = nx.expected_degree_graph(deg_seq, seed=seed) + assert len(G1) == len(G2) == len(deg_seq) + assert nx.is_isomorphic(G1, G2) + + +def test_expected_degree_graph_selfloops(): + deg_seq = [3] * 12 + G1 = nx.expected_degree_graph(deg_seq, seed=1000, selfloops=False) + G2 = nx.expected_degree_graph(deg_seq, seed=1000, selfloops=False) + assert len(G1) == len(G2) == len(deg_seq) + assert nx.is_isomorphic(G1, G2) + assert nx.number_of_selfloops(G1) == nx.number_of_selfloops(G2) == 0 + + +def test_havel_hakimi_construction(): + G = nx.havel_hakimi_graph([]) + assert len(G) == 0 + + z = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z) + z = ["A", 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z) + + z = [5, 4, 3, 3, 3, 2, 2, 2] + G = nx.havel_hakimi_graph(z) + G = nx.configuration_model(z) + z = [6, 5, 4, 4, 2, 1, 1, 1] + pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z) + + z = [10, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2] + + G = nx.havel_hakimi_graph(z) + + pytest.raises(nx.NetworkXError, nx.havel_hakimi_graph, z, create_using=nx.DiGraph()) + + +def test_directed_havel_hakimi(): + # Test range of valid directed degree sequences + n, r = 100, 10 + p = 1.0 / r + for i in range(r): + G1 = nx.erdos_renyi_graph(n, p * (i + 1), None, True) + din1 = [d for n, d in G1.in_degree()] + dout1 = [d for n, d in G1.out_degree()] + G2 = nx.directed_havel_hakimi_graph(din1, dout1) + din2 = [d for n, d in G2.in_degree()] + dout2 = [d for n, d in G2.out_degree()] + assert sorted(din1) == sorted(din2) + assert sorted(dout1) == sorted(dout2) + + # Test non-graphical sequence + dout = [1000, 3, 3, 3, 3, 2, 2, 2, 1, 1, 1] + din = [103, 102, 102, 102, 102, 102, 102, 102, 102, 102] + pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout) + # Test valid sequences + dout = [1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + din = [2, 2, 2, 2, 2, 2, 2, 2, 0, 2] + G2 = nx.directed_havel_hakimi_graph(din, dout) + dout2 = (d for n, d in G2.out_degree()) + din2 = (d for n, d in G2.in_degree()) + assert sorted(dout) == sorted(dout2) + assert sorted(din) == sorted(din2) + # Test unequal sums + din = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2] + pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout) + # Test for negative values + din = [2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -2] + pytest.raises(nx.exception.NetworkXError, nx.directed_havel_hakimi_graph, din, dout) + + +@pytest.mark.parametrize( + "deg_seq", + [ + [0], + [1, 1], + [2, 2, 2, 1, 1], + [3, 1, 1, 1], + [4, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 3, 4], + ], +) +def test_degree_sequence_tree(deg_seq): + G = nx.degree_sequence_tree(deg_seq) + assert sorted(dict(G.degree).values()) == sorted(deg_seq) + assert nx.is_tree(G) + + +@pytest.mark.parametrize("graph_type", [nx.DiGraph, nx.MultiDiGraph]) +def test_degree_sequence_tree_directed(graph_type): + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"): + nx.degree_sequence_tree([1, 1], create_using=graph_type()) + + +@pytest.mark.parametrize( + "deg_seq", + [ + [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4], + [], + [2, 0], + [-1, 3], + [1, 16, 1, 4, 0, 0, 1, 1, 0, 1, 2, 0, 1, 0, 1, 5, 1, 2, 1, 0], + ], +) +def test_degree_sequence_tree_invalid_degree_sequence(deg_seq): + """Test invalid degree sequences raise an error.""" + with pytest.raises(nx.NetworkXError, match="tree must have"): + nx.degree_sequence_tree(deg_seq) + + +def test_random_degree_sequence_graph(): + d = [1, 2, 2, 3] + G = nx.random_degree_sequence_graph(d, seed=42) + assert d == sorted(d for n, d in G.degree()) + + +def test_random_degree_sequence_graph_raise(): + z = [1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 4] + pytest.raises(nx.NetworkXUnfeasible, nx.random_degree_sequence_graph, z) + + +def test_random_degree_sequence_large(): + G1 = nx.fast_gnp_random_graph(100, 0.1, seed=42) + d1 = [d for n, d in G1.degree()] + G2 = nx.random_degree_sequence_graph(d1, seed=42) + d2 = [d for n, d in G2.degree()] + assert sorted(d1) == sorted(d2) + + +def test_random_degree_sequence_iterator(): + G1 = nx.fast_gnp_random_graph(100, 0.1, seed=42) + d1 = (d for n, d in G1.degree()) + G2 = nx.random_degree_sequence_graph(d1, seed=42) + assert len(G2) > 0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py new file mode 100644 index 0000000000000000000000000000000000000000..93d48acfc296901dfeaf59c6b8f72ff4e293d9fd --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_directed.py @@ -0,0 +1,189 @@ +"""Generators - Directed Graphs +---------------------------- +""" + +import pytest + +import networkx as nx +from networkx.classes import Graph, MultiDiGraph +from networkx.generators.directed import ( + _random_k_out_graph_numpy, + _random_k_out_graph_python, + gn_graph, + gnc_graph, + gnr_graph, + random_k_out_graph, + random_uniform_k_out_graph, + scale_free_graph, +) + +try: + import numpy as np + + has_numpy = True +except ImportError: + has_numpy = False + + +class TestGeneratorsDirected: + def test_smoke_test_random_graphs(self): + gn_graph(100) + gnr_graph(100, 0.5) + gnc_graph(100) + scale_free_graph(100) + + gn_graph(100, seed=42) + gnr_graph(100, 0.5, seed=42) + gnc_graph(100, seed=42) + scale_free_graph(100, seed=42) + + def test_create_using_keyword_arguments(self): + pytest.raises(nx.NetworkXError, gn_graph, 100, create_using=Graph()) + pytest.raises(nx.NetworkXError, gnr_graph, 100, 0.5, create_using=Graph()) + pytest.raises(nx.NetworkXError, gnc_graph, 100, create_using=Graph()) + G = gn_graph(100, seed=1) + MG = gn_graph(100, create_using=MultiDiGraph(), seed=1) + assert sorted(G.edges()) == sorted(MG.edges()) + G = gnr_graph(100, 0.5, seed=1) + MG = gnr_graph(100, 0.5, create_using=MultiDiGraph(), seed=1) + assert sorted(G.edges()) == sorted(MG.edges()) + G = gnc_graph(100, seed=1) + MG = gnc_graph(100, create_using=MultiDiGraph(), seed=1) + assert sorted(G.edges()) == sorted(MG.edges()) + + G = scale_free_graph( + 100, + alpha=0.3, + beta=0.4, + gamma=0.3, + delta_in=0.3, + delta_out=0.1, + initial_graph=nx.cycle_graph(4, create_using=MultiDiGraph), + seed=1, + ) + pytest.raises(ValueError, scale_free_graph, 100, 0.5, 0.4, 0.3) + pytest.raises(ValueError, scale_free_graph, 100, alpha=-0.3) + pytest.raises(ValueError, scale_free_graph, 100, beta=-0.3) + pytest.raises(ValueError, scale_free_graph, 100, gamma=-0.3) + + def test_parameters(self): + G = nx.DiGraph() + G.add_node(0) + + def kernel(x): + return x + + assert nx.is_isomorphic(gn_graph(1), G) + assert nx.is_isomorphic(gn_graph(1, kernel=kernel), G) + assert nx.is_isomorphic(gnc_graph(1), G) + assert nx.is_isomorphic(gnr_graph(1, 0.5), G) + + +def test_scale_free_graph_negative_delta(): + with pytest.raises(ValueError, match="delta_in must be >= 0."): + scale_free_graph(10, delta_in=-1) + with pytest.raises(ValueError, match="delta_out must be >= 0."): + scale_free_graph(10, delta_out=-1) + + +def test_non_numeric_ordering(): + G = MultiDiGraph([("a", "b"), ("b", "c"), ("c", "a")]) + s = scale_free_graph(3, initial_graph=G) + assert len(s) == 3 + assert len(s.edges) == 3 + + +@pytest.mark.parametrize("ig", (nx.Graph(), nx.DiGraph([(0, 1)]))) +def test_scale_free_graph_initial_graph_kwarg(ig): + with pytest.raises(nx.NetworkXError): + scale_free_graph(100, initial_graph=ig) + + +class TestRandomKOutGraph: + """Unit tests for the + :func:`~networkx.generators.directed.random_k_out_graph` function. + + """ + + @pytest.fixture( + params=[ + pytest.param( + _random_k_out_graph_numpy, + marks=pytest.mark.skipif(not has_numpy, reason="numpy not installed"), + ), + _random_k_out_graph_python, + ] + ) + def f(self, request): + yield request.param + + @pytest.fixture(params=[(10, 3, 1), (20, 2, 4), (5, 1, 10)]) + def nkalpha(self, request): + yield request.param + + def test_regularity(self, f, nkalpha): + """Test that the generated graph is `k`-out-regular.""" + n, k, alpha = nkalpha + G = f(n, k, alpha, seed=42) + assert all(d == k for _, d in G.out_degree) + + def test_no_self_loops(self, f, nkalpha): + """Test for forbidding self-loops.""" + n, k, alpha = nkalpha + G = f(n, k, alpha, self_loops=False, seed=42) + assert nx.number_of_selfloops(G) == 0 + + def test_random_k_out_graph(self, nkalpha): + """Test that the interface function `random_k_out_graph` works correctly.""" + n, k, alpha = nkalpha + G = random_k_out_graph(n, k, alpha, seed=42) + assert len(G) == n + assert all(d == k for _, d in G.out_degree) + + def test_negative_alpha(self): + with pytest.raises(ValueError, match="alpha must be positive"): + random_k_out_graph(10, 3, -1) + + +class TestUniformRandomKOutGraph: + """Unit tests for the + :func:`~networkx.generators.directed.random_uniform_k_out_graph` + function. + + """ + + def test_regularity(self): + """Tests that the generated graph is `k`-out-regular.""" + n = 10 + k = 3 + G = random_uniform_k_out_graph(n, k) + assert all(d == k for v, d in G.out_degree()) + G = random_uniform_k_out_graph(n, k, seed=42) + assert all(d == k for v, d in G.out_degree()) + + def test_no_self_loops(self): + """Tests for forbidding self-loops.""" + n = 10 + k = 3 + G = random_uniform_k_out_graph(n, k, self_loops=False) + assert nx.number_of_selfloops(G) == 0 + assert all(d == k for v, d in G.out_degree()) + + def test_with_replacement(self): + n = 10 + k = 3 + G = random_uniform_k_out_graph(n, k, with_replacement=True) + assert G.is_multigraph() + assert all(d == k for v, d in G.out_degree()) + n = 10 + k = 9 + G = random_uniform_k_out_graph(n, k, with_replacement=False, self_loops=False) + assert nx.number_of_selfloops(G) == 0 + assert all(d == k for v, d in G.out_degree()) + + def test_without_replacement(self): + n = 10 + k = 3 + G = random_uniform_k_out_graph(n, k, with_replacement=False) + assert not G.is_multigraph() + assert all(d == k for v, d in G.out_degree()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py new file mode 100644 index 0000000000000000000000000000000000000000..9b6100b78e59067b607e310f14d80e5a00c2b691 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_duplication.py @@ -0,0 +1,103 @@ +"""Unit tests for the :mod:`networkx.generators.duplication` module.""" + +import pytest + +import networkx as nx + + +class TestDuplicationDivergenceGraph: + """Unit tests for the + :func:`networkx.generators.duplication.duplication_divergence_graph` + function. + + """ + + def test_final_size(self): + G = nx.duplication_divergence_graph(3, p=1) + assert len(G) == 3 + G = nx.duplication_divergence_graph(3, p=1, seed=42) + assert len(G) == 3 + + def test_probability_too_large(self): + with pytest.raises(nx.NetworkXError): + nx.duplication_divergence_graph(3, p=2) + + def test_probability_too_small(self): + with pytest.raises(nx.NetworkXError): + nx.duplication_divergence_graph(3, p=-1) + + def test_non_extreme_probability_value(self): + G = nx.duplication_divergence_graph(6, p=0.3, seed=42) + assert len(G) == 6 + assert list(G.degree()) == [(0, 2), (1, 3), (2, 2), (3, 3), (4, 1), (5, 1)] + + def test_minimum_desired_nodes(self): + with pytest.raises( + nx.NetworkXError, match=".*n must be greater than or equal to 2" + ): + nx.duplication_divergence_graph(1, p=1) + + def test_create_using(self): + class DummyGraph(nx.Graph): + pass + + class DummyDiGraph(nx.DiGraph): + pass + + G = nx.duplication_divergence_graph(6, 0.3, seed=42, create_using=DummyGraph) + assert isinstance(G, DummyGraph) + with pytest.raises(nx.NetworkXError, match="create_using must not be directed"): + nx.duplication_divergence_graph(6, 0.3, seed=42, create_using=DummyDiGraph) + + +class TestPartialDuplicationGraph: + """Unit tests for the + :func:`networkx.generators.duplication.partial_duplication_graph` + function. + + """ + + def test_final_size(self): + N = 10 + n = 5 + p = 0.5 + q = 0.5 + G = nx.partial_duplication_graph(N, n, p, q) + assert len(G) == N + G = nx.partial_duplication_graph(N, n, p, q, seed=42) + assert len(G) == N + + def test_initial_clique_size(self): + N = 10 + n = 10 + p = 0.5 + q = 0.5 + G = nx.partial_duplication_graph(N, n, p, q) + assert len(G) == n + + def test_invalid_initial_size(self): + with pytest.raises(nx.NetworkXError): + N = 5 + n = 10 + p = 0.5 + q = 0.5 + G = nx.partial_duplication_graph(N, n, p, q) + + def test_invalid_probabilities(self): + N = 1 + n = 1 + for p, q in [(0.5, 2), (0.5, -1), (2, 0.5), (-1, 0.5)]: + args = (N, n, p, q) + pytest.raises(nx.NetworkXError, nx.partial_duplication_graph, *args) + + def test_create_using(self): + class DummyGraph(nx.Graph): + pass + + class DummyDiGraph(nx.DiGraph): + pass + + G = nx.partial_duplication_graph(10, 5, 0.5, 0.5, create_using=DummyGraph) + assert isinstance(G, DummyGraph) + with pytest.raises(nx.NetworkXError, match="create_using must not be directed"): + nx.partial_duplication_graph(10, 5, 0.5, 0.5, create_using=DummyDiGraph) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py new file mode 100644 index 0000000000000000000000000000000000000000..f6fc779548a3fd2e049679987f941b2bc211c2d0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_ego.py @@ -0,0 +1,39 @@ +""" +ego graph +--------- +""" + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + + +class TestGeneratorEgo: + def test_ego(self): + G = nx.star_graph(3) + H = nx.ego_graph(G, 0) + assert nx.is_isomorphic(G, H) + G.add_edge(1, 11) + G.add_edge(2, 22) + G.add_edge(3, 33) + H = nx.ego_graph(G, 0) + assert nx.is_isomorphic(nx.star_graph(3), H) + G = nx.path_graph(3) + H = nx.ego_graph(G, 0) + assert edges_equal(H.edges(), [(0, 1)]) + H = nx.ego_graph(G, 0, undirected=True) + assert edges_equal(H.edges(), [(0, 1)]) + H = nx.ego_graph(G, 0, center=False) + assert edges_equal(H.edges(), []) + + def test_ego_distance(self): + G = nx.Graph() + G.add_edge(0, 1, weight=2, distance=1) + G.add_edge(1, 2, weight=2, distance=2) + G.add_edge(2, 3, weight=2, distance=1) + assert nodes_equal(nx.ego_graph(G, 0, radius=3).nodes(), [0, 1, 2, 3]) + eg = nx.ego_graph(G, 0, radius=3, distance="weight") + assert nodes_equal(eg.nodes(), [0, 1]) + eg = nx.ego_graph(G, 0, radius=3, distance="weight", undirected=True) + assert nodes_equal(eg.nodes(), [0, 1]) + eg = nx.ego_graph(G, 0, radius=3, distance="distance") + assert nodes_equal(eg.nodes(), [0, 1, 2]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py new file mode 100644 index 0000000000000000000000000000000000000000..d742b88f3778fe718c6c53f804f9b938fdb89481 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_expanders.py @@ -0,0 +1,182 @@ +"""Unit tests for the :mod:`networkx.generators.expanders` module.""" + +import pytest + +import networkx as nx + + +@pytest.mark.parametrize("n", (2, 3, 5, 6, 10)) +def test_margulis_gabber_galil_graph_properties(n): + g = nx.margulis_gabber_galil_graph(n) + assert g.number_of_nodes() == n * n + for node in g: + assert g.degree(node) == 8 + assert len(node) == 2 + for i in node: + assert int(i) == i + assert 0 <= i < n + + +@pytest.mark.parametrize("n", (2, 3, 5, 6, 10)) +def test_margulis_gabber_galil_graph_eigvals(n): + np = pytest.importorskip("numpy") + sp = pytest.importorskip("scipy") + + g = nx.margulis_gabber_galil_graph(n) + # Eigenvalues are already sorted using the scipy eigvalsh, + # but the implementation in numpy does not guarantee order. + w = sorted(sp.linalg.eigvalsh(nx.adjacency_matrix(g).toarray())) + assert w[-2] < 5 * np.sqrt(2) + + +@pytest.mark.parametrize("p", (3, 5, 7, 11)) # Primes +def test_chordal_cycle_graph(p): + """Test for the :func:`networkx.chordal_cycle_graph` function.""" + G = nx.chordal_cycle_graph(p) + assert len(G) == p + # TODO The second largest eigenvalue should be smaller than a constant, + # independent of the number of nodes in the graph: + # + # eigs = sorted(sp.linalg.eigvalsh(nx.adjacency_matrix(G).toarray())) + # assert_less(eigs[-2], ...) + # + + +@pytest.mark.parametrize("p", (3, 5, 7, 11, 13)) # Primes +def test_paley_graph(p): + """Test for the :func:`networkx.paley_graph` function.""" + G = nx.paley_graph(p) + # G has p nodes + assert len(G) == p + # G is (p-1)/2-regular + in_degrees = {G.in_degree(node) for node in G.nodes} + out_degrees = {G.out_degree(node) for node in G.nodes} + assert len(in_degrees) == 1 and in_degrees.pop() == (p - 1) // 2 + assert len(out_degrees) == 1 and out_degrees.pop() == (p - 1) // 2 + + # If p = 1 mod 4, -1 is a square mod 4 and therefore the + # edge in the Paley graph are symmetric. + if p % 4 == 1: + for u, v in G.edges: + assert (v, u) in G.edges + + +@pytest.mark.parametrize("d, n", [(2, 7), (4, 10), (4, 16)]) +def test_maybe_regular_expander_graph(d, n): + pytest.importorskip("numpy") + G = nx.maybe_regular_expander_graph(n, d, seed=1729) + + assert len(G) == n, "Should have n nodes" + assert len(G.edges) == n * d / 2, "Should have n*d/2 edges" + assert nx.is_k_regular(G, d), "Should be d-regular" + + +def test_maybe_regular_expander_graph_max_tries(): + pytest.importorskip("numpy") + d, n = 4, 10 + msg = "Too many iterations in maybe_regular_expander_graph" + with pytest.raises(nx.NetworkXError, match=msg): + nx.maybe_regular_expander_graph(n, d, max_tries=100, seed=6818) # See gh-8048 + + nx.maybe_regular_expander_graph(n, d, max_tries=130, seed=6818) + + +def test_maybe_regular_expander_deprecated(): + pytest.importorskip("numpy") + d, n = 2, 7 + with pytest.deprecated_call(): + G = nx.maybe_regular_expander(n, d, seed=1729) + + assert len(G) == n, "Should have n nodes" + assert len(G.edges) == n * d / 2, "Should have n*d/2 edges" + assert nx.is_k_regular(G, d), "Should be d-regular" + + +@pytest.mark.parametrize("n", (3, 5, 6, 10)) +def test_is_regular_expander(n): + pytest.importorskip("numpy") + pytest.importorskip("scipy") + G = nx.complete_graph(n) + + assert nx.is_regular_expander(G), "Should be a regular expander" + + +@pytest.mark.parametrize("d, n", [(2, 7), (4, 10), (4, 16), (4, 2000)]) +def test_random_regular_expander(d, n): + pytest.importorskip("numpy") + pytest.importorskip("scipy") + G = nx.random_regular_expander_graph(n, d, seed=1729) + + assert len(G) == n, "Should have n nodes" + assert len(G.edges) == n * d / 2, "Should have n*d/2 edges" + assert nx.is_k_regular(G, d), "Should be d-regular" + assert nx.is_regular_expander(G), "Should be a regular expander" + + +def test_random_regular_expander_explicit_construction(): + pytest.importorskip("numpy") + pytest.importorskip("scipy") + G = nx.random_regular_expander_graph(d=4, n=5, seed=1729) + + assert len(G) == 5 and len(G.edges) == 10, "Should be a complete graph" + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph, nx.MultiDiGraph)) +def test_margulis_gabber_galil_graph_badinput(graph_type): + with pytest.raises( + nx.NetworkXError, match="`create_using` must be an undirected multigraph" + ): + nx.margulis_gabber_galil_graph(3, create_using=graph_type) + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph, nx.MultiDiGraph)) +def test_chordal_cycle_graph_badinput(graph_type): + with pytest.raises( + nx.NetworkXError, match="`create_using` must be an undirected multigraph" + ): + nx.chordal_cycle_graph(3, create_using=graph_type) + + +def test_paley_graph_badinput(): + with pytest.raises( + nx.NetworkXError, match="`create_using` cannot be a multigraph." + ): + nx.paley_graph(3, create_using=nx.MultiGraph) + + +def test_maybe_regular_expander_graph_badinput(): + pytest.importorskip("numpy") + + with pytest.raises(nx.NetworkXError, match="n must be a positive integer"): + nx.maybe_regular_expander_graph(n=-1, d=2) + + with pytest.raises(nx.NetworkXError, match="d must be greater than or equal to 2"): + nx.maybe_regular_expander_graph(n=10, d=0) + + with pytest.raises(nx.NetworkXError, match="Need n-1>= d to have room"): + nx.maybe_regular_expander_graph(n=5, d=6) + + +def test_is_regular_expander_badinput(): + pytest.importorskip("numpy") + pytest.importorskip("scipy") + + with pytest.raises(nx.NetworkXError, match="epsilon must be non negative"): + nx.is_regular_expander(nx.Graph(), epsilon=-1) + + +def test_random_regular_expander_badinput(): + pytest.importorskip("numpy") + pytest.importorskip("scipy") + + with pytest.raises(nx.NetworkXError, match="n must be a positive integer"): + nx.random_regular_expander_graph(n=-1, d=2) + + with pytest.raises(nx.NetworkXError, match="d must be greater than or equal to 2"): + nx.random_regular_expander_graph(n=10, d=0) + + with pytest.raises(nx.NetworkXError, match="Need n-1>= d to have room"): + nx.random_regular_expander_graph(n=5, d=6) + + with pytest.raises(nx.NetworkXError, match="epsilon must be non negative"): + nx.random_regular_expander_graph(n=4, d=2, epsilon=-1) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py new file mode 100644 index 0000000000000000000000000000000000000000..f1c68bead51b75e7a39484164cc484cbd4e5def8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_geometric.py @@ -0,0 +1,488 @@ +import math +import random +from itertools import combinations + +import pytest + +import networkx as nx + + +def l1dist(x, y): + return sum(abs(a - b) for a, b in zip(x, y)) + + +class TestRandomGeometricGraph: + """Unit tests for :func:`~networkx.random_geometric_graph`""" + + def test_number_of_nodes(self): + G = nx.random_geometric_graph(50, 0.25, seed=42) + assert len(G) == 50 + G = nx.random_geometric_graph(range(50), 0.25, seed=42) + assert len(G) == 50 + + def test_distances(self): + """Tests that pairs of vertices adjacent if and only if they are + within the prescribed radius. + """ + # Use the Euclidean metric, the default according to the + # documentation. + G = nx.random_geometric_graph(50, 0.25) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + # Nonadjacent vertices must be at greater distance. + else: + assert not math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_p(self): + """Tests for providing an alternate distance metric to the generator.""" + # Use the L1 metric. + G = nx.random_geometric_graph(50, 0.25, p=1) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert l1dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + # Nonadjacent vertices must be at greater distance. + else: + assert not l1dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_node_names(self): + """Tests using values other than sequential numbers as node IDs.""" + import string + + nodes = list(string.ascii_lowercase) + G = nx.random_geometric_graph(nodes, 0.25) + assert len(G) == len(nodes) + + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + # Nonadjacent vertices must be at greater distance. + else: + assert not math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_pos_name(self): + G = nx.random_geometric_graph(50, 0.25, seed=42, pos_name="coords") + assert all(len(d["coords"]) == 2 for n, d in G.nodes.items()) + + +class TestSoftRandomGeometricGraph: + """Unit tests for :func:`~networkx.soft_random_geometric_graph`""" + + def test_number_of_nodes(self): + G = nx.soft_random_geometric_graph(50, 0.25, seed=42) + assert len(G) == 50 + G = nx.soft_random_geometric_graph(range(50), 0.25, seed=42) + assert len(G) == 50 + + def test_distances(self): + """Tests that pairs of vertices adjacent if and only if they are + within the prescribed radius. + """ + # Use the Euclidean metric, the default according to the + # documentation. + G = nx.soft_random_geometric_graph(50, 0.25) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_p(self): + """Tests for providing an alternate distance metric to the generator.""" + + # Use the L1 metric. + def dist(x, y): + return sum(abs(a - b) for a, b in zip(x, y)) + + G = nx.soft_random_geometric_graph(50, 0.25, p=1) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_node_names(self): + """Tests using values other than sequential numbers as node IDs.""" + import string + + nodes = list(string.ascii_lowercase) + G = nx.soft_random_geometric_graph(nodes, 0.25) + assert len(G) == len(nodes) + + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_p_dist_default(self): + """Tests default p_dict = 0.5 returns graph with edge count <= RGG with + same n, radius, dim and positions + """ + nodes = 50 + dim = 2 + pos = {v: [random.random() for i in range(dim)] for v in range(nodes)} + RGG = nx.random_geometric_graph(50, 0.25, pos=pos) + SRGG = nx.soft_random_geometric_graph(50, 0.25, pos=pos) + assert len(SRGG.edges()) <= len(RGG.edges()) + + def test_p_dist_zero(self): + """Tests if p_dict = 0 returns disconnected graph with 0 edges""" + + def p_dist(dist): + return 0 + + G = nx.soft_random_geometric_graph(50, 0.25, p_dist=p_dist) + assert len(G.edges) == 0 + + def test_pos_name(self): + G = nx.soft_random_geometric_graph(50, 0.25, seed=42, pos_name="coords") + assert all(len(d["coords"]) == 2 for n, d in G.nodes.items()) + + +def join(G, u, v, theta, alpha, metric): + """Returns ``True`` if and only if the nodes whose attributes are + ``du`` and ``dv`` should be joined, according to the threshold + condition for geographical threshold graphs. + + ``G`` is an undirected NetworkX graph, and ``u`` and ``v`` are nodes + in that graph. The nodes must have node attributes ``'pos'`` and + ``'weight'``. + + ``metric`` is a distance metric. + """ + du, dv = G.nodes[u], G.nodes[v] + u_pos, v_pos = du["pos"], dv["pos"] + u_weight, v_weight = du["weight"], dv["weight"] + return (u_weight + v_weight) * metric(u_pos, v_pos) ** alpha >= theta + + +class TestGeographicalThresholdGraph: + """Unit tests for :func:`~networkx.geographical_threshold_graph`""" + + def test_number_of_nodes(self): + G = nx.geographical_threshold_graph(50, 100, seed=42) + assert len(G) == 50 + G = nx.geographical_threshold_graph(range(50), 100, seed=42) + assert len(G) == 50 + + def test_distances(self): + """Tests that pairs of vertices adjacent if and only if their + distances meet the given threshold. + """ + # Use the Euclidean metric and alpha = -2 + # the default according to the documentation. + G = nx.geographical_threshold_graph(50, 10) + for u, v in combinations(G, 2): + # Adjacent vertices must exceed the threshold. + if v in G[u]: + assert join(G, u, v, 10, -2, math.dist) + # Nonadjacent vertices must not exceed the threshold. + else: + assert not join(G, u, v, 10, -2, math.dist) + + def test_metric(self): + """Tests for providing an alternate distance metric to the generator.""" + # Use the L1 metric. + G = nx.geographical_threshold_graph(50, 10, metric=l1dist) + for u, v in combinations(G, 2): + # Adjacent vertices must exceed the threshold. + if v in G[u]: + assert join(G, u, v, 10, -2, l1dist) + # Nonadjacent vertices must not exceed the threshold. + else: + assert not join(G, u, v, 10, -2, l1dist) + + def test_p_dist_zero(self): + """Tests if p_dict = 0 returns disconnected graph with 0 edges""" + + def p_dist(dist): + return 0 + + G = nx.geographical_threshold_graph(50, 1, p_dist=p_dist) + assert len(G.edges) == 0 + + def test_pos_weight_name(self): + gtg = nx.geographical_threshold_graph + G = gtg(50, 100, seed=42, pos_name="coords", weight_name="wt") + assert all(len(d["coords"]) == 2 for n, d in G.nodes.items()) + assert all(d["wt"] > 0 for n, d in G.nodes.items()) + + +class TestWaxmanGraph: + """Unit tests for the :func:`~networkx.waxman_graph` function.""" + + def test_number_of_nodes_1(self): + G = nx.waxman_graph(50, 0.5, 0.1, seed=42) + assert len(G) == 50 + G = nx.waxman_graph(range(50), 0.5, 0.1, seed=42) + assert len(G) == 50 + + def test_number_of_nodes_2(self): + G = nx.waxman_graph(50, 0.5, 0.1, L=1) + assert len(G) == 50 + G = nx.waxman_graph(range(50), 0.5, 0.1, L=1) + assert len(G) == 50 + + def test_metric(self): + """Tests for providing an alternate distance metric to the generator.""" + # Use the L1 metric. + G = nx.waxman_graph(50, 0.5, 0.1, metric=l1dist) + assert len(G) == 50 + + def test_pos_name(self): + G = nx.waxman_graph(50, 0.5, 0.1, seed=42, pos_name="coords") + assert all(len(d["coords"]) == 2 for n, d in G.nodes.items()) + + +class TestNavigableSmallWorldGraph: + def test_navigable_small_world(self): + G = nx.navigable_small_world_graph(5, p=1, q=0, seed=42) + gg = nx.grid_2d_graph(5, 5).to_directed() + assert nx.is_isomorphic(G, gg) + + G = nx.navigable_small_world_graph(5, p=1, q=0, dim=3) + gg = nx.grid_graph([5, 5, 5]).to_directed() + assert nx.is_isomorphic(G, gg) + + G = nx.navigable_small_world_graph(5, p=1, q=0, dim=1) + gg = nx.grid_graph([5]).to_directed() + assert nx.is_isomorphic(G, gg) + + def test_invalid_diameter_value(self): + with pytest.raises(nx.NetworkXException, match=".*p must be >= 1"): + nx.navigable_small_world_graph(5, p=0, q=0, dim=1) + + def test_invalid_long_range_connections_value(self): + with pytest.raises(nx.NetworkXException, match=".*q must be >= 0"): + nx.navigable_small_world_graph(5, p=1, q=-1, dim=1) + + def test_invalid_exponent_for_decaying_probability_value(self): + with pytest.raises(nx.NetworkXException, match=".*r must be >= 0"): + nx.navigable_small_world_graph(5, p=1, q=0, r=-1, dim=1) + + def test_r_between_0_and_1(self): + """Smoke test for radius in range [0, 1]""" + # q=0 means no long-range connections + G = nx.navigable_small_world_graph(3, p=1, q=0, r=0.5, dim=2, seed=42) + expected = nx.grid_2d_graph(3, 3, create_using=nx.DiGraph) + assert nx.utils.graphs_equal(G, expected) + + @pytest.mark.parametrize("seed", range(2478, 2578, 10)) + def test_r_general_scaling(self, seed): + """The probability of adding a long-range edge scales with `1 / dist**r`, + so a navigable_small_world graph created with r < 1 should generally + result in more edges than a navigable_small_world graph with r >= 1 + (for 0 < q << n). + + N.B. this is probabilistic, so this test may not hold for all seeds.""" + G1 = nx.navigable_small_world_graph(7, q=3, r=0.5, seed=seed) + G2 = nx.navigable_small_world_graph(7, q=3, r=1, seed=seed) + G3 = nx.navigable_small_world_graph(7, q=3, r=2, seed=seed) + assert G1.number_of_edges() > G2.number_of_edges() + assert G2.number_of_edges() > G3.number_of_edges() + + +class TestThresholdedRandomGeometricGraph: + """Unit tests for :func:`~networkx.thresholded_random_geometric_graph`""" + + def test_number_of_nodes(self): + G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1, seed=42) + assert len(G) == 50 + G = nx.thresholded_random_geometric_graph(range(50), 0.2, 0.1, seed=42) + assert len(G) == 50 + + def test_distances(self): + """Tests that pairs of vertices adjacent if and only if they are + within the prescribed radius. + """ + # Use the Euclidean metric, the default according to the + # documentation. + G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, seed=42) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_p(self): + """Tests for providing an alternate distance metric to the generator.""" + + # Use the L1 metric. + def dist(x, y): + return sum(abs(a - b) for a, b in zip(x, y)) + + G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, p=1, seed=42) + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_node_names(self): + """Tests using values other than sequential numbers as node IDs.""" + import string + + nodes = list(string.ascii_lowercase) + G = nx.thresholded_random_geometric_graph(nodes, 0.25, 0.1, seed=42) + assert len(G) == len(nodes) + + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert math.dist(G.nodes[u]["pos"], G.nodes[v]["pos"]) <= 0.25 + + def test_theta(self): + """Tests that pairs of vertices adjacent if and only if their sum + weights exceeds the threshold parameter theta. + """ + G = nx.thresholded_random_geometric_graph(50, 0.25, 0.1, seed=42) + + for u, v in combinations(G, 2): + # Adjacent vertices must be within the given distance. + if v in G[u]: + assert (G.nodes[u]["weight"] + G.nodes[v]["weight"]) >= 0.1 + + def test_pos_name(self): + trgg = nx.thresholded_random_geometric_graph + G = trgg(50, 0.25, 0.1, seed=42, pos_name="p", weight_name="wt") + assert all(len(d["p"]) == 2 for n, d in G.nodes.items()) + assert all(d["wt"] > 0 for n, d in G.nodes.items()) + + +def test_geometric_edges_pos_attribute(): + G = nx.Graph() + G.add_nodes_from( + [ + (0, {"position": (0, 0)}), + (1, {"position": (0, 1)}), + (2, {"position": (1, 0)}), + ] + ) + expected_edges = [(0, 1), (0, 2)] + assert expected_edges == nx.geometric_edges(G, radius=1, pos_name="position") + + +def test_geometric_edges_raises_no_pos(): + G = nx.path_graph(3) + msg = "all nodes. must have a '" + with pytest.raises(nx.NetworkXError, match=msg): + nx.geometric_edges(G, radius=1) + + +def test_number_of_nodes_S1(): + G = nx.geometric_soft_configuration_graph( + beta=1.5, n=100, gamma=2.7, mean_degree=10, seed=42 + ) + assert len(G) == 100 + + +def test_set_attributes_S1(): + G = nx.geometric_soft_configuration_graph( + beta=1.5, n=100, gamma=2.7, mean_degree=10, seed=42 + ) + kappas = nx.get_node_attributes(G, "kappa") + assert len(kappas) == 100 + thetas = nx.get_node_attributes(G, "theta") + assert len(thetas) == 100 + radii = nx.get_node_attributes(G, "radius") + assert len(radii) == 100 + + +def test_mean_kappas_mean_degree_S1(): + G = nx.geometric_soft_configuration_graph( + beta=2.5, n=50, gamma=2.7, mean_degree=10, seed=8023 + ) + + kappas = nx.get_node_attributes(G, "kappa") + mean_kappas = sum(kappas.values()) / len(kappas) + assert math.fabs(mean_kappas - 10) < 0.5 + + degrees = dict(G.degree()) + mean_degree = sum(degrees.values()) / len(degrees) + assert math.fabs(mean_degree - 10) < 1 + + +def test_dict_kappas_S1(): + kappas = {i: 10 for i in range(1000)} + G = nx.geometric_soft_configuration_graph(beta=1, kappas=kappas) + assert len(G) == 1000 + kappas = nx.get_node_attributes(G, "kappa") + assert all(kappa == 10 for kappa in kappas.values()) + + +def test_beta_clustering_S1(): + G1 = nx.geometric_soft_configuration_graph( + beta=1.5, n=100, gamma=3.5, mean_degree=10, seed=42 + ) + G2 = nx.geometric_soft_configuration_graph( + beta=3.0, n=100, gamma=3.5, mean_degree=10, seed=42 + ) + assert nx.average_clustering(G1) < nx.average_clustering(G2) + + +def test_wrong_parameters_S1(): + with pytest.raises( + nx.NetworkXError, + match="Please provide either kappas, or all 3 of: n, gamma and mean_degree.", + ): + G = nx.geometric_soft_configuration_graph( + beta=1.5, gamma=3.5, mean_degree=10, seed=42 + ) + + with pytest.raises( + nx.NetworkXError, + match="When kappas is input, n, gamma and mean_degree must not be.", + ): + kappas = {i: 10 for i in range(1000)} + G = nx.geometric_soft_configuration_graph( + beta=1.5, kappas=kappas, gamma=2.3, seed=42 + ) + + with pytest.raises( + nx.NetworkXError, + match="Please provide either kappas, or all 3 of: n, gamma and mean_degree.", + ): + G = nx.geometric_soft_configuration_graph(beta=1.5, seed=42) + + +def test_negative_beta_S1(): + with pytest.raises( + nx.NetworkXError, match="The parameter beta cannot be smaller or equal to 0." + ): + G = nx.geometric_soft_configuration_graph( + beta=-1, n=100, gamma=2.3, mean_degree=10, seed=42 + ) + + +def test_non_zero_clustering_beta_lower_one_S1(): + G = nx.geometric_soft_configuration_graph( + beta=0.5, n=100, gamma=3.5, mean_degree=10, seed=42 + ) + assert nx.average_clustering(G) > 0 + + +def test_mean_degree_influence_on_connectivity_S1(): + low_mean_degree = 2 + high_mean_degree = 20 + G_low = nx.geometric_soft_configuration_graph( + beta=1.2, n=100, gamma=2.7, mean_degree=low_mean_degree, seed=42 + ) + G_high = nx.geometric_soft_configuration_graph( + beta=1.2, n=100, gamma=2.7, mean_degree=high_mean_degree, seed=42 + ) + assert nx.number_connected_components(G_low) > nx.number_connected_components( + G_high + ) + + +def test_compare_mean_kappas_different_gammas_S1(): + G1 = nx.geometric_soft_configuration_graph( + beta=1.5, n=20, gamma=2.7, mean_degree=5, seed=42 + ) + G2 = nx.geometric_soft_configuration_graph( + beta=1.5, n=20, gamma=3.5, mean_degree=5, seed=42 + ) + kappas1 = nx.get_node_attributes(G1, "kappa") + mean_kappas1 = sum(kappas1.values()) / len(kappas1) + kappas2 = nx.get_node_attributes(G2, "kappa") + mean_kappas2 = sum(kappas2.values()) / len(kappas2) + assert math.fabs(mean_kappas1 - mean_kappas2) < 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..8a0142df2a4340bc81d7dc25f05ea5d57e8f2d16 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_harary_graph.py @@ -0,0 +1,133 @@ +"""Unit tests for the :mod:`networkx.generators.harary_graph` module.""" + +import pytest + +import networkx as nx +from networkx.algorithms.isomorphism.isomorph import is_isomorphic +from networkx.generators.harary_graph import hkn_harary_graph, hnm_harary_graph + + +class TestHararyGraph: + """ + Suppose n nodes, m >= n-1 edges, d = 2m // n, r = 2m % n + """ + + def test_hnm_harary_graph(self): + # When d is even and r = 0, the hnm_harary_graph(n,m) is + # the circulant_graph(n, list(range(1,d/2+1))) + for n, m in [(5, 5), (6, 12), (7, 14)]: + G1 = hnm_harary_graph(n, m) + d = 2 * m // n + G2 = nx.circulant_graph(n, list(range(1, d // 2 + 1))) + assert is_isomorphic(G1, G2) + + # When d is even and r > 0, the hnm_harary_graph(n,m) is + # the circulant_graph(n, list(range(1,d/2+1))) + # with r edges added arbitrarily + for n, m in [(5, 7), (6, 13), (7, 16)]: + G1 = hnm_harary_graph(n, m) + d = 2 * m // n + G2 = nx.circulant_graph(n, list(range(1, d // 2 + 1))) + assert set(G2.edges) < set(G1.edges) + assert G1.number_of_edges() == m + + # When d is odd and n is even and r = 0, the hnm_harary_graph(n,m) + # is the circulant_graph(n, list(range(1,(d+1)/2) plus [n//2]) + for n, m in [(6, 9), (8, 12), (10, 15)]: + G1 = hnm_harary_graph(n, m) + d = 2 * m // n + L = list(range(1, (d + 1) // 2)) + L.append(n // 2) + G2 = nx.circulant_graph(n, L) + assert is_isomorphic(G1, G2) + + # When d is odd and n is even and r > 0, the hnm_harary_graph(n,m) + # is the circulant_graph(n, list(range(1,(d+1)/2) plus [n//2]) + # with r edges added arbitrarily + for n, m in [(6, 10), (8, 13), (10, 17)]: + G1 = hnm_harary_graph(n, m) + d = 2 * m // n + L = list(range(1, (d + 1) // 2)) + L.append(n // 2) + G2 = nx.circulant_graph(n, L) + assert set(G2.edges) < set(G1.edges) + assert G1.number_of_edges() == m + + # When d is odd and n is odd, the hnm_harary_graph(n,m) is + # the circulant_graph(n, list(range(1,(d+1)/2)) + # with m - n*(d-1)/2 edges added arbitrarily + for n, m in [(5, 4), (7, 12), (9, 14)]: + G1 = hnm_harary_graph(n, m) + d = 2 * m // n + L = list(range(1, (d + 1) // 2)) + G2 = nx.circulant_graph(n, L) + assert set(G2.edges) < set(G1.edges) + assert G1.number_of_edges() == m + + # Raise NetworkXError if n<1 + n = 0 + m = 0 + pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m) + + # Raise NetworkXError if m < n-1 + n = 6 + m = 4 + pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m) + + # Raise NetworkXError if m > n(n-1)/2 + n = 6 + m = 16 + pytest.raises(nx.NetworkXError, hnm_harary_graph, n, m) + + """ + Suppose connectivity k, number of nodes n + """ + + def test_hkn_harary_graph(self): + # When k == 1, the hkn_harary_graph(k,n) is + # the path_graph(n) + for k, n in [(1, 6), (1, 7)]: + G1 = hkn_harary_graph(k, n) + G2 = nx.path_graph(n) + assert is_isomorphic(G1, G2) + + # When k is even, the hkn_harary_graph(k,n) is + # the circulant_graph(n, list(range(1,k/2+1))) + for k, n in [(2, 6), (2, 7), (4, 6), (4, 7)]: + G1 = hkn_harary_graph(k, n) + G2 = nx.circulant_graph(n, list(range(1, k // 2 + 1))) + assert is_isomorphic(G1, G2) + + # When k is odd and n is even, the hkn_harary_graph(k,n) is + # the circulant_graph(n, list(range(1,(k+1)/2)) plus [n/2]) + for k, n in [(3, 6), (5, 8), (7, 10)]: + G1 = hkn_harary_graph(k, n) + L = list(range(1, (k + 1) // 2)) + L.append(n // 2) + G2 = nx.circulant_graph(n, L) + assert is_isomorphic(G1, G2) + + # When k is odd and n is odd, the hkn_harary_graph(k,n) is + # the circulant_graph(n, list(range(1,(k+1)/2))) with + # n//2+1 edges added between node i and node i+n//2+1 + for k, n in [(3, 5), (5, 9), (7, 11)]: + G1 = hkn_harary_graph(k, n) + G2 = nx.circulant_graph(n, list(range(1, (k + 1) // 2))) + eSet1 = set(G1.edges) + eSet2 = set(G2.edges) + eSet3 = set() + half = n // 2 + for i in range(half + 1): + # add half+1 edges between i and i+half + eSet3.add((i, (i + half) % n)) + assert eSet1 == eSet2 | eSet3 + + # Raise NetworkXError if k<1 + k = 0 + n = 0 + pytest.raises(nx.NetworkXError, hkn_harary_graph, k, n) + + # Raise NetworkXError if ndegree_count[1]*degree_count[4] + joint_degrees_3 = { + 1: {4: 2}, + 2: {2: 2, 3: 2, 4: 2}, + 3: {2: 2, 4: 1}, + 4: {1: 2, 2: 2, 3: 1}, + } + assert not is_valid_joint_degree(joint_degrees_3) + + # test condition 5 + # joint_degrees_5[1][1] not even + joint_degrees_5 = {1: {1: 9}} + assert not is_valid_joint_degree(joint_degrees_5) + + +def test_joint_degree_graph(ntimes=10): + for _ in range(ntimes): + seed = int(time.time()) + + n, m, p = 20, 10, 1 + # generate random graph with model powerlaw_cluster and calculate + # its joint degree + g = powerlaw_cluster_graph(n, m, p, seed=seed) + joint_degrees_g = degree_mixing_dict(g, normalized=False) + + # generate simple undirected graph with given joint degree + # joint_degrees_g + G = joint_degree_graph(joint_degrees_g) + joint_degrees_G = degree_mixing_dict(G, normalized=False) + + # assert that the given joint degree is equal to the generated + # graph's joint degree + assert joint_degrees_g == joint_degrees_G + + +def test_is_valid_directed_joint_degree(): + in_degrees = [0, 1, 1, 2] + out_degrees = [1, 1, 1, 1] + nkk = {1: {1: 2, 2: 2}} + assert is_valid_directed_joint_degree(in_degrees, out_degrees, nkk) + + # not realizable, values are not integers. + nkk = {1: {1: 1.5, 2: 2.5}} + assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk) + + # not realizable, number of edges between 1-2 are insufficient. + nkk = {1: {1: 2, 2: 1}} + assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk) + + # not realizable, in/out degree sequences have different number of nodes. + out_degrees = [1, 1, 1] + nkk = {1: {1: 2, 2: 2}} + assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk) + + # not realizable, degree sequences have fewer than required nodes. + in_degrees = [0, 1, 2] + assert not is_valid_directed_joint_degree(in_degrees, out_degrees, nkk) + + +def test_directed_joint_degree_graph(n=15, m=100, ntimes=1000): + for _ in range(ntimes): + # generate gnm random graph and calculate its joint degree. + g = gnm_random_graph(n, m, None, directed=True) + + # in-degree sequence of g as a list of integers. + in_degrees = list(dict(g.in_degree()).values()) + # out-degree sequence of g as a list of integers. + out_degrees = list(dict(g.out_degree()).values()) + nkk = degree_mixing_dict(g) + + # generate simple directed graph with given degree sequence and joint + # degree matrix. + G = directed_joint_degree_graph(in_degrees, out_degrees, nkk) + + # assert degree sequence correctness. + assert in_degrees == list(dict(G.in_degree()).values()) + assert out_degrees == list(dict(G.out_degree()).values()) + # assert joint degree matrix correctness. + assert nkk == degree_mixing_dict(G) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py new file mode 100644 index 0000000000000000000000000000000000000000..10ac9fc7f0b7ed74c69859ad323b9fbe33a0a460 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_lattice.py @@ -0,0 +1,264 @@ +"""Unit tests for the :mod:`networkx.generators.lattice` module.""" + +from itertools import product + +import pytest + +import networkx as nx +from networkx.utils import edges_equal + + +def test_hexagonal_lattice_no_pos(): + # Test positions are note computed/stored when with_positions=False + G = nx.hexagonal_lattice_graph(6, 6, with_positions=False) + assert all(pos is None for _, pos in G.nodes(data="pos")) + + +@pytest.mark.parametrize( + "lattice_graph", (nx.triangular_lattice_graph, nx.hexagonal_lattice_graph) +) +def test_2D_lattice_no_contraction_leftovers(lattice_graph): + """hexagonal_lattice_graph and triangular_lattice_graph use nx.contracted_nodes + under-the-hood when periodic=True. Check that there are no leftover + "contraction" node attributes on the returned graph.""" + G = lattice_graph(6, 6, with_positions=False, periodic=True) + assert all(data == {} for _, data in G.nodes(data=True)) + assert all(data == {} for _, _, data in G.edges(data=True)) + + +class TestGrid2DGraph: + """Unit tests for :func:`networkx.generators.lattice.grid_2d_graph`""" + + def test_number_of_vertices(self): + m, n = 5, 6 + G = nx.grid_2d_graph(m, n) + assert len(G) == m * n + + def test_degree_distribution(self): + m, n = 5, 6 + G = nx.grid_2d_graph(m, n) + expected_histogram = [0, 0, 4, 2 * (m + n) - 8, (m - 2) * (n - 2)] + assert nx.degree_histogram(G) == expected_histogram + + def test_directed(self): + m, n = 5, 6 + G = nx.grid_2d_graph(m, n) + H = nx.grid_2d_graph(m, n, create_using=nx.DiGraph()) + assert H.succ == G.adj + assert H.pred == G.adj + + def test_multigraph(self): + m, n = 5, 6 + G = nx.grid_2d_graph(m, n) + H = nx.grid_2d_graph(m, n, create_using=nx.MultiGraph()) + assert list(H.edges()) == list(G.edges()) + + def test_periodic(self): + G = nx.grid_2d_graph(0, 0, periodic=True) + assert dict(G.degree()) == {} + + for m, n, H in [ + (2, 2, nx.cycle_graph(4)), + (1, 7, nx.cycle_graph(7)), + (7, 1, nx.cycle_graph(7)), + (2, 5, nx.circular_ladder_graph(5)), + (5, 2, nx.circular_ladder_graph(5)), + (2, 4, nx.cubical_graph()), + (4, 2, nx.cubical_graph()), + ]: + G = nx.grid_2d_graph(m, n, periodic=True) + assert nx.could_be_isomorphic(G, H) + + def test_periodic_iterable(self): + m, n = 3, 7 + for a, b in product([0, 1], [0, 1]): + G = nx.grid_2d_graph(m, n, periodic=(a, b)) + assert G.number_of_nodes() == m * n + assert G.number_of_edges() == (m + a - 1) * n + (n + b - 1) * m + + def test_periodic_directed(self): + G = nx.grid_2d_graph(4, 2, periodic=True) + H = nx.grid_2d_graph(4, 2, periodic=True, create_using=nx.DiGraph()) + assert H.succ == G.adj + assert H.pred == G.adj + + def test_periodic_multigraph(self): + G = nx.grid_2d_graph(4, 2, periodic=True) + H = nx.grid_2d_graph(4, 2, periodic=True, create_using=nx.MultiGraph()) + assert list(G.edges()) == list(H.edges()) + + def test_exceptions(self): + pytest.raises(nx.NetworkXError, nx.grid_2d_graph, -3, 2) + pytest.raises(nx.NetworkXError, nx.grid_2d_graph, 3, -2) + pytest.raises(TypeError, nx.grid_2d_graph, 3.3, 2) + pytest.raises(TypeError, nx.grid_2d_graph, 3, 2.2) + + def test_node_input(self): + G = nx.grid_2d_graph(4, 2, periodic=True) + H = nx.grid_2d_graph(range(4), range(2), periodic=True) + assert nx.is_isomorphic(H, G) + H = nx.grid_2d_graph("abcd", "ef", periodic=True) + assert nx.is_isomorphic(H, G) + G = nx.grid_2d_graph(5, 6) + H = nx.grid_2d_graph(range(5), range(6)) + assert edges_equal(H, G) + + +class TestGridGraph: + """Unit tests for :func:`networkx.generators.lattice.grid_graph`""" + + def test_grid_graph(self): + """grid_graph([n,m]) is a connected simple graph with the + following properties: + number_of_nodes = n*m + degree_histogram = [0,0,4,2*(n+m)-8,(n-2)*(m-2)] + """ + for n, m in [(3, 5), (5, 3), (4, 5), (5, 4)]: + dim = [n, m] + g = nx.grid_graph(dim) + assert len(g) == n * m + assert nx.degree_histogram(g) == [ + 0, + 0, + 4, + 2 * (n + m) - 8, + (n - 2) * (m - 2), + ] + + for n, m in [(1, 5), (5, 1)]: + dim = [n, m] + g = nx.grid_graph(dim) + assert len(g) == n * m + assert nx.is_isomorphic(g, nx.path_graph(5)) + + # mg = nx.grid_graph([n,m], create_using=MultiGraph()) + # assert_equal(mg.edges(), g.edges()) + + def test_node_input(self): + G = nx.grid_graph([range(7, 9), range(3, 6)]) + assert len(G) == 2 * 3 + assert nx.is_isomorphic(G, nx.grid_graph([2, 3])) + + def test_periodic_iterable(self): + m, n, k = 3, 7, 5 + for a, b, c in product([0, 1], [0, 1], [0, 1]): + G = nx.grid_graph([m, n, k], periodic=(a, b, c)) + num_e = (m + a - 1) * n * k + (n + b - 1) * m * k + (k + c - 1) * m * n + assert G.number_of_nodes() == m * n * k + assert G.number_of_edges() == num_e + + +class TestHypercubeGraph: + """Unit tests for :func:`networkx.generators.lattice.hypercube_graph`""" + + def test_special_cases(self): + for n, H in [ + (0, nx.null_graph()), + (1, nx.path_graph(2)), + (2, nx.cycle_graph(4)), + (3, nx.cubical_graph()), + ]: + G = nx.hypercube_graph(n) + assert nx.could_be_isomorphic(G, H) + + def test_degree_distribution(self): + for n in range(1, 10): + G = nx.hypercube_graph(n) + expected_histogram = [0] * n + [2**n] + assert nx.degree_histogram(G) == expected_histogram + + +class TestTriangularLatticeGraph: + "Tests for :func:`networkx.generators.lattice.triangular_lattice_graph`" + + def test_lattice_points(self): + """Tests that the graph is really a triangular lattice.""" + for m, n in [(2, 3), (2, 2), (2, 1), (3, 3), (3, 2), (3, 4)]: + G = nx.triangular_lattice_graph(m, n) + N = (n + 1) // 2 + assert len(G) == (m + 1) * (1 + N) - (n % 2) * ((m + 1) // 2) + for i, j in G.nodes(): + nbrs = G[(i, j)] + if i < N: + assert (i + 1, j) in nbrs + if j < m: + assert (i, j + 1) in nbrs + if j < m and (i > 0 or j % 2) and (i < N or (j + 1) % 2): + assert (i + 1, j + 1) in nbrs or (i - 1, j + 1) in nbrs + + def test_directed(self): + """Tests for creating a directed triangular lattice.""" + G = nx.triangular_lattice_graph(3, 4, create_using=nx.Graph()) + H = nx.triangular_lattice_graph(3, 4, create_using=nx.DiGraph()) + assert H.is_directed() + for u, v in H.edges(): + assert v[1] >= u[1] + if v[1] == u[1]: + assert v[0] > u[0] + + def test_multigraph(self): + """Tests for creating a triangular lattice multigraph.""" + G = nx.triangular_lattice_graph(3, 4, create_using=nx.Graph()) + H = nx.triangular_lattice_graph(3, 4, create_using=nx.MultiGraph()) + assert list(H.edges()) == list(G.edges()) + + def test_periodic(self): + G = nx.triangular_lattice_graph(4, 6, periodic=True) + assert len(G) == 12 + assert G.size() == 36 + # all degrees are 6 + assert len([n for n, d in G.degree() if d != 6]) == 0 + G = nx.triangular_lattice_graph(5, 7, periodic=True) + TLG = nx.triangular_lattice_graph + pytest.raises(nx.NetworkXError, TLG, 2, 4, periodic=True) + pytest.raises(nx.NetworkXError, TLG, 4, 4, periodic=True) + pytest.raises(nx.NetworkXError, TLG, 2, 6, periodic=True) + + +class TestHexagonalLatticeGraph: + "Tests for :func:`networkx.generators.lattice.hexagonal_lattice_graph`" + + def test_lattice_points(self): + """Tests that the graph is really a hexagonal lattice.""" + for m, n in [(4, 5), (4, 4), (4, 3), (3, 2), (3, 3), (3, 5)]: + G = nx.hexagonal_lattice_graph(m, n) + assert len(G) == 2 * (m + 1) * (n + 1) - 2 + C_6 = nx.cycle_graph(6) + hexagons = [ + [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)], + [(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)], + [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)], + [(2, 0), (2, 1), (2, 2), (3, 0), (3, 1), (3, 2)], + [(2, 2), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4)], + ] + for hexagon in hexagons: + assert nx.is_isomorphic(G.subgraph(hexagon), C_6) + + def test_directed(self): + """Tests for creating a directed hexagonal lattice.""" + G = nx.hexagonal_lattice_graph(3, 5, create_using=nx.Graph()) + H = nx.hexagonal_lattice_graph(3, 5, create_using=nx.DiGraph()) + assert H.is_directed() + pos = nx.get_node_attributes(H, "pos") + for u, v in H.edges(): + assert pos[v][1] >= pos[u][1] + if pos[v][1] == pos[u][1]: + assert pos[v][0] > pos[u][0] + + def test_multigraph(self): + """Tests for creating a hexagonal lattice multigraph.""" + G = nx.hexagonal_lattice_graph(3, 5, create_using=nx.Graph()) + H = nx.hexagonal_lattice_graph(3, 5, create_using=nx.MultiGraph()) + assert list(H.edges()) == list(G.edges()) + + def test_periodic(self): + G = nx.hexagonal_lattice_graph(4, 6, periodic=True) + assert len(G) == 48 + assert G.size() == 72 + # all degrees are 3 + assert len([n for n, d in G.degree() if d != 3]) == 0 + G = nx.hexagonal_lattice_graph(5, 8, periodic=True) + HLG = nx.hexagonal_lattice_graph + pytest.raises(nx.NetworkXError, HLG, 2, 7, periodic=True) + pytest.raises(nx.NetworkXError, HLG, 1, 4, periodic=True) + pytest.raises(nx.NetworkXError, HLG, 2, 1, periodic=True) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_line.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_line.py new file mode 100644 index 0000000000000000000000000000000000000000..7b3ff079e0a9bf7ac2233e0cac7f47d9e7d7ea04 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_line.py @@ -0,0 +1,316 @@ +import pytest + +import networkx as nx +from networkx.generators import line +from networkx.utils import edges_equal + + +class TestGeneratorLine: + def test_star(self): + G = nx.star_graph(5) + L = nx.line_graph(G) + assert nx.is_isomorphic(L, nx.complete_graph(5)) + + def test_path(self): + G = nx.path_graph(5) + L = nx.line_graph(G) + assert nx.is_isomorphic(L, nx.path_graph(4)) + + def test_cycle(self): + G = nx.cycle_graph(5) + L = nx.line_graph(G) + assert nx.is_isomorphic(L, G) + + def test_digraph1(self): + G = nx.DiGraph([(0, 1), (0, 2), (0, 3)]) + L = nx.line_graph(G) + # no edge graph, but with nodes + assert L.adj == {(0, 1): {}, (0, 2): {}, (0, 3): {}} + + def test_multigraph1(self): + G = nx.MultiGraph([(0, 1), (0, 1), (1, 0), (0, 2), (2, 0), (0, 3)]) + L = nx.line_graph(G) + # no edge graph, but with nodes + assert edges_equal( + L.edges(), + [ + ((0, 3, 0), (0, 1, 0)), + ((0, 3, 0), (0, 2, 0)), + ((0, 3, 0), (0, 2, 1)), + ((0, 3, 0), (0, 1, 1)), + ((0, 3, 0), (0, 1, 2)), + ((0, 1, 0), (0, 1, 1)), + ((0, 1, 0), (0, 2, 0)), + ((0, 1, 0), (0, 1, 2)), + ((0, 1, 0), (0, 2, 1)), + ((0, 1, 1), (0, 1, 2)), + ((0, 1, 1), (0, 2, 0)), + ((0, 1, 1), (0, 2, 1)), + ((0, 1, 2), (0, 2, 0)), + ((0, 1, 2), (0, 2, 1)), + ((0, 2, 0), (0, 2, 1)), + ], + ) + + def test_multigraph2(self): + G = nx.MultiGraph([(1, 2), (2, 1)]) + L = nx.line_graph(G) + assert edges_equal(L.edges(), [((1, 2, 0), (1, 2, 1))]) + + def test_multidigraph1(self): + G = nx.MultiDiGraph([(1, 2), (2, 1)]) + L = nx.line_graph(G) + assert edges_equal( + L.edges(), [((1, 2, 0), (2, 1, 0)), ((2, 1, 0), (1, 2, 0))], directed=True + ) + + def test_multidigraph2(self): + G = nx.MultiDiGraph([(0, 1), (0, 1), (0, 1), (1, 2)]) + L = nx.line_graph(G) + assert edges_equal( + L.edges(), + [((0, 1, 0), (1, 2, 0)), ((0, 1, 1), (1, 2, 0)), ((0, 1, 2), (1, 2, 0))], + directed=True, + ) + + def test_digraph2(self): + G = nx.DiGraph([(0, 1), (1, 2), (2, 3)]) + L = nx.line_graph(G) + assert edges_equal( + L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))], directed=True + ) + + def test_create1(self): + G = nx.DiGraph([(0, 1), (1, 2), (2, 3)]) + L = nx.line_graph(G, create_using=nx.Graph()) + assert edges_equal(L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))]) + + def test_create2(self): + G = nx.Graph([(0, 1), (1, 2), (2, 3)]) + L = nx.line_graph(G, create_using=nx.DiGraph()) + assert edges_equal( + L.edges(), [((0, 1), (1, 2)), ((1, 2), (2, 3))], directed=True + ) + + +class TestGeneratorInverseLine: + def test_example(self): + G = nx.Graph() + G_edges = [ + [1, 2], + [1, 3], + [1, 4], + [1, 5], + [2, 3], + [2, 5], + [2, 6], + [2, 7], + [3, 4], + [3, 5], + [6, 7], + [6, 8], + [7, 8], + ] + G.add_edges_from(G_edges) + H = nx.inverse_line_graph(G) + solution = nx.Graph() + solution_edges = [ + ("a", "b"), + ("a", "c"), + ("a", "d"), + ("a", "e"), + ("c", "d"), + ("e", "f"), + ("e", "g"), + ("f", "g"), + ] + solution.add_edges_from(solution_edges) + assert nx.is_isomorphic(H, solution) + + def test_example_2(self): + G = nx.Graph() + G_edges = [[1, 2], [1, 3], [2, 3], [3, 4], [3, 5], [4, 5]] + G.add_edges_from(G_edges) + H = nx.inverse_line_graph(G) + solution = nx.Graph() + solution_edges = [("a", "c"), ("b", "c"), ("c", "d"), ("d", "e"), ("d", "f")] + solution.add_edges_from(solution_edges) + assert nx.is_isomorphic(H, solution) + + def test_pair(self): + G = nx.path_graph(2) + H = nx.inverse_line_graph(G) + solution = nx.path_graph(3) + assert nx.is_isomorphic(H, solution) + + def test_line(self): + G = nx.path_graph(5) + solution = nx.path_graph(6) + H = nx.inverse_line_graph(G) + assert nx.is_isomorphic(H, solution) + + def test_triangle_graph(self): + G = nx.complete_graph(3) + H = nx.inverse_line_graph(G) + alternative_solution = nx.Graph() + alternative_solution.add_edges_from([[0, 1], [0, 2], [0, 3]]) + # there are two alternative inverse line graphs for this case + # so long as we get one of them the test should pass + assert nx.is_isomorphic(H, G) or nx.is_isomorphic(H, alternative_solution) + + def test_cycle(self): + G = nx.cycle_graph(5) + H = nx.inverse_line_graph(G) + assert nx.is_isomorphic(H, G) + + def test_empty(self): + G = nx.Graph() + H = nx.inverse_line_graph(G) + assert nx.is_isomorphic(H, nx.complete_graph(1)) + + def test_K1(self): + G = nx.complete_graph(1) + H = nx.inverse_line_graph(G) + solution = nx.path_graph(2) + assert nx.is_isomorphic(H, solution) + + def test_edgeless_graph(self): + G = nx.empty_graph(5) + with pytest.raises(nx.NetworkXError, match="edgeless graph"): + nx.inverse_line_graph(G) + + def test_selfloops_error(self): + G = nx.cycle_graph(4) + G.add_edge(0, 0) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + def test_non_line_graphs(self): + # Tests several known non-line graphs for impossibility + # Adapted from L.W.Beineke, "Characterizations of derived graphs" + + # claw graph + claw = nx.star_graph(3) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, claw) + + # wheel graph with 6 nodes + wheel = nx.wheel_graph(6) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, wheel) + + # K5 with one edge remove + K5m = nx.complete_graph(5) + K5m.remove_edge(0, 1) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, K5m) + + # graph without any odd triangles (contains claw as induced subgraph) + G = nx.compose(nx.path_graph(2), nx.complete_bipartite_graph(2, 3)) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + ## Variations on a diamond graph + + # Diamond + 2 edges (+ "roof") + G = nx.diamond_graph() + G.add_edges_from([(4, 0), (5, 3)]) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + G.add_edge(4, 5) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + # Diamond + 2 connected edges + G = nx.diamond_graph() + G.add_edges_from([(4, 0), (4, 3)]) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + # Diamond + K3 + one edge (+ 2*K3) + G = nx.diamond_graph() + G.add_edges_from([(4, 0), (4, 1), (4, 2), (5, 3)]) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + G.add_edges_from([(5, 1), (5, 2)]) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + # 4 triangles + G = nx.diamond_graph() + G.add_edges_from([(4, 0), (4, 1), (5, 2), (5, 3)]) + pytest.raises(nx.NetworkXError, nx.inverse_line_graph, G) + + def test_wrong_graph_type(self): + G = nx.DiGraph() + G_edges = [[0, 1], [0, 2], [0, 3]] + G.add_edges_from(G_edges) + pytest.raises(nx.NetworkXNotImplemented, nx.inverse_line_graph, G) + + G = nx.MultiGraph() + G_edges = [[0, 1], [0, 2], [0, 3]] + G.add_edges_from(G_edges) + pytest.raises(nx.NetworkXNotImplemented, nx.inverse_line_graph, G) + + def test_line_inverse_line_complete(self): + G = nx.complete_graph(10) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_path(self): + G = nx.path_graph(10) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_hypercube(self): + G = nx.hypercube_graph(5) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_cycle(self): + G = nx.cycle_graph(10) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_star(self): + G = nx.star_graph(20) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_multipartite(self): + G = nx.complete_multipartite_graph(3, 4, 5) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_inverse_line_dgm(self): + G = nx.dorogovtsev_goltsev_mendes_graph(4) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + def test_line_different_node_types(self): + G = nx.path_graph([1, 2, 3, "a", "b", "c"]) + H = nx.line_graph(G) + J = nx.inverse_line_graph(H) + assert nx.is_isomorphic(G, J) + + +class TestGeneratorPrivateFunctions: + def test_triangles_error(self): + G = nx.diamond_graph() + pytest.raises(nx.NetworkXError, line._triangles, G, (4, 0)) + pytest.raises(nx.NetworkXError, line._triangles, G, (0, 3)) + + def test_odd_triangles_error(self): + G = nx.diamond_graph() + pytest.raises(nx.NetworkXError, line._odd_triangle, G, (0, 1, 4)) + pytest.raises(nx.NetworkXError, line._odd_triangle, G, (0, 1, 3)) + + def test_select_starting_cell_error(self): + G = nx.diamond_graph() + pytest.raises(nx.NetworkXError, line._select_starting_cell, G, (4, 0)) + pytest.raises(nx.NetworkXError, line._select_starting_cell, G, (0, 3)) + + def test_diamond_graph(self): + G = nx.diamond_graph() + for edge in G.edges: + cell = line._select_starting_cell(G, starting_edge=edge) + # Starting cell should always be one of the two triangles + assert len(cell) == 3 + assert all(v in G[u] for u in cell for v in cell if u != v) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py new file mode 100644 index 0000000000000000000000000000000000000000..eb12b1412ad4559bb500a7125c8d65e6239c5fed --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_mycielski.py @@ -0,0 +1,30 @@ +"""Unit tests for the :mod:`networkx.generators.mycielski` module.""" + +import pytest + +import networkx as nx + + +class TestMycielski: + def test_construction(self): + G = nx.path_graph(2) + M = nx.mycielskian(G) + assert nx.is_isomorphic(M, nx.cycle_graph(5)) + + def test_size(self): + G = nx.path_graph(2) + M = nx.mycielskian(G, 2) + assert len(M) == 11 + assert M.size() == 20 + + def test_mycielski_graph_generator(self): + G = nx.mycielski_graph(1) + assert nx.is_isomorphic(G, nx.empty_graph(1)) + G = nx.mycielski_graph(2) + assert nx.is_isomorphic(G, nx.path_graph(2)) + G = nx.mycielski_graph(3) + assert nx.is_isomorphic(G, nx.cycle_graph(5)) + G = nx.mycielski_graph(4) + assert nx.is_isomorphic(G, nx.mycielskian(nx.cycle_graph(5))) + with pytest.raises(nx.NetworkXError, match="must satisfy n >= 1"): + nx.mycielski_graph(0) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py new file mode 100644 index 0000000000000000000000000000000000000000..7c09303cc98f329084160733e8004fbe2710255f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_nonisomorphic_trees.py @@ -0,0 +1,82 @@ +""" +Unit tests for WROM algorithm generator in generators/nonisomorphic_trees.py +""" + +import pytest + +import networkx as nx +from networkx.utils import edges_equal + + +def test_nonisomorphic_tree_negative_order(): + with pytest.raises(ValueError, match="order must be non-negative"): + nx.number_of_nonisomorphic_trees(-1) + with pytest.raises(ValueError, match="order must be non-negative"): + next(nx.nonisomorphic_trees(-1)) + + +def test_nonisomorphic_tree_order_0(): + assert nx.number_of_nonisomorphic_trees(0) == 0 + assert list(nx.nonisomorphic_trees(0)) == [] + + +def test_nonisomorphic_tree_order_1(): + assert nx.number_of_nonisomorphic_trees(1) == 1 + nit_list = list(nx.nonisomorphic_trees(1)) + assert len(nit_list) == 1 + G = nit_list[0] + assert nx.utils.graphs_equal(G, nx.empty_graph(1)) + + +@pytest.mark.parametrize("n", range(5)) +def test_nonisomorphic_tree_low_order_agreement(n): + """Ensure all the order<2 'special cases' are consistent.""" + assert len(list(nx.nonisomorphic_trees(n))) == nx.number_of_nonisomorphic_trees(n) + + +class TestGeneratorNonIsomorphicTrees: + def test_tree_structure(self): + # test for tree structure for nx.nonisomorphic_trees() + def f(x): + return list(nx.nonisomorphic_trees(x)) + + for i in f(6): + assert nx.is_tree(i) + for i in f(8): + assert nx.is_tree(i) + + def test_nonisomorphism(self): + # test for nonisomorphism of trees for nx.nonisomorphic_trees() + def f(x): + return list(nx.nonisomorphic_trees(x)) + + trees = f(6) + for i in range(len(trees)): + for j in range(i + 1, len(trees)): + assert not nx.is_isomorphic(trees[i], trees[j]) + trees = f(8) + for i in range(len(trees)): + for j in range(i + 1, len(trees)): + assert not nx.is_isomorphic(trees[i], trees[j]) + + def test_number_of_nonisomorphic_trees(self): + # http://oeis.org/A000055 + assert nx.number_of_nonisomorphic_trees(2) == 1 + assert nx.number_of_nonisomorphic_trees(3) == 1 + assert nx.number_of_nonisomorphic_trees(4) == 2 + assert nx.number_of_nonisomorphic_trees(5) == 3 + assert nx.number_of_nonisomorphic_trees(6) == 6 + assert nx.number_of_nonisomorphic_trees(7) == 11 + assert nx.number_of_nonisomorphic_trees(8) == 23 + assert nx.number_of_nonisomorphic_trees(9) == 47 + assert nx.number_of_nonisomorphic_trees(10) == 106 + assert nx.number_of_nonisomorphic_trees(20) == 823065 + assert nx.number_of_nonisomorphic_trees(30) == 14830871802 + + def test_nonisomorphic_trees(self): + def f(x): + return list(nx.nonisomorphic_trees(x)) + + assert edges_equal(f(3)[0].edges(), [(0, 1), (0, 2)]) + assert edges_equal(f(4)[0].edges(), [(0, 1), (0, 3), (1, 2)]) + assert edges_equal(f(4)[1].edges(), [(0, 1), (0, 2), (0, 3)]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py new file mode 100644 index 0000000000000000000000000000000000000000..85066520ae59f1e9bec03327630276918d573fb2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_clustered.py @@ -0,0 +1,33 @@ +import pytest + +import networkx as nx + + +class TestRandomClusteredGraph: + def test_custom_joint_degree_sequence(self): + node = [1, 1, 1, 2, 1, 2, 0, 0] + tri = [0, 0, 0, 0, 0, 1, 1, 1] + joint_degree_sequence = zip(node, tri) + G = nx.random_clustered_graph(joint_degree_sequence) + assert G.number_of_nodes() == 8 + assert G.number_of_edges() == 7 + + def test_tuple_joint_degree_sequence(self): + G = nx.random_clustered_graph([(1, 2), (2, 1), (1, 1), (1, 1), (1, 1), (2, 0)]) + assert G.number_of_nodes() == 6 + assert G.number_of_edges() == 10 + + def test_invalid_joint_degree_sequence_type(self): + with pytest.raises(nx.NetworkXError, match="Invalid degree sequence"): + nx.random_clustered_graph([[1, 1], [2, 1], [0, 1]]) + + def test_invalid_joint_degree_sequence_value(self): + with pytest.raises(nx.NetworkXError, match="Invalid degree sequence"): + nx.random_clustered_graph([[1, 1], [1, 2], [0, 1]]) + + def test_directed_graph_raises_error(self): + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"): + nx.random_clustered_graph( + [(1, 2), (2, 1), (1, 1), (1, 1), (1, 1), (2, 0)], + create_using=nx.DiGraph, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py new file mode 100644 index 0000000000000000000000000000000000000000..db8d6996fd4c501100818aa7a7a71c5d2207d3d2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_random_graphs.py @@ -0,0 +1,495 @@ +"""Unit tests for the :mod:`networkx.generators.random_graphs` module.""" + +import pytest + +import networkx as nx + +_gnp_generators = [ + nx.gnp_random_graph, + nx.fast_gnp_random_graph, + nx.binomial_graph, + nx.erdos_renyi_graph, +] + + +@pytest.mark.parametrize("generator", _gnp_generators) +@pytest.mark.parametrize("directed", (True, False)) +def test_gnp_generators_negative_edge_probability(generator, directed): + """If the edge probability `p` is <=0, the resulting graph should have no edges.""" + G = generator(10, -1.1, directed=directed) + assert len(G) == 10 + assert G.number_of_edges() == 0 + assert G.is_directed() == directed + + +@pytest.mark.parametrize("generator", _gnp_generators) +@pytest.mark.parametrize( + ("directed", "expected_num_edges"), + [(False, 45), (True, 90)], +) +def test_gnp_generators_greater_than_1_edge_probability( + generator, directed, expected_num_edges +): + """If the edge probability `p` is >=1, the resulting graph should be complete.""" + G = generator(10, 1.1, directed=directed) + assert len(G) == 10 + assert G.number_of_edges() == expected_num_edges + assert G.is_directed() == directed + + +@pytest.mark.parametrize("generator", _gnp_generators) +@pytest.mark.parametrize("directed", (True, False)) +def test_gnp_generators_basic(generator, directed): + """If the edge probability `p` is >0 and <1, test only the basic properties.""" + G = generator(10, 0.1, directed=directed) + assert len(G) == 10 + assert G.is_directed() == directed + + +@pytest.mark.parametrize("generator", _gnp_generators) +def test_gnp_generators_for_p_close_to_1(generator): + """If the edge probability `p` is close to 1, the resulting graph should have all edges.""" + runs = 100 + edges = sum( + generator(10, 0.99999, directed=True).number_of_edges() for _ in range(runs) + ) + assert abs(edges / float(runs) - 90) <= runs * 2.0 / 100 + + +@pytest.mark.parametrize("generator", _gnp_generators) +@pytest.mark.parametrize("p", (0.2, 0.8)) +@pytest.mark.parametrize("directed", (True, False)) +def test_gnp_generators_edge_probability(generator, p, directed): + """Test that gnp generators generate edges according to the their probability `p`.""" + runs = 5000 + n = 5 + edge_counts = [[0] * n for _ in range(n)] + for i in range(runs): + G = generator(n, p, directed=directed) + for v, w in G.edges: + edge_counts[v][w] += 1 + if not directed: + edge_counts[w][v] += 1 + for v in range(n): + for w in range(n): + if v == w: + # There should be no loops + assert edge_counts[v][w] == 0 + else: + # Each edge should have been generated with probability close to p + assert abs(edge_counts[v][w] / float(runs) - p) <= 0.03 + + +@pytest.mark.parametrize( + "generator", [nx.gnp_random_graph, nx.binomial_graph, nx.erdos_renyi_graph] +) +@pytest.mark.parametrize( + ("seed", "directed", "expected_num_edges"), + [(42, False, 1219), (42, True, 2454), (314, False, 1247), (314, True, 2476)], +) +def test_gnp_random_graph_aliases(generator, seed, directed, expected_num_edges): + """Test that aliases give the same result with the same seed.""" + G = generator(100, 0.25, seed=seed, directed=directed) + assert len(G) == 100 + assert G.number_of_edges() == expected_num_edges + assert G.is_directed() == directed + + +class TestGeneratorsRandom: + def test_random_graph(self): + seed = 42 + G = nx.gnm_random_graph(100, 20, seed) + G = nx.gnm_random_graph(100, 20, seed, directed=True) + G = nx.dense_gnm_random_graph(100, 20, seed) + + G = nx.barabasi_albert_graph(100, 1, seed) + G = nx.barabasi_albert_graph(100, 3, seed) + assert G.number_of_edges() == (97 * 3) + + G = nx.barabasi_albert_graph(100, 3, seed, nx.complete_graph(5)) + assert G.number_of_edges() == (10 + 95 * 3) + + G = nx.extended_barabasi_albert_graph(100, 1, 0, 0, seed) + assert G.number_of_edges() == 99 + G = nx.extended_barabasi_albert_graph(100, 3, 0, 0, seed) + assert G.number_of_edges() == 97 * 3 + G = nx.extended_barabasi_albert_graph(100, 1, 0, 0.5, seed) + assert G.number_of_edges() == 99 + G = nx.extended_barabasi_albert_graph(100, 2, 0.5, 0, seed) + assert G.number_of_edges() > 100 * 3 + assert G.number_of_edges() < 100 * 4 + + G = nx.extended_barabasi_albert_graph(100, 2, 0.3, 0.3, seed) + assert G.number_of_edges() > 100 * 2 + assert G.number_of_edges() < 100 * 4 + + G = nx.powerlaw_cluster_graph(100, 1, 1.0, seed) + G = nx.powerlaw_cluster_graph(100, 3, 0.0, seed) + assert G.number_of_edges() == (97 * 3) + + G = nx.random_regular_graph(10, 20, seed) + + pytest.raises(nx.NetworkXError, nx.random_regular_graph, 3, 21) + pytest.raises(nx.NetworkXError, nx.random_regular_graph, 33, 21) + + constructor = [(10, 20, 0.8), (20, 40, 0.8)] + G = nx.random_shell_graph(constructor, seed) + + def is_caterpillar(g): + """ + A tree is a caterpillar iff all nodes of degree >=3 are surrounded + by at most two nodes of degree two or greater. + ref: http://mathworld.wolfram.com/CaterpillarGraph.html + """ + deg_over_3 = [n for n in g if g.degree(n) >= 3] + for n in deg_over_3: + nbh_deg_over_2 = [nbh for nbh in g.neighbors(n) if g.degree(nbh) >= 2] + if not len(nbh_deg_over_2) <= 2: + return False + return True + + def is_lobster(g): + """ + A tree is a lobster if it has the property that the removal of leaf + nodes leaves a caterpillar graph (Gallian 2007) + ref: http://mathworld.wolfram.com/LobsterGraph.html + """ + non_leafs = [n for n in g if g.degree(n) > 1] + return is_caterpillar(g.subgraph(non_leafs)) + + G = nx.random_lobster_graph(10, 0.1, 0.5, seed) + assert max(G.degree(n) for n in G.nodes()) > 3 + assert is_lobster(G) + pytest.raises(nx.NetworkXError, nx.random_lobster_graph, 10, 0.1, 1, seed) + pytest.raises(nx.NetworkXError, nx.random_lobster_graph, 10, 1, 1, seed) + pytest.raises(nx.NetworkXError, nx.random_lobster_graph, 10, 1, 0.5, seed) + + # docstring says this should be a caterpillar + G = nx.random_lobster_graph(10, 0.1, 0.0, seed) + assert is_caterpillar(G) + + # difficult to find seed that requires few tries + seq = nx.random_powerlaw_tree_sequence(10, 3, seed=14, tries=1) + G = nx.random_powerlaw_tree(10, 3, seed=14, tries=1) + + def test_dual_barabasi_albert(self, m1=1, m2=4, p=0.5): + """ + Tests that the dual BA random graph generated behaves consistently. + + Tests the exceptions are raised as expected. + + The graphs generation are repeated several times to prevent lucky shots + + """ + seeds = [42, 314, 2718] + initial_graph = nx.complete_graph(10) + + for seed in seeds: + # This should be BA with m = m1 + BA1 = nx.barabasi_albert_graph(100, m1, seed) + DBA1 = nx.dual_barabasi_albert_graph(100, m1, m2, 1, seed) + assert BA1.edges() == DBA1.edges() + + # This should be BA with m = m2 + BA2 = nx.barabasi_albert_graph(100, m2, seed) + DBA2 = nx.dual_barabasi_albert_graph(100, m1, m2, 0, seed) + assert BA2.edges() == DBA2.edges() + + BA3 = nx.barabasi_albert_graph(100, m1, seed) + DBA3 = nx.dual_barabasi_albert_graph(100, m1, m1, p, seed) + # We can't compare edges here since randomness is "consumed" when drawing + # between m1 and m2 + assert BA3.size() == DBA3.size() + + DBA = nx.dual_barabasi_albert_graph(100, m1, m2, p, seed, initial_graph) + BA1 = nx.barabasi_albert_graph(100, m1, seed, initial_graph) + BA2 = nx.barabasi_albert_graph(100, m2, seed, initial_graph) + assert ( + min(BA1.size(), BA2.size()) <= DBA.size() <= max(BA1.size(), BA2.size()) + ) + + # Testing exceptions + dbag = nx.dual_barabasi_albert_graph + pytest.raises(nx.NetworkXError, dbag, m1, m1, m2, 0) + pytest.raises(nx.NetworkXError, dbag, m2, m1, m2, 0) + pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, -0.5) + pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, 1.5) + initial = nx.complete_graph(max(m1, m2) - 1) + pytest.raises(nx.NetworkXError, dbag, 100, m1, m2, p, initial_graph=initial) + + def test_extended_barabasi_albert(self, m=2): + """ + Tests that the extended BA random graph generated behaves consistently. + + Tests the exceptions are raised as expected. + + The graphs generation are repeated several times to prevent lucky-shots + + """ + seeds = [42, 314, 2718] + + for seed in seeds: + BA_model = nx.barabasi_albert_graph(100, m, seed) + BA_model_edges = BA_model.number_of_edges() + + # This behaves just like BA, the number of edges must be the same + G1 = nx.extended_barabasi_albert_graph(100, m, 0, 0, seed) + assert G1.size() == BA_model_edges + + # More than twice more edges should have been added + G1 = nx.extended_barabasi_albert_graph(100, m, 0.8, 0, seed) + assert G1.size() > BA_model_edges * 2 + + # Only edge rewiring, so the number of edges less than original + G2 = nx.extended_barabasi_albert_graph(100, m, 0, 0.8, seed) + assert G2.size() == BA_model_edges + + # Mixed scenario: less edges than G1 and more edges than G2 + G3 = nx.extended_barabasi_albert_graph(100, m, 0.3, 0.3, seed) + assert G3.size() > G2.size() + assert G3.size() < G1.size() + + # Testing exceptions + ebag = nx.extended_barabasi_albert_graph + pytest.raises(nx.NetworkXError, ebag, m, m, 0, 0) + pytest.raises(nx.NetworkXError, ebag, 1, 0.5, 0, 0) + pytest.raises(nx.NetworkXError, ebag, 100, 2, 0.5, 0.5) + + def test_random_zero_regular_graph(self): + """Tests that a 0-regular graph has the correct number of nodes and + edges. + + """ + seed = 42 + G = nx.random_regular_graph(0, 10, seed) + assert len(G) == 10 + assert G.number_of_edges() == 0 + + def test_gnm(self): + G = nx.gnm_random_graph(10, 3) + assert len(G) == 10 + assert G.number_of_edges() == 3 + + G = nx.gnm_random_graph(10, 3, seed=42) + assert len(G) == 10 + assert G.number_of_edges() == 3 + + G = nx.gnm_random_graph(10, 100) + assert len(G) == 10 + assert G.number_of_edges() == 45 + + G = nx.gnm_random_graph(10, 100, directed=True) + assert len(G) == 10 + assert G.number_of_edges() == 90 + + G = nx.gnm_random_graph(10, -1.1) + assert len(G) == 10 + assert G.number_of_edges() == 0 + + def test_watts_strogatz_big_k(self): + # Test to make sure than n <= k + pytest.raises(nx.NetworkXError, nx.watts_strogatz_graph, 10, 11, 0.25) + pytest.raises(nx.NetworkXError, nx.newman_watts_strogatz_graph, 10, 11, 0.25) + + # could create an infinite loop, now doesn't + # infinite loop used to occur when a node has degree n-1 and needs to rewire + nx.watts_strogatz_graph(10, 9, 0.25, seed=0) + nx.newman_watts_strogatz_graph(10, 9, 0.5, seed=0) + + # Test k==n scenario + nx.watts_strogatz_graph(10, 10, 0.25, seed=0) + nx.newman_watts_strogatz_graph(10, 10, 0.25, seed=0) + + def test_random_kernel_graph(self): + def integral(u, w, z): + return c * (z - w) + + def root(u, w, r): + return r / c + w + + c = 1 + graph = nx.random_kernel_graph(1000, integral, root, seed=42) + assert len(graph) == 1000 + + def test_random_kernel_graph_default_root(self): + """When `kernel_root` is not provided, `sp.optimize.brentq` is used to + construct the default kernel. + """ + pytest.importorskip("scipy") + + def integral(u, w, z): + return z - w + + graph = nx.random_kernel_graph(1000, integral, seed=42) + assert len(graph) == 1000 + + +@pytest.mark.parametrize( + ("k", "expected_num_nodes", "expected_num_edges"), + [ + (2, 10, 10), + (4, 10, 20), + ], +) +def test_watts_strogatz(k, expected_num_nodes, expected_num_edges): + G = nx.watts_strogatz_graph(10, k, 0.25, seed=42) + assert len(G) == expected_num_nodes + assert G.number_of_edges() == expected_num_edges + + +def test_newman_watts_strogatz_zero_probability(): + G = nx.newman_watts_strogatz_graph(10, 2, 0.0, seed=42) + assert len(G) == 10 + assert G.number_of_edges() == 10 + + +def test_newman_watts_strogatz_nonzero_probability(): + G = nx.newman_watts_strogatz_graph(10, 4, 0.25, seed=42) + assert len(G) == 10 + assert G.number_of_edges() >= 20 + + +def test_connected_watts_strogatz(): + G = nx.connected_watts_strogatz_graph(10, 2, 0.1, tries=10, seed=42) + assert len(G) == 10 + assert G.number_of_edges() == 10 + + +def test_connected_watts_strogatz_zero_tries(): + with pytest.raises(nx.NetworkXError, match="Maximum number of tries exceeded"): + nx.connected_watts_strogatz_graph(10, 2, 0.1, tries=0) + + +def test_connected_watts_strogatz_graph_disconnected(): + """Test that `connected_watts_strogatz_graph` properly loops when disconnected.""" + with pytest.raises(nx.NetworkXError, match="Maximum number of tries exceeded"): + nx.connected_watts_strogatz_graph(10, 0, 0.0) + + +@pytest.mark.parametrize( + "generator, kwargs", + [ + (nx.fast_gnp_random_graph, {"n": 20, "p": 0.2, "directed": False}), + (nx.fast_gnp_random_graph, {"n": 20, "p": 0.2, "directed": True}), + (nx.gnp_random_graph, {"n": 20, "p": 0.2, "directed": False}), + (nx.gnp_random_graph, {"n": 20, "p": 0.2, "directed": True}), + (nx.dense_gnm_random_graph, {"n": 30, "m": 4}), + (nx.gnm_random_graph, {"n": 30, "m": 4, "directed": False}), + (nx.gnm_random_graph, {"n": 30, "m": 4, "directed": True}), + (nx.newman_watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}), + (nx.watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}), + (nx.connected_watts_strogatz_graph, {"n": 50, "k": 5, "p": 0.1}), + (nx.random_regular_graph, {"d": 5, "n": 20}), + (nx.barabasi_albert_graph, {"n": 40, "m": 3}), + (nx.dual_barabasi_albert_graph, {"n": 40, "m1": 3, "m2": 2, "p": 0.1}), + (nx.extended_barabasi_albert_graph, {"n": 40, "m": 3, "p": 0.1, "q": 0.2}), + (nx.powerlaw_cluster_graph, {"n": 40, "m": 3, "p": 0.1}), + (nx.random_lobster_graph, {"n": 40, "p1": 0.1, "p2": 0.2}), + (nx.random_shell_graph, {"constructor": [(10, 20, 0.8), (20, 40, 0.8)]}), + (nx.random_powerlaw_tree, {"n": 10, "seed": 14, "tries": 1}), + ( + nx.random_kernel_graph, + { + "n": 10, + "kernel_integral": lambda u, w, z: z - w, + "kernel_root": lambda u, w, r: r + w, + }, + ), + ], +) +@pytest.mark.parametrize("create_using_instance", [False, True]) +def test_create_using(generator, kwargs, create_using_instance): + class DummyGraph(nx.Graph): + pass + + class DummyDiGraph(nx.DiGraph): + pass + + create_using_type = DummyDiGraph if kwargs.get("directed") else DummyGraph + create_using = create_using_type() if create_using_instance else create_using_type + graph = generator(**kwargs, create_using=create_using) + assert isinstance(graph, create_using_type) + + +@pytest.mark.parametrize("directed", [True, False]) +@pytest.mark.parametrize("fn", (nx.fast_gnp_random_graph, nx.gnp_random_graph)) +def test_gnp_fns_disallow_multigraph(fn, directed): + with pytest.raises(nx.NetworkXError, match="must not be a multi-graph"): + fn(20, 0.2, create_using=nx.MultiGraph) + + +@pytest.mark.parametrize("fn", (nx.gnm_random_graph, nx.dense_gnm_random_graph)) +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_gnm_fns_disallow_directed_and_multigraph(fn, graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + fn(10, 20, create_using=graphtype) + + +@pytest.mark.parametrize( + "fn", + ( + nx.newman_watts_strogatz_graph, + nx.watts_strogatz_graph, + nx.connected_watts_strogatz_graph, + ), +) +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_watts_strogatz_disallow_directed_and_multigraph(fn, graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + fn(10, 2, 0.2, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_random_regular_graph_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.random_regular_graph(2, 10, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_barabasi_albert_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.barabasi_albert_graph(10, 3, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_dual_barabasi_albert_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.dual_barabasi_albert_graph(10, 2, 1, 0.4, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_extended_barabasi_albert_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.extended_barabasi_albert_graph(10, 2, 0.2, 0.3, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_powerlaw_cluster_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.powerlaw_cluster_graph(10, 5, 0.2, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_random_lobster_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.random_lobster_graph(10, 0.1, 0.1, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_random_shell_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.random_shell_graph([(10, 20, 2), (10, 20, 5)], create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_random_powerlaw_tree_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.random_powerlaw_tree(10, create_using=graphtype) + + +@pytest.mark.parametrize("graphtype", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)) +def test_random_kernel_disallow_directed_and_multigraph(graphtype): + with pytest.raises(nx.NetworkXError, match="must not be"): + nx.random_kernel_graph( + 10, lambda y, a, b: a + b, lambda u, w, r: r + w, create_using=graphtype + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_small.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_small.py new file mode 100644 index 0000000000000000000000000000000000000000..2072678e473d4ec7f094b38865fc080058f94218 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_small.py @@ -0,0 +1,220 @@ +import pytest + +import networkx as nx + +null = nx.null_graph() + + +class TestGeneratorsSmall: + def test__LCF_graph(self): + # If n<=0, then return the null_graph + G = nx.LCF_graph(-10, [1, 2], 100) + assert nx.could_be_isomorphic(G, null) + G = nx.LCF_graph(0, [1, 2], 3) + assert nx.could_be_isomorphic(G, null) + G = nx.LCF_graph(0, [1, 2], 10) + assert nx.could_be_isomorphic(G, null) + + # Test that LCF(n,[],0) == cycle_graph(n) + for a, b, c in [(5, [], 0), (10, [], 0), (5, [], 1), (10, [], 10)]: + G = nx.LCF_graph(a, b, c) + assert nx.could_be_isomorphic(G, nx.cycle_graph(a)) + + # Generate the utility graph K_{3,3} + G = nx.LCF_graph(6, [3, -3], 3) + utility_graph = nx.complete_bipartite_graph(3, 3) + assert nx.could_be_isomorphic(G, utility_graph) + + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"): + G = nx.LCF_graph(6, [3, -3], 3, create_using=nx.DiGraph) + + def test_properties_of_named_small_graphs(self): + G = nx.bull_graph() + assert sorted(G) == list(range(5)) + assert G.number_of_edges() == 5 + assert sorted(d for n, d in G.degree()) == [1, 1, 2, 3, 3] + assert nx.diameter(G) == 3 + assert nx.radius(G) == 2 + + G = nx.chvatal_graph() + assert sorted(G) == list(range(12)) + assert G.number_of_edges() == 24 + assert [d for n, d in G.degree()] == 12 * [4] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 2 + + G = nx.cubical_graph() + assert sorted(G) == list(range(8)) + assert G.number_of_edges() == 12 + assert [d for n, d in G.degree()] == 8 * [3] + assert nx.diameter(G) == 3 + assert nx.radius(G) == 3 + + G = nx.desargues_graph() + assert sorted(G) == list(range(20)) + assert G.number_of_edges() == 30 + assert [d for n, d in G.degree()] == 20 * [3] + assert nx.is_isomorphic(G, nx.generalized_petersen_graph(10, 3)) + + G = nx.diamond_graph() + assert sorted(G) == list(range(4)) + assert sorted(d for n, d in G.degree()) == [2, 2, 3, 3] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 1 + + G = nx.dodecahedral_graph() + assert sorted(G) == list(range(20)) + assert G.number_of_edges() == 30 + assert [d for n, d in G.degree()] == 20 * [3] + assert nx.diameter(G) == 5 + assert nx.radius(G) == 5 + assert nx.is_isomorphic(G, nx.generalized_petersen_graph(10, 2)) + + G = nx.frucht_graph() + assert sorted(G) == list(range(12)) + assert G.number_of_edges() == 18 + assert [d for n, d in G.degree()] == 12 * [3] + assert nx.diameter(G) == 4 + assert nx.radius(G) == 3 + + G = nx.generalized_petersen_graph(10, 4) + assert sorted(G) == list(range(20)) + assert G.number_of_edges() == 30 + assert [d for n, d in G.degree()] == 20 * [3] + assert nx.diameter(G) == 4 + assert nx.radius(G) == 4 + + G = nx.heawood_graph() + assert sorted(G) == list(range(14)) + assert G.number_of_edges() == 21 + assert [d for n, d in G.degree()] == 14 * [3] + assert nx.diameter(G) == 3 + assert nx.radius(G) == 3 + + G = nx.hoffman_singleton_graph() + assert sorted(G) == list(range(50)) + assert G.number_of_edges() == 175 + assert [d for n, d in G.degree()] == 50 * [7] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 2 + + G = nx.house_graph() + assert sorted(G) == list(range(5)) + assert G.number_of_edges() == 6 + assert sorted(d for n, d in G.degree()) == [2, 2, 2, 3, 3] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 2 + + G = nx.house_x_graph() + assert sorted(G) == list(range(5)) + assert G.number_of_edges() == 8 + assert sorted(d for n, d in G.degree()) == [2, 3, 3, 4, 4] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 1 + + G = nx.icosahedral_graph() + assert sorted(G) == list(range(12)) + assert G.number_of_edges() == 30 + assert [d for n, d in G.degree()] == [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] + assert nx.diameter(G) == 3 + assert nx.radius(G) == 3 + + G = nx.krackhardt_kite_graph() + assert sorted(G) == list(range(10)) + assert G.number_of_edges() == 18 + assert sorted(d for n, d in G.degree()) == [1, 2, 3, 3, 3, 4, 4, 5, 5, 6] + + G = nx.moebius_kantor_graph() + assert sorted(G) == list(range(16)) + assert G.number_of_edges() == 24 + assert [d for n, d in G.degree()] == 16 * [3] + assert nx.diameter(G) == 4 + assert nx.is_isomorphic(G, nx.generalized_petersen_graph(8, 3)) + + G = nx.octahedral_graph() + assert sorted(G) == list(range(6)) + assert G.number_of_edges() == 12 + assert [d for n, d in G.degree()] == 6 * [4] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 2 + + G = nx.pappus_graph() + assert sorted(G) == list(range(18)) + assert G.number_of_edges() == 27 + assert [d for n, d in G.degree()] == 18 * [3] + assert nx.diameter(G) == 4 + + G = nx.petersen_graph() + assert sorted(G) == list(range(10)) + assert G.number_of_edges() == 15 + assert [d for n, d in G.degree()] == 10 * [3] + assert nx.diameter(G) == 2 + assert nx.radius(G) == 2 + assert nx.is_isomorphic(G, nx.generalized_petersen_graph(5, 2)) + + G = nx.sedgewick_maze_graph() + assert sorted(G) == list(range(8)) + assert G.number_of_edges() == 10 + assert sorted(d for n, d in G.degree()) == [1, 2, 2, 2, 3, 3, 3, 4] + + G = nx.tetrahedral_graph() + assert sorted(G) == list(range(4)) + assert G.number_of_edges() == 6 + assert [d for n, d in G.degree()] == [3, 3, 3, 3] + assert nx.diameter(G) == 1 + assert nx.radius(G) == 1 + + G = nx.truncated_cube_graph() + assert sorted(G) == list(range(24)) + assert G.number_of_edges() == 36 + assert [d for n, d in G.degree()] == 24 * [3] + + G = nx.truncated_tetrahedron_graph() + assert sorted(G) == list(range(12)) + assert G.number_of_edges() == 18 + assert [d for n, d in G.degree()] == 12 * [3] + + G = nx.tutte_graph() + assert sorted(G) == list(range(46)) + assert G.number_of_edges() == 69 + assert [d for n, d in G.degree()] == 46 * [3] + + MG = nx.tutte_graph(create_using=nx.MultiGraph) + assert sorted(MG.edges()) == sorted(G.edges()) + + # Test create_using with directed or multigraphs on small graphs + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported "): + nx.generalized_petersen_graph(5, 2, create_using=nx.DiGraph) + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported "): + nx.generalized_petersen_graph(5, 2, create_using=nx.MultiDiGraph) + G = nx.generalized_petersen_graph(5, 2) + MG = nx.generalized_petersen_graph(5, 2, create_using=nx.MultiGraph) + assert sorted(MG.edges()) == sorted(G.edges()) + + +@pytest.mark.parametrize( + "fn", + ( + nx.bull_graph, + nx.chvatal_graph, + nx.cubical_graph, + nx.diamond_graph, + nx.house_graph, + nx.house_x_graph, + nx.icosahedral_graph, + nx.krackhardt_kite_graph, + nx.octahedral_graph, + nx.petersen_graph, + nx.truncated_cube_graph, + nx.tutte_graph, + ), +) +@pytest.mark.parametrize( + "create_using", (nx.DiGraph, nx.MultiDiGraph, nx.DiGraph([(0, 1)])) +) +def tests_raises_with_directed_create_using(fn, create_using): + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"): + fn(create_using=create_using) + # All these functions have `create_using` as the first positional argument too + with pytest.raises(nx.NetworkXError, match="Directed Graph not supported"): + fn(create_using) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py new file mode 100644 index 0000000000000000000000000000000000000000..b554bfd7017658c9e3ac801c4504c9702d1e03d9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_spectral_graph_forge.py @@ -0,0 +1,49 @@ +import pytest + +pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +from networkx import is_isomorphic +from networkx.exception import NetworkXError +from networkx.generators import karate_club_graph +from networkx.generators.spectral_graph_forge import spectral_graph_forge +from networkx.utils import nodes_equal + + +def test_spectral_graph_forge(): + G = karate_club_graph() + + seed = 54321 + + # common cases, just checking node number preserving and difference + # between identity and modularity cases + H = spectral_graph_forge(G, 0.1, transformation="identity", seed=seed) + assert nodes_equal(G, H) + + I = spectral_graph_forge(G, 0.1, transformation="identity", seed=seed) + assert nodes_equal(G, H) + assert is_isomorphic(I, H) + + I = spectral_graph_forge(G, 0.1, transformation="modularity", seed=seed) + assert nodes_equal(G, I) + + assert not is_isomorphic(I, H) + + # with all the eigenvectors, output graph is identical to the input one + H = spectral_graph_forge(G, 1, transformation="modularity", seed=seed) + assert nodes_equal(G, H) + assert is_isomorphic(G, H) + + # invalid alpha input value, it is silently truncated in [0,1] + H = spectral_graph_forge(G, -1, transformation="identity", seed=seed) + assert nodes_equal(G, H) + + H = spectral_graph_forge(G, 10, transformation="identity", seed=seed) + assert nodes_equal(G, H) + assert is_isomorphic(G, H) + + # invalid transformation mode, checking the error raising + pytest.raises( + NetworkXError, spectral_graph_forge, G, 0.1, transformation="unknown", seed=seed + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py new file mode 100644 index 0000000000000000000000000000000000000000..0404d9d8454b36b546152c1428790441c6952fa2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_stochastic.py @@ -0,0 +1,72 @@ +"""Unit tests for the :mod:`networkx.generators.stochastic` module.""" + +import pytest + +import networkx as nx + + +class TestStochasticGraph: + """Unit tests for the :func:`~networkx.stochastic_graph` function.""" + + def test_default_weights(self): + G = nx.DiGraph() + G.add_edge(0, 1) + G.add_edge(0, 2) + S = nx.stochastic_graph(G) + assert nx.is_isomorphic(G, S) + assert sorted(S.edges(data=True)) == [ + (0, 1, {"weight": 0.5}), + (0, 2, {"weight": 0.5}), + ] + + def test_in_place(self): + """Tests for an in-place reweighting of the edges of the graph.""" + G = nx.DiGraph() + G.add_edge(0, 1, weight=1) + G.add_edge(0, 2, weight=1) + nx.stochastic_graph(G, copy=False) + assert sorted(G.edges(data=True)) == [ + (0, 1, {"weight": 0.5}), + (0, 2, {"weight": 0.5}), + ] + + def test_arbitrary_weights(self): + G = nx.DiGraph() + G.add_edge(0, 1, weight=1) + G.add_edge(0, 2, weight=1) + S = nx.stochastic_graph(G) + assert sorted(S.edges(data=True)) == [ + (0, 1, {"weight": 0.5}), + (0, 2, {"weight": 0.5}), + ] + + def test_multidigraph(self): + G = nx.MultiDiGraph() + G.add_edges_from([(0, 1), (0, 1), (0, 2), (0, 2)]) + S = nx.stochastic_graph(G) + d = {"weight": 0.25} + assert sorted(S.edges(data=True)) == [ + (0, 1, d), + (0, 1, d), + (0, 2, d), + (0, 2, d), + ] + + def test_zero_weights(self): + """Smoke test: ensure ZeroDivisionError is not raised.""" + G = nx.DiGraph() + G.add_edge(0, 1, weight=0) + G.add_edge(0, 2, weight=0) + S = nx.stochastic_graph(G) + assert sorted(S.edges(data=True)) == [ + (0, 1, {"weight": 0}), + (0, 2, {"weight": 0}), + ] + + def test_graph_disallowed(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.stochastic_graph(nx.Graph()) + + def test_multigraph_disallowed(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.stochastic_graph(nx.MultiGraph()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py new file mode 100644 index 0000000000000000000000000000000000000000..7c3560aa81890d0dc308219d7f0983d3950f9fd5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_sudoku.py @@ -0,0 +1,92 @@ +"""Unit tests for the :mod:`networkx.generators.sudoku_graph` module.""" + +import pytest + +import networkx as nx + + +def test_sudoku_negative(): + """Raise an error when generating a Sudoku graph of order -1.""" + pytest.raises(nx.NetworkXError, nx.sudoku_graph, n=-1) + + +@pytest.mark.parametrize("n", [0, 1, 2, 3, 4]) +def test_sudoku_generator(n): + """Generate Sudoku graphs of various sizes and verify their properties.""" + G = nx.sudoku_graph(n) + expected_nodes = n**4 + expected_degree = (n - 1) * (3 * n + 1) + expected_edges = expected_nodes * expected_degree // 2 + assert not G.is_directed() + assert not G.is_multigraph() + assert G.number_of_nodes() == expected_nodes + assert G.number_of_edges() == expected_edges + assert all(d == expected_degree for _, d in G.degree) + + if n == 2: + assert sorted(G.neighbors(6)) == [2, 3, 4, 5, 7, 10, 14] + elif n == 3: + assert sorted(G.neighbors(42)) == [ + 6, + 15, + 24, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 43, + 44, + 51, + 52, + 53, + 60, + 69, + 78, + ] + elif n == 4: + assert sorted(G.neighbors(0)) == [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 32, + 33, + 34, + 35, + 48, + 49, + 50, + 51, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + ] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0cc90a53589a46d0444be6df7c31a1f5beec06 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_time_series.py @@ -0,0 +1,64 @@ +"""Unit tests for the :mod:`networkx.generators.time_series` module.""" + +import itertools + +import networkx as nx + + +def test_visibility_graph__empty_series__empty_graph(): + null_graph = nx.visibility_graph([]) # move along nothing to see here + assert nx.is_empty(null_graph) + + +def test_visibility_graph__single_value_ts__single_node_graph(): + node_graph = nx.visibility_graph([10]) # So Lonely + assert node_graph.number_of_nodes() == 1 + assert node_graph.number_of_edges() == 0 + + +def test_visibility_graph__two_values_ts__single_edge_graph(): + edge_graph = nx.visibility_graph([10, 20]) # Two of Us + assert list(edge_graph.edges) == [(0, 1)] + + +def test_visibility_graph__convex_series__complete_graph(): + series = [i**2 for i in range(10)] # no obstructions + expected_series_length = len(series) + + actual_graph = nx.visibility_graph(series) + + assert actual_graph.number_of_nodes() == expected_series_length + assert actual_graph.number_of_edges() == 45 + assert nx.is_isomorphic(actual_graph, nx.complete_graph(expected_series_length)) + + +def test_visibility_graph__concave_series__path_graph(): + series = [-(i**2) for i in range(10)] # Slip Slidin' Away + expected_node_count = len(series) + + actual_graph = nx.visibility_graph(series) + + assert actual_graph.number_of_nodes() == expected_node_count + assert actual_graph.number_of_edges() == expected_node_count - 1 + assert nx.is_isomorphic(actual_graph, nx.path_graph(expected_node_count)) + + +def test_visibility_graph__flat_series__path_graph(): + series = [0] * 10 # living in 1D flatland + expected_node_count = len(series) + + actual_graph = nx.visibility_graph(series) + + assert actual_graph.number_of_nodes() == expected_node_count + assert actual_graph.number_of_edges() == expected_node_count - 1 + assert nx.is_isomorphic(actual_graph, nx.path_graph(expected_node_count)) + + +def test_visibility_graph_cyclic_series(): + series = list(itertools.islice(itertools.cycle((2, 1, 3)), 17)) # It's so bumpy! + expected_node_count = len(series) + + actual_graph = nx.visibility_graph(series) + + assert actual_graph.number_of_nodes() == expected_node_count + assert actual_graph.number_of_edges() == 25 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py new file mode 100644 index 0000000000000000000000000000000000000000..7932436bf7ad6bb5ab5124f6ff59b7523358354d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_trees.py @@ -0,0 +1,195 @@ +import random + +import pytest + +import networkx as nx +from networkx.utils import arbitrary_element, graphs_equal + + +@pytest.mark.parametrize("prefix_tree_fn", (nx.prefix_tree, nx.prefix_tree_recursive)) +def test_basic_prefix_tree(prefix_tree_fn): + # This example is from the Wikipedia article "Trie" + # . + strings = ["a", "to", "tea", "ted", "ten", "i", "in", "inn"] + T = prefix_tree_fn(strings) + root, NIL = 0, -1 + + def source_label(v): + return T.nodes[v]["source"] + + # First, we check that the tree has the expected + # structure. Recall that each node that corresponds to one of + # the input strings has an edge to the NIL node. + # + # Consider the three children at level 1 in the trie. + a, i, t = sorted(T[root], key=source_label) + # Check the 'a' branch. + assert len(T[a]) == 1 + nil = arbitrary_element(T[a]) + assert len(T[nil]) == 0 + # Check the 'i' branch. + assert len(T[i]) == 2 + nil, in_ = sorted(T[i], key=source_label) + assert len(T[nil]) == 0 + assert len(T[in_]) == 2 + nil, inn = sorted(T[in_], key=source_label) + assert len(T[nil]) == 0 + assert len(T[inn]) == 1 + nil = arbitrary_element(T[inn]) + assert len(T[nil]) == 0 + # Check the 't' branch. + te, to = sorted(T[t], key=source_label) + assert len(T[to]) == 1 + nil = arbitrary_element(T[to]) + assert len(T[nil]) == 0 + tea, ted, ten = sorted(T[te], key=source_label) + assert len(T[tea]) == 1 + assert len(T[ted]) == 1 + assert len(T[ten]) == 1 + nil = arbitrary_element(T[tea]) + assert len(T[nil]) == 0 + nil = arbitrary_element(T[ted]) + assert len(T[nil]) == 0 + nil = arbitrary_element(T[ten]) + assert len(T[nil]) == 0 + + # Next, we check that the "sources" of each of the nodes is the + # rightmost letter in the string corresponding to the path to + # that node. + assert source_label(root) is None + assert source_label(a) == "a" + assert source_label(i) == "i" + assert source_label(t) == "t" + assert source_label(in_) == "n" + assert source_label(inn) == "n" + assert source_label(to) == "o" + assert source_label(te) == "e" + assert source_label(tea) == "a" + assert source_label(ted) == "d" + assert source_label(ten) == "n" + assert source_label(NIL) == "NIL" + + +@pytest.mark.parametrize( + "strings", + ( + ["a", "to", "tea", "ted", "ten", "i", "in", "inn"], + ["ab", "abs", "ad"], + ["ab", "abs", "ad", ""], + ["distant", "disparaging", "distant", "diamond", "ruby"], + ), +) +def test_implementations_consistent(strings): + """Ensure results are consistent between prefix_tree implementations.""" + assert graphs_equal(nx.prefix_tree(strings), nx.prefix_tree_recursive(strings)) + + +def test_random_labeled_rooted_tree(): + for i in range(1, 10): + t1 = nx.random_labeled_rooted_tree(i, seed=42) + t2 = nx.random_labeled_rooted_tree(i, seed=42) + assert nx.utils.misc.graphs_equal(t1, t2) + assert nx.is_tree(t1) + assert "root" in t1.graph + assert "roots" not in t1.graph + + +def test_random_labeled_tree_n_zero(): + """Tests if n = 0 then the NetworkXPointlessConcept exception is raised.""" + with pytest.raises(nx.NetworkXPointlessConcept): + T = nx.random_labeled_tree(0, seed=1234) + with pytest.raises(nx.NetworkXPointlessConcept): + T = nx.random_labeled_rooted_tree(0, seed=1234) + + +def test_random_labeled_rooted_forest(): + for i in range(1, 10): + t1 = nx.random_labeled_rooted_forest(i, seed=42) + t2 = nx.random_labeled_rooted_forest(i, seed=42) + assert nx.utils.misc.graphs_equal(t1, t2) + for c in nx.connected_components(t1): + assert nx.is_tree(t1.subgraph(c)) + assert "root" not in t1.graph + assert "roots" in t1.graph + + +def test_random_labeled_rooted_forest_n_zero(): + """Tests generation of empty labeled forests.""" + F = nx.random_labeled_rooted_forest(0, seed=1234) + assert len(F) == 0 + assert len(F.graph["roots"]) == 0 + + +def test_random_unlabeled_rooted_tree(): + for i in range(1, 10): + t1 = nx.random_unlabeled_rooted_tree(i, seed=42) + t2 = nx.random_unlabeled_rooted_tree(i, seed=42) + assert nx.utils.misc.graphs_equal(t1, t2) + assert nx.is_tree(t1) + assert "root" in t1.graph + assert "roots" not in t1.graph + t = nx.random_unlabeled_rooted_tree(15, number_of_trees=10, seed=43) + random.seed(43) + s = nx.random_unlabeled_rooted_tree(15, number_of_trees=10, seed=random) + for i in range(10): + assert nx.utils.misc.graphs_equal(t[i], s[i]) + assert nx.is_tree(t[i]) + assert "root" in t[i].graph + assert "roots" not in t[i].graph + + +def test_random_unlabeled_tree_n_zero(): + """Tests if n = 0 then the NetworkXPointlessConcept exception is raised.""" + with pytest.raises(nx.NetworkXPointlessConcept): + T = nx.random_unlabeled_tree(0, seed=1234) + with pytest.raises(nx.NetworkXPointlessConcept): + T = nx.random_unlabeled_rooted_tree(0, seed=1234) + + +def test_random_unlabeled_rooted_forest(): + with pytest.raises(ValueError): + nx.random_unlabeled_rooted_forest(10, q=0, seed=42) + for i in range(1, 10): + for q in range(1, i + 1): + t1 = nx.random_unlabeled_rooted_forest(i, q=q, seed=42) + t2 = nx.random_unlabeled_rooted_forest(i, q=q, seed=42) + assert nx.utils.misc.graphs_equal(t1, t2) + for c in nx.connected_components(t1): + assert nx.is_tree(t1.subgraph(c)) + assert len(c) <= q + assert "root" not in t1.graph + assert "roots" in t1.graph + t = nx.random_unlabeled_rooted_forest(15, number_of_forests=10, seed=43) + random.seed(43) + s = nx.random_unlabeled_rooted_forest(15, number_of_forests=10, seed=random) + for i in range(10): + assert nx.utils.misc.graphs_equal(t[i], s[i]) + for c in nx.connected_components(t[i]): + assert nx.is_tree(t[i].subgraph(c)) + assert "root" not in t[i].graph + assert "roots" in t[i].graph + + +def test_random_unlabeled_forest_n_zero(): + """Tests generation of empty unlabeled forests.""" + F = nx.random_unlabeled_rooted_forest(0, seed=1234) + assert len(F) == 0 + assert len(F.graph["roots"]) == 0 + + +def test_random_unlabeled_tree(): + for i in range(1, 10): + t1 = nx.random_unlabeled_tree(i, seed=42) + t2 = nx.random_unlabeled_tree(i, seed=42) + assert nx.utils.misc.graphs_equal(t1, t2) + assert nx.is_tree(t1) + assert "root" not in t1.graph + assert "roots" not in t1.graph + t = nx.random_unlabeled_tree(10, number_of_trees=10, seed=43) + random.seed(43) + s = nx.random_unlabeled_tree(10, number_of_trees=10, seed=random) + for i in range(10): + assert nx.utils.misc.graphs_equal(t[i], s[i]) + assert nx.is_tree(t[i]) + assert "root" not in t[i].graph + assert "roots" not in t[i].graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py new file mode 100644 index 0000000000000000000000000000000000000000..463844be23a07f71375873bbc71e09c402d51118 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/tests/test_triads.py @@ -0,0 +1,15 @@ +"""Unit tests for the :mod:`networkx.generators.triads` module.""" + +import pytest + +from networkx import triad_graph + + +def test_triad_graph(): + G = triad_graph("030T") + assert [tuple(e) for e in ("ab", "ac", "cb")] == sorted(G.edges()) + + +def test_invalid_name(): + with pytest.raises(ValueError): + triad_graph("bogus") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/time_series.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/time_series.py new file mode 100644 index 0000000000000000000000000000000000000000..592d7734a408bf33e58aad20cb117be674558ad2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/time_series.py @@ -0,0 +1,74 @@ +""" +Time Series Graphs +""" + +import itertools + +import networkx as nx + +__all__ = ["visibility_graph"] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def visibility_graph(series): + """ + Return a Visibility Graph of an input Time Series. + + A visibility graph converts a time series into a graph. The constructed graph + uses integer nodes to indicate which event in the series the node represents. + Edges are formed as follows: consider a bar plot of the series and view that + as a side view of a landscape with a node at the top of each bar. An edge + means that the nodes can be connected by a straight "line-of-sight" without + being obscured by any bars between the nodes. + + The resulting graph inherits several properties of the series in its structure. + Thereby, periodic series convert into regular graphs, random series convert + into random graphs, and fractal series convert into scale-free networks [1]_. + + Parameters + ---------- + series : Sequence[Number] + A Time Series sequence (iterable and sliceable) of numeric values + representing times. + + Returns + ------- + NetworkX Graph + The Visibility Graph of the input series + + Examples + -------- + >>> series_list = [range(10), [2, 1, 3, 2, 1, 3, 2, 1, 3, 2, 1, 3]] + >>> for s in series_list: + ... g = nx.visibility_graph(s) + ... print(g) + Graph with 10 nodes and 9 edges + Graph with 12 nodes and 18 edges + + References + ---------- + .. [1] Lacasa, Lucas, Bartolo Luque, Fernando Ballesteros, Jordi Luque, and Juan Carlos Nuno. + "From time series to complex networks: The visibility graph." Proceedings of the + National Academy of Sciences 105, no. 13 (2008): 4972-4975. + https://www.pnas.org/doi/10.1073/pnas.0709247105 + """ + + # Sequential values are always connected + G = nx.path_graph(len(series)) + nx.set_node_attributes(G, dict(enumerate(series)), "value") + + # Check all combinations of nodes n series + for (n1, t1), (n2, t2) in itertools.combinations(enumerate(series), 2): + # check if any value between obstructs line of sight + slope = (t2 - t1) / (n2 - n1) + offset = t2 - slope * n2 + + obstructed = any( + t >= slope * n + offset + for n, t in enumerate(series[n1 + 1 : n2], start=n1 + 1) + ) + + if not obstructed: + G.add_edge(n1, n2) + + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/trees.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/trees.py new file mode 100644 index 0000000000000000000000000000000000000000..a8b24fad27c59a85daa3b31ecb2e3ddd6fe7875f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/trees.py @@ -0,0 +1,1070 @@ +"""Functions for generating trees. + +The functions sampling trees at random in this module come +in two variants: labeled and unlabeled. The labeled variants +sample from every possible tree with the given number of nodes +uniformly at random. The unlabeled variants sample from every +possible *isomorphism class* of trees with the given number +of nodes uniformly at random. + +To understand the difference, consider the following example. +There are two isomorphism classes of trees with four nodes. +One is that of the path graph, the other is that of the +star graph. The unlabeled variant will return a line graph or +a star graph with probability 1/2. + +The labeled variant will return the line graph +with probability 3/4 and the star graph with probability 1/4, +because there are more labeled variants of the line graph +than of the star graph. More precisely, the line graph has +an automorphism group of order 2, whereas the star graph has +an automorphism group of order 6, so the line graph has three +times as many labeled variants as the star graph, and thus +three more chances to be drawn. + +Additionally, some functions in this module can sample rooted +trees and forests uniformly at random. A rooted tree is a tree +with a designated root node. A rooted forest is a disjoint union +of rooted trees. +""" + +from collections import Counter, defaultdict +from math import comb, factorial + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "prefix_tree", + "prefix_tree_recursive", + "random_labeled_tree", + "random_labeled_rooted_tree", + "random_labeled_rooted_forest", + "random_unlabeled_tree", + "random_unlabeled_rooted_tree", + "random_unlabeled_rooted_forest", +] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def prefix_tree(paths): + """Creates a directed prefix tree from a list of paths. + + Usually the paths are described as strings or lists of integers. + + A "prefix tree" represents the prefix structure of the strings. + Each node represents a prefix of some string. The root represents + the empty prefix with children for the single letter prefixes which + in turn have children for each double letter prefix starting with + the single letter corresponding to the parent node, and so on. + + More generally the prefixes do not need to be strings. A prefix refers + to the start of a sequence. The root has children for each one element + prefix and they have children for each two element prefix that starts + with the one element sequence of the parent, and so on. + + Note that this implementation uses integer nodes with an attribute. + Each node has an attribute "source" whose value is the original element + of the path to which this node corresponds. For example, suppose `paths` + consists of one path: "can". Then the nodes `[1, 2, 3]` which represent + this path have "source" values "c", "a" and "n". + + All the descendants of a node have a common prefix in the sequence/path + associated with that node. From the returned tree, the prefix for each + node can be constructed by traversing the tree up to the root and + accumulating the "source" values along the way. + + The root node is always `0` and has "source" attribute `None`. + The root is the only node with in-degree zero. + The nil node is always `-1` and has "source" attribute `"NIL"`. + The nil node is the only node with out-degree zero. + + + Parameters + ---------- + paths: iterable of paths + An iterable of paths which are themselves sequences. + Matching prefixes among these sequences are identified with + nodes of the prefix tree. One leaf of the tree is associated + with each path. (Identical paths are associated with the same + leaf of the tree.) + + + Returns + ------- + tree: DiGraph + A directed graph representing an arborescence consisting of the + prefix tree generated by `paths`. Nodes are directed "downward", + from parent to child. A special "synthetic" root node is added + to be the parent of the first node in each path. A special + "synthetic" leaf node, the "nil" node `-1`, is added to be the child + of all nodes representing the last element in a path. (The + addition of this nil node technically makes this not an + arborescence but a directed acyclic graph; removing the nil node + makes it an arborescence.) + + + Notes + ----- + The prefix tree is also known as a *trie*. + + + Examples + -------- + Create a prefix tree from a list of strings with common prefixes:: + + >>> paths = ["ab", "abs", "ad"] + >>> T = nx.prefix_tree(paths) + >>> list(T.edges) + [(0, 1), (1, 2), (1, 4), (2, -1), (2, 3), (3, -1), (4, -1)] + + The leaf nodes can be obtained as predecessors of the nil node:: + + >>> root, NIL = 0, -1 + >>> list(T.predecessors(NIL)) + [2, 3, 4] + + To recover the original paths that generated the prefix tree, + traverse up the tree from the node `-1` to the node `0`:: + + >>> recovered = [] + >>> for v in T.predecessors(NIL): + ... prefix = "" + ... while v != root: + ... prefix = str(T.nodes[v]["source"]) + prefix + ... v = next(T.predecessors(v)) # only one predecessor + ... recovered.append(prefix) + >>> sorted(recovered) + ['ab', 'abs', 'ad'] + """ + + def get_children(parent, paths): + children = defaultdict(list) + # Populate dictionary with key(s) as the child/children of the root and + # value(s) as the remaining paths of the corresponding child/children + for path in paths: + # If path is empty, we add an edge to the NIL node. + if not path: + tree.add_edge(parent, NIL) + continue + child, *rest = path + # `child` may exist as the head of more than one path in `paths`. + children[child].append(rest) + return children + + # Initialize the prefix tree with a root node and a nil node. + tree = nx.DiGraph() + root = 0 + tree.add_node(root, source=None) + NIL = -1 + tree.add_node(NIL, source="NIL") + children = get_children(root, paths) + stack = [(root, iter(children.items()))] + while stack: + parent, remaining_children = stack[-1] + try: + child, remaining_paths = next(remaining_children) + # Pop item off stack if there are no remaining children + except StopIteration: + stack.pop() + continue + # We relabel each child with an unused name. + new_name = len(tree) - 1 + # The "source" node attribute stores the original node name. + tree.add_node(new_name, source=child) + tree.add_edge(parent, new_name) + children = get_children(new_name, remaining_paths) + stack.append((new_name, iter(children.items()))) + + return tree + + +@nx._dispatchable(graphs=None, returns_graph=True) +def prefix_tree_recursive(paths): + """Recursively creates a directed prefix tree from a list of paths. + + The original recursive version of prefix_tree for comparison. It is + the same algorithm but the recursion is unrolled onto a stack. + + Usually the paths are described as strings or lists of integers. + + A "prefix tree" represents the prefix structure of the strings. + Each node represents a prefix of some string. The root represents + the empty prefix with children for the single letter prefixes which + in turn have children for each double letter prefix starting with + the single letter corresponding to the parent node, and so on. + + More generally the prefixes do not need to be strings. A prefix refers + to the start of a sequence. The root has children for each one element + prefix and they have children for each two element prefix that starts + with the one element sequence of the parent, and so on. + + Note that this implementation uses integer nodes with an attribute. + Each node has an attribute "source" whose value is the original element + of the path to which this node corresponds. For example, suppose `paths` + consists of one path: "can". Then the nodes `[1, 2, 3]` which represent + this path have "source" values "c", "a" and "n". + + All the descendants of a node have a common prefix in the sequence/path + associated with that node. From the returned tree, ehe prefix for each + node can be constructed by traversing the tree up to the root and + accumulating the "source" values along the way. + + The root node is always `0` and has "source" attribute `None`. + The root is the only node with in-degree zero. + The nil node is always `-1` and has "source" attribute `"NIL"`. + The nil node is the only node with out-degree zero. + + + Parameters + ---------- + paths: iterable of paths + An iterable of paths which are themselves sequences. + Matching prefixes among these sequences are identified with + nodes of the prefix tree. One leaf of the tree is associated + with each path. (Identical paths are associated with the same + leaf of the tree.) + + + Returns + ------- + tree: DiGraph + A directed graph representing an arborescence consisting of the + prefix tree generated by `paths`. Nodes are directed "downward", + from parent to child. A special "synthetic" root node is added + to be the parent of the first node in each path. A special + "synthetic" leaf node, the "nil" node `-1`, is added to be the child + of all nodes representing the last element in a path. (The + addition of this nil node technically makes this not an + arborescence but a directed acyclic graph; removing the nil node + makes it an arborescence.) + + + Notes + ----- + The prefix tree is also known as a *trie*. + + + Examples + -------- + Create a prefix tree from a list of strings with common prefixes:: + + >>> paths = ["ab", "abs", "ad"] + >>> T = nx.prefix_tree(paths) + >>> list(T.edges) + [(0, 1), (1, 2), (1, 4), (2, -1), (2, 3), (3, -1), (4, -1)] + + The leaf nodes can be obtained as predecessors of the nil node. + + >>> root, NIL = 0, -1 + >>> list(T.predecessors(NIL)) + [2, 3, 4] + + To recover the original paths that generated the prefix tree, + traverse up the tree from the node `-1` to the node `0`:: + + >>> recovered = [] + >>> for v in T.predecessors(NIL): + ... prefix = "" + ... while v != root: + ... prefix = str(T.nodes[v]["source"]) + prefix + ... v = next(T.predecessors(v)) # only one predecessor + ... recovered.append(prefix) + >>> sorted(recovered) + ['ab', 'abs', 'ad'] + """ + + def _helper(paths, root, tree): + """Recursively create a trie from the given list of paths. + + `paths` is a list of paths, each of which is itself a list of + nodes, relative to the given `root` (but not including it). This + list of paths will be interpreted as a tree-like structure, in + which two paths that share a prefix represent two branches of + the tree with the same initial segment. + + `root` is the parent of the node at index 0 in each path. + + `tree` is the "accumulator", the :class:`networkx.DiGraph` + representing the branching to which the new nodes and edges will + be added. + + """ + # For each path, remove the first node and make it a child of root. + # Any remaining paths then get processed recursively. + children = defaultdict(list) + for path in paths: + # If path is empty, we add an edge to the NIL node. + if not path: + tree.add_edge(root, NIL) + continue + child, *rest = path + # `child` may exist as the head of more than one path in `paths`. + children[child].append(rest) + # Add a node for each child, connect root, recurse to remaining paths + for child, remaining_paths in children.items(): + # We relabel each child with an unused name. + new_name = len(tree) - 1 + # The "source" node attribute stores the original node name. + tree.add_node(new_name, source=child) + tree.add_edge(root, new_name) + _helper(remaining_paths, new_name, tree) + + # Initialize the prefix tree with a root node and a nil node. + tree = nx.DiGraph() + root = 0 + tree.add_node(root, source=None) + NIL = -1 + tree.add_node(NIL, source="NIL") + # Populate the tree. + _helper(paths, root, tree) + return tree + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_labeled_tree(n, *, seed=None): + """Returns a labeled tree on `n` nodes chosen uniformly at random. + + Generating uniformly distributed random Prüfer sequences and + converting them into the corresponding trees is a straightforward + method of generating uniformly distributed random labeled trees. + This function implements this method. + + Parameters + ---------- + n : int + The number of nodes, greater than zero. + seed : random_state + Indicator of random number generation state. + See :ref:`Randomness` + + Returns + ------- + :class:`networkx.Graph` + A `networkx.Graph` with nodes in the set {0, …, *n* - 1}. + + Raises + ------ + NetworkXPointlessConcept + If `n` is zero (because the null graph is not a tree). + + Examples + -------- + >>> G = nx.random_labeled_tree(5, seed=42) + >>> nx.is_tree(G) + True + >>> G.edges + EdgeView([(0, 1), (0, 3), (0, 2), (2, 4)]) + + A tree with *arbitrarily directed* edges can be created by assigning + generated edges to a ``DiGraph``: + + >>> DG = nx.DiGraph() + >>> DG.add_edges_from(G.edges) + >>> nx.is_tree(DG) + True + >>> DG.edges + OutEdgeView([(0, 1), (0, 3), (0, 2), (2, 4)]) + """ + # Cannot create a Prüfer sequence unless `n` is at least two. + if n == 0: + raise nx.NetworkXPointlessConcept("the null graph is not a tree") + if n == 1: + return nx.empty_graph(1) + return nx.from_prufer_sequence([seed.choice(range(n)) for i in range(n - 2)]) + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_labeled_rooted_tree(n, *, seed=None): + """Returns a labeled rooted tree with `n` nodes. + + The returned tree is chosen uniformly at random from all labeled rooted trees. + + Parameters + ---------- + n : int + The number of nodes + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + :class:`networkx.Graph` + A `networkx.Graph` with integer nodes 0 <= node <= `n` - 1. + The root of the tree is selected uniformly from the nodes. + The "root" graph attribute identifies the root of the tree. + + Notes + ----- + This function returns the result of :func:`random_labeled_tree` + with a randomly selected root. + + Raises + ------ + NetworkXPointlessConcept + If `n` is zero (because the null graph is not a tree). + """ + t = random_labeled_tree(n, seed=seed) + t.graph["root"] = seed.randint(0, n - 1) + return t + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_labeled_rooted_forest(n, *, seed=None): + """Returns a labeled rooted forest with `n` nodes. + + The returned forest is chosen uniformly at random using a + generalization of Prüfer sequences [1]_ in the form described in [2]_. + + Parameters + ---------- + n : int + The number of nodes. + seed : random_state + See :ref:`Randomness`. + + Returns + ------- + :class:`networkx.Graph` + A `networkx.Graph` with integer nodes 0 <= node <= `n` - 1. + The "roots" graph attribute is a set of integers containing the roots. + + References + ---------- + .. [1] Knuth, Donald E. "Another Enumeration of Trees." + Canadian Journal of Mathematics, 20 (1968): 1077-1086. + https://doi.org/10.4153/CJM-1968-104-8 + .. [2] Rubey, Martin. "Counting Spanning Trees". Diplomarbeit + zur Erlangung des akademischen Grades Magister der + Naturwissenschaften an der Formal- und Naturwissenschaftlichen + Fakultät der Universität Wien. Wien, May 2000. + """ + + # Select the number of roots by iterating over the cumulative count of trees + # with at most k roots + def _select_k(n, seed): + r = seed.randint(0, (n + 1) ** (n - 1) - 1) + cum_sum = 0 + for k in range(1, n): + cum_sum += (factorial(n - 1) * n ** (n - k)) // ( + factorial(k - 1) * factorial(n - k) + ) + if r < cum_sum: + return k + + return n + + F = nx.empty_graph(n) + if n == 0: + F.graph["roots"] = {} + return F + # Select the number of roots k + k = _select_k(n, seed) + if k == n: + F.graph["roots"] = set(range(n)) + return F # Nothing to do + # Select the roots + roots = seed.sample(range(n), k) + # Nonroots + p = set(range(n)).difference(roots) + # Coding sequence + N = [seed.randint(0, n - 1) for i in range(n - k - 1)] + # Multiset of elements in N also in p + degree = Counter([x for x in N if x in p]) + # Iterator over the elements of p with degree zero + iterator = iter(x for x in p if degree[x] == 0) + u = last = next(iterator) + # This loop is identical to that for Prüfer sequences, + # except that we can draw nodes only from p + for v in N: + F.add_edge(u, v) + degree[v] -= 1 + if v < last and degree[v] == 0: + u = v + else: + last = u = next(iterator) + + F.add_edge(u, roots[0]) + F.graph["roots"] = set(roots) + return F + + +# The following functions support generation of unlabeled trees and forests. + + +def _to_nx(edges, n_nodes, root=None, roots=None): + """ + Converts the (edges, n_nodes) input to a :class:`networkx.Graph`. + The (edges, n_nodes) input is a list of even length, where each pair + of consecutive integers represents an edge, and an integer `n_nodes`. + Integers in the list are elements of `range(n_nodes)`. + + Parameters + ---------- + edges : list of ints + The flattened list of edges of the graph. + n_nodes : int + The number of nodes of the graph. + root: int (default=None) + If not None, the "root" attribute of the graph will be set to this value. + roots: collection of ints (default=None) + If not None, he "roots" attribute of the graph will be set to this value. + + Returns + ------- + :class:`networkx.Graph` + The graph with `n_nodes` nodes and edges given by `edges`. + """ + G = nx.empty_graph(n_nodes) + G.add_edges_from(edges) + if root is not None: + G.graph["root"] = root + if roots is not None: + G.graph["roots"] = roots + return G + + +def _num_rooted_trees(n, cache_trees): + """Returns the number of unlabeled rooted trees with `n` nodes. + + See also https://oeis.org/A000081. + + Parameters + ---------- + n : int + The number of nodes + cache_trees : list of ints + The $i$-th element is the number of unlabeled rooted trees with $i$ nodes, + which is used as a cache (and is extended to length $n+1$ if needed) + + Returns + ------- + int + The number of unlabeled rooted trees with `n` nodes. + """ + for n_i in range(len(cache_trees), n + 1): + cache_trees.append( + sum( + [ + d * cache_trees[n_i - j * d] * cache_trees[d] + for d in range(1, n_i) + for j in range(1, (n_i - 1) // d + 1) + ] + ) + // (n_i - 1) + ) + return cache_trees[n] + + +def _select_jd_trees(n, cache_trees, seed): + """Returns a pair $(j,d)$ with a specific probability + + Given $n$, returns a pair of positive integers $(j,d)$ with the probability + specified in formula (5) of Chapter 29 of [1]_. + + Parameters + ---------- + n : int + The number of nodes + cache_trees : list of ints + Cache for :func:`_num_rooted_trees`. + seed : random_state + See :ref:`Randomness`. + + Returns + ------- + (int, int) + A pair of positive integers $(j,d)$ satisfying formula (5) of + Chapter 29 of [1]_. + + References + ---------- + .. [1] Nijenhuis, Albert, and Wilf, Herbert S. + "Combinatorial algorithms: for computers and calculators." + Academic Press, 1978. + https://doi.org/10.1016/C2013-0-11243-3 + """ + p = seed.randint(0, _num_rooted_trees(n, cache_trees) * (n - 1) - 1) + cumsum = 0 + for d in range(n - 1, 0, -1): + for j in range(1, (n - 1) // d + 1): + cumsum += ( + d + * _num_rooted_trees(n - j * d, cache_trees) + * _num_rooted_trees(d, cache_trees) + ) + if p < cumsum: + return (j, d) + + +def _random_unlabeled_rooted_tree(n, cache_trees, seed): + """Returns an unlabeled rooted tree with `n` nodes. + + Returns an unlabeled rooted tree with `n` nodes chosen uniformly + at random using the "RANRUT" algorithm from [1]_. + The tree is returned in the form: (list_of_edges, number_of_nodes) + + Parameters + ---------- + n : int + The number of nodes, greater than zero. + cache_trees : list ints + Cache for :func:`_num_rooted_trees`. + seed : random_state + See :ref:`Randomness`. + + Returns + ------- + (list_of_edges, number_of_nodes) : list, int + A random unlabeled rooted tree with `n` nodes as a 2-tuple + ``(list_of_edges, number_of_nodes)``. + The root is node 0. + + References + ---------- + .. [1] Nijenhuis, Albert, and Wilf, Herbert S. + "Combinatorial algorithms: for computers and calculators." + Academic Press, 1978. + https://doi.org/10.1016/C2013-0-11243-3 + """ + if n == 1: + edges, n_nodes = [], 1 + return edges, n_nodes + if n == 2: + edges, n_nodes = [(0, 1)], 2 + return edges, n_nodes + + j, d = _select_jd_trees(n, cache_trees, seed) + t1, t1_nodes = _random_unlabeled_rooted_tree(n - j * d, cache_trees, seed) + t2, t2_nodes = _random_unlabeled_rooted_tree(d, cache_trees, seed) + t12 = [(0, t2_nodes * i + t1_nodes) for i in range(j)] + t1.extend(t12) + for _ in range(j): + t1.extend((n1 + t1_nodes, n2 + t1_nodes) for n1, n2 in t2) + t1_nodes += t2_nodes + + return t1, t1_nodes + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_unlabeled_rooted_tree(n, *, number_of_trees=None, seed=None): + """Returns a number of unlabeled rooted trees uniformly at random + + Returns one or more (depending on `number_of_trees`) + unlabeled rooted trees with `n` nodes drawn uniformly + at random. + + Parameters + ---------- + n : int + The number of nodes + number_of_trees : int or None (default) + If not None, this number of trees is generated and returned. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + :class:`networkx.Graph` or list of :class:`networkx.Graph` + A single `networkx.Graph` (or a list thereof, if `number_of_trees` + is specified) with nodes in the set {0, …, *n* - 1}. + The "root" graph attribute identifies the root of the tree. + + Notes + ----- + The trees are generated using the "RANRUT" algorithm from [1]_. + The algorithm needs to compute some counting functions + that are relatively expensive: in case several trees are needed, + it is advisable to use the `number_of_trees` optional argument + to reuse the counting functions. + + Raises + ------ + NetworkXPointlessConcept + If `n` is zero (because the null graph is not a tree). + + References + ---------- + .. [1] Nijenhuis, Albert, and Wilf, Herbert S. + "Combinatorial algorithms: for computers and calculators." + Academic Press, 1978. + https://doi.org/10.1016/C2013-0-11243-3 + """ + if n == 0: + raise nx.NetworkXPointlessConcept("the null graph is not a tree") + cache_trees = [0, 1] # initial cache of number of rooted trees + if number_of_trees is None: + return _to_nx(*_random_unlabeled_rooted_tree(n, cache_trees, seed), root=0) + return [ + _to_nx(*_random_unlabeled_rooted_tree(n, cache_trees, seed), root=0) + for i in range(number_of_trees) + ] + + +def _num_rooted_forests(n, q, cache_forests): + """Returns the number of unlabeled rooted forests with `n` nodes, and with + no more than `q` nodes per tree. A recursive formula for this is (2) in + [1]_. This function is implemented using dynamic programming instead of + recursion. + + Parameters + ---------- + n : int + The number of nodes. + q : int + The maximum number of nodes for each tree of the forest. + cache_forests : list of ints + The $i$-th element is the number of unlabeled rooted forests with + $i$ nodes, and with no more than `q` nodes per tree; this is used + as a cache (and is extended to length `n` + 1 if needed). + + Returns + ------- + int + The number of unlabeled rooted forests with `n` nodes with no more than + `q` nodes per tree. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + for n_i in range(len(cache_forests), n + 1): + q_i = min(n_i, q) + cache_forests.append( + sum( + [ + d * cache_forests[n_i - j * d] * cache_forests[d - 1] + for d in range(1, q_i + 1) + for j in range(1, n_i // d + 1) + ] + ) + // n_i + ) + + return cache_forests[n] + + +def _select_jd_forests(n, q, cache_forests, seed): + """Given `n` and `q`, returns a pair of positive integers $(j,d)$ + such that $j\\leq d$, with probability satisfying (F1) of [1]_. + + Parameters + ---------- + n : int + The number of nodes. + q : int + The maximum number of nodes for each tree of the forest. + cache_forests : list of ints + Cache for :func:`_num_rooted_forests`. + seed : random_state + See :ref:`Randomness`. + + Returns + ------- + (int, int) + A pair of positive integers $(j,d)$ + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + p = seed.randint(0, _num_rooted_forests(n, q, cache_forests) * n - 1) + cumsum = 0 + for d in range(q, 0, -1): + for j in range(1, n // d + 1): + cumsum += ( + d + * _num_rooted_forests(n - j * d, q, cache_forests) + * _num_rooted_forests(d - 1, q, cache_forests) + ) + if p < cumsum: + return (j, d) + + +def _random_unlabeled_rooted_forest(n, q, cache_trees, cache_forests, seed): + """Returns an unlabeled rooted forest with `n` nodes, and with no more + than `q` nodes per tree, drawn uniformly at random. It is an implementation + of the algorithm "Forest" of [1]_. + + Parameters + ---------- + n : int + The number of nodes. + q : int + The maximum number of nodes per tree. + cache_trees : + Cache for :func:`_num_rooted_trees`. + cache_forests : + Cache for :func:`_num_rooted_forests`. + seed : random_state + See :ref:`Randomness`. + + Returns + ------- + (edges, n, r) : (list, int, list) + The forest (edges, n) and a list r of root nodes. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + if n == 0: + return ([], 0, []) + + j, d = _select_jd_forests(n, q, cache_forests, seed) + t1, t1_nodes, r1 = _random_unlabeled_rooted_forest( + n - j * d, q, cache_trees, cache_forests, seed + ) + t2, t2_nodes = _random_unlabeled_rooted_tree(d, cache_trees, seed) + for _ in range(j): + r1.append(t1_nodes) + t1.extend((n1 + t1_nodes, n2 + t1_nodes) for n1, n2 in t2) + t1_nodes += t2_nodes + return t1, t1_nodes, r1 + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_unlabeled_rooted_forest(n, *, q=None, number_of_forests=None, seed=None): + """Returns a forest or list of forests selected at random. + + Returns one or more (depending on `number_of_forests`) + unlabeled rooted forests with `n` nodes, and with no more than + `q` nodes per tree, drawn uniformly at random. + The "roots" graph attribute identifies the roots of the forest. + + Parameters + ---------- + n : int + The number of nodes + q : int or None (default) + The maximum number of nodes per tree. + number_of_forests : int or None (default) + If not None, this number of forests is generated and returned. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + :class:`networkx.Graph` or list of :class:`networkx.Graph` + A single `networkx.Graph` (or a list thereof, if `number_of_forests` + is specified) with nodes in the set {0, …, *n* - 1}. + The "roots" graph attribute is a set containing the roots + of the trees in the forest. + + Notes + ----- + This function implements the algorithm "Forest" of [1]_. + The algorithm needs to compute some counting functions + that are relatively expensive: in case several trees are needed, + it is advisable to use the `number_of_forests` optional argument + to reuse the counting functions. + + Raises + ------ + ValueError + If `n` is non-zero but `q` is zero. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + if q is None: + q = n + if q == 0 and n != 0: + raise ValueError("q must be a positive integer if n is positive.") + + cache_trees = [0, 1] # initial cache of number of rooted trees + cache_forests = [1] # initial cache of number of rooted forests + + if number_of_forests is None: + g, nodes, rs = _random_unlabeled_rooted_forest( + n, q, cache_trees, cache_forests, seed + ) + return _to_nx(g, nodes, roots=set(rs)) + + res = [] + for i in range(number_of_forests): + g, nodes, rs = _random_unlabeled_rooted_forest( + n, q, cache_trees, cache_forests, seed + ) + res.append(_to_nx(g, nodes, roots=set(rs))) + return res + + +def _num_trees(n, cache_trees): + """Returns the number of unlabeled trees with `n` nodes. + + See also https://oeis.org/A000055. + + Parameters + ---------- + n : int + The number of nodes. + cache_trees : list of ints + Cache for :func:`_num_rooted_trees`. + + Returns + ------- + int + The number of unlabeled trees with `n` nodes. + """ + r = _num_rooted_trees(n, cache_trees) - sum( + [ + _num_rooted_trees(j, cache_trees) * _num_rooted_trees(n - j, cache_trees) + for j in range(1, n // 2 + 1) + ] + ) + if n % 2 == 0: + r += comb(_num_rooted_trees(n // 2, cache_trees) + 1, 2) + return r + + +def _bicenter(n, cache, seed): + """Returns a bi-centroidal tree on `n` nodes drawn uniformly at random. + + This function implements the algorithm Bicenter of [1]_. + + Parameters + ---------- + n : int + The number of nodes (must be even). + cache : list of ints. + Cache for :func:`_num_rooted_trees`. + seed : random_state + See :ref:`Randomness` + + Returns + ------- + (edges, n) + The tree as a list of edges and number of nodes. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + t, t_nodes = _random_unlabeled_rooted_tree(n // 2, cache, seed) + if seed.randint(0, _num_rooted_trees(n // 2, cache)) == 0: + t2, t2_nodes = t, t_nodes + else: + t2, t2_nodes = _random_unlabeled_rooted_tree(n // 2, cache, seed) + t.extend([(n1 + (n // 2), n2 + (n // 2)) for n1, n2 in t2]) + t.append((0, n // 2)) + return t, t_nodes + t2_nodes + + +def _random_unlabeled_tree(n, cache_trees, cache_forests, seed): + """Returns a tree on `n` nodes drawn uniformly at random. + It implements the Wilf's algorithm "Free" of [1]_. + + Parameters + ---------- + n : int + The number of nodes, greater than zero. + cache_trees : list of ints + Cache for :func:`_num_rooted_trees`. + cache_forests : list of ints + Cache for :func:`_num_rooted_forests`. + seed : random_state + Indicator of random number generation state. + See :ref:`Randomness` + + Returns + ------- + (edges, n) + The tree as a list of edges and number of nodes. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + if n % 2 == 1: + p = 0 + else: + p = comb(_num_rooted_trees(n // 2, cache_trees) + 1, 2) + if seed.randint(0, _num_trees(n, cache_trees) - 1) < p: + return _bicenter(n, cache_trees, seed) + else: + f, n_f, r = _random_unlabeled_rooted_forest( + n - 1, (n - 1) // 2, cache_trees, cache_forests, seed + ) + for i in r: + f.append((i, n_f)) + return f, n_f + 1 + + +@py_random_state("seed") +@nx._dispatchable(graphs=None, returns_graph=True) +def random_unlabeled_tree(n, *, number_of_trees=None, seed=None): + """Returns a tree or list of trees chosen randomly. + + Returns one or more (depending on `number_of_trees`) + unlabeled trees with `n` nodes drawn uniformly at random. + + Parameters + ---------- + n : int + The number of nodes + number_of_trees : int or None (default) + If not None, this number of trees is generated and returned. + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + :class:`networkx.Graph` or list of :class:`networkx.Graph` + A single `networkx.Graph` (or a list thereof, if + `number_of_trees` is specified) with nodes in the set {0, …, *n* - 1}. + + Raises + ------ + NetworkXPointlessConcept + If `n` is zero (because the null graph is not a tree). + + Notes + ----- + This function generates an unlabeled tree uniformly at random using + Wilf's algorithm "Free" of [1]_. The algorithm needs to + compute some counting functions that are relatively expensive: + in case several trees are needed, it is advisable to use the + `number_of_trees` optional argument to reuse the counting + functions. + + References + ---------- + .. [1] Wilf, Herbert S. "The uniform selection of free trees." + Journal of Algorithms 2.2 (1981): 204-207. + https://doi.org/10.1016/0196-6774(81)90021-3 + """ + if n == 0: + raise nx.NetworkXPointlessConcept("the null graph is not a tree") + + cache_trees = [0, 1] # initial cache of number of rooted trees + cache_forests = [1] # initial cache of number of rooted forests + if number_of_trees is None: + return _to_nx(*_random_unlabeled_tree(n, cache_trees, cache_forests, seed)) + else: + return [ + _to_nx(*_random_unlabeled_tree(n, cache_trees, cache_forests, seed)) + for i in range(number_of_trees) + ] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/triads.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/triads.py new file mode 100644 index 0000000000000000000000000000000000000000..09b722dd1bd49dddae16086115d170ec989f8b06 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/generators/triads.py @@ -0,0 +1,94 @@ +# See https://github.com/networkx/networkx/pull/1474 +# Copyright 2011 Reya Group +# Copyright 2011 Alex Levenson +# Copyright 2011 Diederik van Liere +"""Functions that generate the triad graphs, that is, the possible +digraphs on three nodes. + +""" + +import networkx as nx +from networkx.classes import DiGraph + +__all__ = ["triad_graph"] + +#: Dictionary mapping triad name to list of directed edges in the +#: digraph representation of that triad (with nodes 'a', 'b', and 'c'). +TRIAD_EDGES = { + "003": [], + "012": ["ab"], + "102": ["ab", "ba"], + "021D": ["ba", "bc"], + "021U": ["ab", "cb"], + "021C": ["ab", "bc"], + "111D": ["ac", "ca", "bc"], + "111U": ["ac", "ca", "cb"], + "030T": ["ab", "cb", "ac"], + "030C": ["ba", "cb", "ac"], + "201": ["ab", "ba", "ac", "ca"], + "120D": ["bc", "ba", "ac", "ca"], + "120U": ["ab", "cb", "ac", "ca"], + "120C": ["ab", "bc", "ac", "ca"], + "210": ["ab", "bc", "cb", "ac", "ca"], + "300": ["ab", "ba", "bc", "cb", "ac", "ca"], +} + + +@nx._dispatchable(graphs=None, returns_graph=True) +def triad_graph(triad_name): + """Returns the triad graph with the given name. + + Each string in the following tuple is a valid triad name:: + + ( + "003", + "012", + "102", + "021D", + "021U", + "021C", + "111D", + "111U", + "030T", + "030C", + "201", + "120D", + "120U", + "120C", + "210", + "300", + ) + + Each triad name corresponds to one of the possible valid digraph on + three nodes. + + Parameters + ---------- + triad_name : string + The name of a triad, as described above. + + Returns + ------- + :class:`~networkx.DiGraph` + The digraph on three nodes with the given name. The nodes of the + graph are the single-character strings 'a', 'b', and 'c'. + + Raises + ------ + ValueError + If `triad_name` is not the name of a triad. + + See also + -------- + triadic_census + + """ + if triad_name not in TRIAD_EDGES: + raise ValueError( + f'unknown triad name "{triad_name}"; use one of the triad names' + " in the TRIAD_NAMES constant" + ) + G = DiGraph() + G.add_nodes_from("abc") + G.add_edges_from(TRIAD_EDGES[triad_name]) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..119db185a1ae440fd2cdb6c7f531331642313c34 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__init__.py @@ -0,0 +1,13 @@ +from networkx.linalg.attrmatrix import * +from networkx.linalg import attrmatrix +from networkx.linalg.spectrum import * +from networkx.linalg import spectrum +from networkx.linalg.graphmatrix import * +from networkx.linalg import graphmatrix +from networkx.linalg.laplacianmatrix import * +from networkx.linalg import laplacianmatrix +from networkx.linalg.algebraicconnectivity import * +from networkx.linalg.modularitymatrix import * +from networkx.linalg import modularitymatrix +from networkx.linalg.bethehessianmatrix import * +from networkx.linalg import bethehessianmatrix diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..415d07c48e06390d133e6eede83010a5d2f6b242 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/algebraicconnectivity.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/algebraicconnectivity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b70ad86e74ed50925036d1e5e697545441646e1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/algebraicconnectivity.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/attrmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/attrmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0461f17d4d6db76ee209604aa8819ad6e05ac922 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/attrmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/bethehessianmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/bethehessianmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9913969121894e530a5c4fbd185ec6eb874977da Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/bethehessianmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/graphmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/graphmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..949ce362a64bc72b372f2bb94e446806f070186d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/graphmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/laplacianmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/laplacianmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..634db0cadecea324ddc988f5a03a53acd71de527 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/laplacianmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/modularitymatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/modularitymatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1645bc30eafa1d03d0def5509b69c0a11373bf23 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/modularitymatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/spectrum.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/spectrum.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb336f536e7f40aedd9dd5dc2d2cc31ad847270c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/__pycache__/spectrum.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..c7bcf4384802c932d8cda0b579295a4fd3031951 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py @@ -0,0 +1,650 @@ +""" +Algebraic connectivity and Fiedler vectors of undirected graphs. +""" + +import networkx as nx +from networkx.utils import ( + not_implemented_for, + np_random_state, + reverse_cuthill_mckee_ordering, +) + +__all__ = [ + "algebraic_connectivity", + "fiedler_vector", + "spectral_ordering", + "spectral_bisection", +] + + +class _PCGSolver: + """Preconditioned conjugate gradient method. + + To solve Ax = b: + M = A.diagonal() # or some other preconditioner + solver = _PCGSolver(lambda x: A * x, lambda x: M * x) + x = solver.solve(b) + + The inputs A and M are functions which compute + matrix multiplication on the argument. + A - multiply by the matrix A in Ax=b + M - multiply by M, the preconditioner surrogate for A + + Warning: There is no limit on number of iterations. + """ + + def __init__(self, A, M): + self._A = A + self._M = M + + def solve(self, B, tol): + import numpy as np + + # Densifying step - can this be kept sparse? + B = np.asarray(B) + X = np.ndarray(B.shape, order="F") + for j in range(B.shape[1]): + X[:, j] = self._solve(B[:, j], tol) + return X + + def _solve(self, b, tol): + import numpy as np + import scipy as sp + + A = self._A + M = self._M + tol *= sp.linalg.blas.dasum(b) + # Initialize. + x = np.zeros(b.shape) + r = b.copy() + z = M(r) + rz = sp.linalg.blas.ddot(r, z) + p = z.copy() + # Iterate. + while True: + Ap = A(p) + alpha = rz / sp.linalg.blas.ddot(p, Ap) + x = sp.linalg.blas.daxpy(p, x, a=alpha) + r = sp.linalg.blas.daxpy(Ap, r, a=-alpha) + if sp.linalg.blas.dasum(r) < tol: + return x + z = M(r) + beta = sp.linalg.blas.ddot(r, z) + beta, rz = beta / rz, beta + p = sp.linalg.blas.daxpy(p, z, a=beta) + + +class _LUSolver: + """LU factorization. + + To solve Ax = b: + solver = _LUSolver(A) + x = solver.solve(b) + + optional argument `tol` on solve method is ignored but included + to match _PCGsolver API. + """ + + def __init__(self, A): + import scipy as sp + + self._LU = sp.sparse.linalg.splu( + A, + permc_spec="MMD_AT_PLUS_A", + diag_pivot_thresh=0.0, + options={"Equil": True, "SymmetricMode": True}, + ) + + def solve(self, B, tol=None): + import numpy as np + + B = np.asarray(B) + X = np.ndarray(B.shape, order="F") + for j in range(B.shape[1]): + X[:, j] = self._LU.solve(B[:, j]) + return X + + +def _preprocess_graph(G, weight): + """Compute edge weights and eliminate zero-weight edges.""" + if G.is_directed(): + H = nx.MultiGraph() + H.add_nodes_from(G) + H.add_weighted_edges_from( + ((u, v, e.get(weight, 1.0)) for u, v, e in G.edges(data=True) if u != v), + weight=weight, + ) + G = H + if not G.is_multigraph(): + edges = ( + (u, v, abs(e.get(weight, 1.0))) for u, v, e in G.edges(data=True) if u != v + ) + else: + edges = ( + (u, v, sum(abs(e.get(weight, 1.0)) for e in G[u][v].values())) + for u, v in G.edges() + if u != v + ) + H = nx.Graph() + H.add_nodes_from(G) + H.add_weighted_edges_from((u, v, e) for u, v, e in edges if e != 0) + return H + + +def _rcm_estimate(G, nodelist): + """Estimate the Fiedler vector using the reverse Cuthill-McKee ordering.""" + import numpy as np + + G = G.subgraph(nodelist) + order = reverse_cuthill_mckee_ordering(G) + n = len(nodelist) + index = dict(zip(nodelist, range(n))) + x = np.ndarray(n, dtype=float) + for i, u in enumerate(order): + x[index[u]] = i + x -= (n - 1) / 2.0 + return x + + +def _tracemin_fiedler(L, X, normalized, tol, method): + """Compute the Fiedler vector of L using the TraceMIN-Fiedler algorithm. + + The Fiedler vector of a connected undirected graph is the eigenvector + corresponding to the second smallest eigenvalue of the Laplacian matrix + of the graph. This function starts with the Laplacian L, not the Graph. + + Parameters + ---------- + L : Laplacian of a possibly weighted or normalized, but undirected graph + + X : Initial guess for a solution. Usually a matrix of random numbers. + This function allows more than one column in X to identify more than + one eigenvector if desired. + + normalized : bool + Whether the normalized Laplacian matrix is used. + + tol : float + Tolerance of relative residual in eigenvalue computation. + Warning: There is no limit on number of iterations. + + method : string + Should be 'tracemin_pcg' or 'tracemin_lu'. + Otherwise exception is raised. + + Returns + ------- + sigma, X : Two NumPy arrays of floats. + The lowest eigenvalues and corresponding eigenvectors of L. + The size of input X determines the size of these outputs. + As this is for Fiedler vectors, the zero eigenvalue (and + constant eigenvector) are avoided. + """ + import numpy as np + import scipy as sp + + n = X.shape[0] + + if normalized: + # Form the normalized Laplacian matrix and determine the eigenvector of + # its nullspace. + e = np.sqrt(L.diagonal()) + D = sp.sparse.dia_array((1 / e, 0), shape=(n, n)).tocsr() + L = D @ L @ D + e *= 1.0 / np.linalg.norm(e, 2) + + if normalized: + + def project(X): + """Make X orthogonal to the nullspace of L.""" + X = np.asarray(X) + for j in range(X.shape[1]): + X[:, j] -= (X[:, j] @ e) * e + + else: + + def project(X): + """Make X orthogonal to the nullspace of L.""" + X = np.asarray(X) + for j in range(X.shape[1]): + X[:, j] -= X[:, j].sum() / n + + if method == "tracemin_pcg": + D = L.diagonal().astype(float) + solver = _PCGSolver(lambda x: L @ x, lambda x: D * x) + elif method == "tracemin_lu": + # Convert A to CSC to suppress SparseEfficiencyWarning. + A = sp.sparse.csc_array(L, dtype=float, copy=True) + # Force A to be nonsingular. Since A is the Laplacian matrix of a + # connected graph, its rank deficiency is one, and thus one diagonal + # element needs to modified. Changing to infinity forces a zero in the + # corresponding element in the solution. + i = (A.indptr[1:] - A.indptr[:-1]).argmax() + A[i, i] = np.inf + solver = _LUSolver(A) + else: + raise nx.NetworkXError(f"Unknown linear system solver: {method}") + + # Initialize. + Lnorm = abs(L).sum(axis=1).flatten().max() + project(X) + W = np.ndarray(X.shape, order="F") + + while True: + # Orthonormalize X. + X = np.linalg.qr(X)[0] + # Compute iteration matrix H. + W[:, :] = L @ X + H = X.T @ W + sigma, Y = sp.linalg.eigh(H, overwrite_a=True) + # Compute the Ritz vectors. + X = X @ Y + # Test for convergence exploiting the fact that L * X == W * Y. + res = sp.linalg.blas.dasum(W @ Y[:, 0] - sigma[0] * X[:, 0]) / Lnorm + if res < tol: + break + # Compute X = L \ X / (X' * (L \ X)). + # L \ X can have an arbitrary projection on the nullspace of L, + # which will be eliminated. + W[:, :] = solver.solve(X, tol) + X = (sp.linalg.inv(W.T @ X) @ W.T).T # Preserves Fortran storage order. + project(X) + + return sigma, np.asarray(X) + + +def _get_fiedler_func(method): + """Returns a function that solves the Fiedler eigenvalue problem.""" + import numpy as np + + if method == "tracemin": # old style keyword `. + + Returns + ------- + algebraic_connectivity : float + Algebraic connectivity. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + NetworkXError + If G has less than two nodes. + + Notes + ----- + Edge weights are interpreted by their absolute values. For MultiGraph's, + weights of parallel edges are summed. Zero-weighted edges are ignored. + + See Also + -------- + laplacian_matrix + + Examples + -------- + For undirected graphs algebraic connectivity can tell us if a graph is connected or not + `G` is connected iff ``algebraic_connectivity(G) > 0``: + + >>> G = nx.complete_graph(5) + >>> nx.algebraic_connectivity(G) > 0 + True + >>> G.add_node(10) # G is no longer connected + >>> nx.algebraic_connectivity(G) > 0 + False + + """ + if len(G) < 2: + raise nx.NetworkXError("graph has less than two nodes.") + G = _preprocess_graph(G, weight) + if not nx.is_connected(G): + return 0.0 + + L = nx.laplacian_matrix(G) + if L.shape[0] == 2: + return 2.0 * float(L[0, 0]) if not normalized else 2.0 + + find_fiedler = _get_fiedler_func(method) + x = None if method != "lobpcg" else _rcm_estimate(G, G) + sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) + return float(sigma) + + +@not_implemented_for("directed") +@np_random_state(5) +@nx._dispatchable(edge_attrs="weight") +def fiedler_vector( + G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None +): + """Returns the Fiedler vector of a connected undirected graph. + + The Fiedler vector of a connected undirected graph is the eigenvector + corresponding to the second smallest eigenvalue of the Laplacian matrix + of the graph. + + Parameters + ---------- + G : NetworkX graph + An undirected graph. + + weight : object, optional (default: None) + The data key used to determine the weight of each edge. If None, then + each edge has unit weight. + + normalized : bool, optional (default: False) + Whether the normalized Laplacian matrix is used. + + tol : float, optional (default: 1e-8) + Tolerance of relative residual in eigenvalue computation. + + method : string, optional (default: 'tracemin_pcg') + Method of eigenvalue computation. It must be one of the tracemin + options shown below (TraceMIN), 'lanczos' (Lanczos iteration) + or 'lobpcg' (LOBPCG). + + The TraceMIN algorithm uses a linear system solver. The following + values allow specifying the solver to be used. + + =============== ======================================== + Value Solver + =============== ======================================== + 'tracemin_pcg' Preconditioned conjugate gradient method + 'tracemin_lu' LU factorization + =============== ======================================== + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + fiedler_vector : NumPy array of floats. + Fiedler vector. + + Raises + ------ + NetworkXNotImplemented + If G is directed. + + NetworkXError + If G has less than two nodes or is not connected. + + Notes + ----- + Edge weights are interpreted by their absolute values. For MultiGraph's, + weights of parallel edges are summed. Zero-weighted edges are ignored. + + See Also + -------- + laplacian_matrix + + Examples + -------- + Given a connected graph the signs of the values in the Fiedler vector can be + used to partition the graph into two components. + + >>> G = nx.barbell_graph(5, 0) + >>> nx.fiedler_vector(G, normalized=True, seed=1) + array([-0.32864129, -0.32864129, -0.32864129, -0.32864129, -0.26072899, + 0.26072899, 0.32864129, 0.32864129, 0.32864129, 0.32864129]) + + The connected components are the two 5-node cliques of the barbell graph. + """ + import numpy as np + + if len(G) < 2: + raise nx.NetworkXError("graph has less than two nodes.") + G = _preprocess_graph(G, weight) + if not nx.is_connected(G): + raise nx.NetworkXError("graph is not connected.") + + if len(G) == 2: + return np.array([1.0, -1.0]) + + find_fiedler = _get_fiedler_func(method) + L = nx.laplacian_matrix(G) + x = None if method != "lobpcg" else _rcm_estimate(G, G) + sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) + return fiedler + + +@np_random_state(5) +@nx._dispatchable(edge_attrs="weight") +def spectral_ordering( + G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None +): + """Compute the spectral_ordering of a graph. + + The spectral ordering of a graph is an ordering of its nodes where nodes + in the same weakly connected components appear contiguous and ordered by + their corresponding elements in the Fiedler vector of the component. + + Parameters + ---------- + G : NetworkX graph + A graph. + + weight : object, optional (default: None) + The data key used to determine the weight of each edge. If None, then + each edge has unit weight. + + normalized : bool, optional (default: False) + Whether the normalized Laplacian matrix is used. + + tol : float, optional (default: 1e-8) + Tolerance of relative residual in eigenvalue computation. + + method : string, optional (default: 'tracemin_pcg') + Method of eigenvalue computation. It must be one of the tracemin + options shown below (TraceMIN), 'lanczos' (Lanczos iteration) + or 'lobpcg' (LOBPCG). + + The TraceMIN algorithm uses a linear system solver. The following + values allow specifying the solver to be used. + + =============== ======================================== + Value Solver + =============== ======================================== + 'tracemin_pcg' Preconditioned conjugate gradient method + 'tracemin_lu' LU factorization + =============== ======================================== + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + spectral_ordering : NumPy array of floats. + Spectral ordering of nodes. + + Raises + ------ + NetworkXError + If G is empty. + + Notes + ----- + Edge weights are interpreted by their absolute values. For MultiGraph's, + weights of parallel edges are summed. Zero-weighted edges are ignored. + + See Also + -------- + laplacian_matrix + """ + if len(G) == 0: + raise nx.NetworkXError("graph is empty.") + G = _preprocess_graph(G, weight) + + find_fiedler = _get_fiedler_func(method) + order = [] + for component in nx.connected_components(G): + size = len(component) + if size > 2: + L = nx.laplacian_matrix(G, component) + x = None if method != "lobpcg" else _rcm_estimate(G, component) + sigma, fiedler = find_fiedler(L, x, normalized, tol, seed) + sort_info = zip(fiedler, range(size), component) + order.extend(u for x, c, u in sorted(sort_info)) + else: + order.extend(component) + + return order + + +@nx._dispatchable(edge_attrs="weight") +def spectral_bisection( + G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None +): + """Bisect the graph using the Fiedler vector. + + This method uses the Fiedler vector to bisect a graph. + The partition is defined by the nodes which are associated with + either positive or negative values in the vector. + + Parameters + ---------- + G : NetworkX Graph + + weight : str, optional (default: weight) + The data key used to determine the weight of each edge. If None, then + each edge has unit weight. + + normalized : bool, optional (default: False) + Whether the normalized Laplacian matrix is used. + + tol : float, optional (default: 1e-8) + Tolerance of relative residual in eigenvalue computation. + + method : string, optional (default: 'tracemin_pcg') + Method of eigenvalue computation. It must be one of the tracemin + options shown below (TraceMIN), 'lanczos' (Lanczos iteration) + or 'lobpcg' (LOBPCG). + + The TraceMIN algorithm uses a linear system solver. The following + values allow specifying the solver to be used. + + =============== ======================================== + Value Solver + =============== ======================================== + 'tracemin_pcg' Preconditioned conjugate gradient method + 'tracemin_lu' LU factorization + =============== ======================================== + + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + bisection : tuple of sets + Sets with the bisection of nodes + + Examples + -------- + >>> G = nx.barbell_graph(3, 0) + >>> nx.spectral_bisection(G) + ({0, 1, 2}, {3, 4, 5}) + + References + ---------- + .. [1] M. E. J Newman 'Networks: An Introduction', pages 364-370 + Oxford University Press 2011. + """ + import numpy as np + + v = nx.fiedler_vector(G, weight, normalized, tol, method, seed) + nodes = np.array(list(G)) + pos_vals = v >= 0 + + return set(nodes[~pos_vals].tolist()), set(nodes[pos_vals].tolist()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..989e8ff5798e3d778dca6c13d762b09a3dd58bd7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py @@ -0,0 +1,466 @@ +""" +Functions for constructing matrix-like objects from graph attributes. +""" + +import networkx as nx + +__all__ = ["attr_matrix", "attr_sparse_matrix"] + + +def _node_value(G, node_attr): + """Returns a function that returns a value from G.nodes[u]. + + We return a function expecting a node as its sole argument. Then, in the + simplest scenario, the returned function will return G.nodes[u][node_attr]. + However, we also handle the case when `node_attr` is None (returns the node) + or when `node_attr` is a function itself. + + Parameters + ---------- + G : graph + A NetworkX graph + + node_attr : {None, str, callable} + Specification of how the value of the node attribute should be obtained + from the node attribute dictionary. + + Returns + ------- + value : function + A function expecting a node as its sole argument. The function will + returns a value from G.nodes[u] that depends on `edge_attr`. + + """ + if node_attr is None: + + def value(u): + return u + + elif not callable(node_attr): + # assume it is a key for the node attribute dictionary + def value(u): + return G.nodes[u][node_attr] + + else: + # Advanced: Allow users to specify something else. + # + # For example, + # node_attr = lambda u: G.nodes[u].get('size', .5) * 3 + # + value = node_attr + + return value + + +def _edge_value(G, edge_attr): + """Returns a function that returns a value from G[u][v]. + + Suppose there exists an edge between u and v. Then we return a function + expecting u and v as arguments. For Graph and DiGraph, G[u][v] is + the edge attribute dictionary, and the function (essentially) returns + G[u][v][edge_attr]. However, we also handle cases when `edge_attr` is None + and when it is a function itself. For MultiGraph and MultiDiGraph, G[u][v] + is a dictionary of all edges between u and v. In this case, the returned + function sums the value of `edge_attr` for every edge between u and v. + + Parameters + ---------- + G : graph + A NetworkX graph + + edge_attr : {None, str, callable} + Specification of how the value of the edge attribute should be obtained + from the edge attribute dictionary, G[u][v]. For multigraphs, G[u][v] + is a dictionary of all the edges between u and v. This allows for + special treatment of multiedges. + + Returns + ------- + value : function + A function expecting two nodes as parameters. The nodes should + represent the from- and to- node of an edge. The function will + return a value from G[u][v] that depends on `edge_attr`. + + """ + + if edge_attr is None: + # topological count of edges + + if G.is_multigraph(): + + def value(u, v): + return len(G[u][v]) + + else: + + def value(u, v): + return 1 + + elif not callable(edge_attr): + # assume it is a key for the edge attribute dictionary + + if edge_attr == "weight": + # provide a default value + if G.is_multigraph(): + + def value(u, v): + return sum(d.get(edge_attr, 1) for d in G[u][v].values()) + + else: + + def value(u, v): + return G[u][v].get(edge_attr, 1) + + else: + # otherwise, the edge attribute MUST exist for each edge + if G.is_multigraph(): + + def value(u, v): + return sum(d[edge_attr] for d in G[u][v].values()) + + else: + + def value(u, v): + return G[u][v][edge_attr] + + else: + # Advanced: Allow users to specify something else. + # + # Alternative default value: + # edge_attr = lambda u,v: G[u][v].get('thickness', .5) + # + # Function on an attribute: + # edge_attr = lambda u,v: abs(G[u][v]['weight']) + # + # Handle Multi(Di)Graphs differently: + # edge_attr = lambda u,v: numpy.prod([d['size'] for d in G[u][v].values()]) + # + # Ignore multiple edges + # edge_attr = lambda u,v: 1 if len(G[u][v]) else 0 + # + value = edge_attr + + return value + + +@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr") +def attr_matrix( + G, + edge_attr=None, + node_attr=None, + normalized=False, + rc_order=None, + dtype=None, + order=None, +): + """Returns the attribute matrix using attributes from `G` as a numpy array. + + If only `G` is passed in, then the adjacency matrix is constructed. + + Let A be a discrete set of values for the node attribute `node_attr`. Then + the elements of A represent the rows and columns of the constructed matrix. + Now, iterate through every edge e=(u,v) in `G` and consider the value + of the edge attribute `edge_attr`. If ua and va are the values of the + node attribute `node_attr` for u and v, respectively, then the value of + the edge attribute is added to the matrix element at (ua, va). + + Parameters + ---------- + G : graph + The NetworkX graph used to construct the attribute matrix. + + edge_attr : str, optional (default: number of edges for each matrix element) + Each element of the matrix represents a running total of the + specified edge attribute for edges whose node attributes correspond + to the rows/cols of the matrix. The attribute must be present for + all edges in the graph. If no attribute is specified, then we + just count the number of edges whose node attributes correspond + to the matrix element. + + node_attr : str, optional (default: use nodes of the graph) + Each row and column in the matrix represents a particular value + of the node attribute. The attribute must be present for all nodes + in the graph. Note, the values of this attribute should be reliably + hashable. So, float values are not recommended. If no attribute is + specified, then the rows and columns will be the nodes of the graph. + + normalized : bool, optional (default: False) + If True, then each row is normalized by the summation of its values. + + rc_order : list, optional (default: order of nodes in G) + A list of the node attribute values. This list specifies the ordering + of rows and columns of the array. If no ordering is provided, then + the ordering will be the same as the node order in `G`. + When `rc_order` is `None`, the function returns a 2-tuple ``(matrix, ordering)`` + + Other Parameters + ---------------- + dtype : NumPy data-type, optional + A valid NumPy dtype used to initialize the array. Keep in mind certain + dtypes can yield unexpected results if the array is to be normalized. + The parameter is passed to numpy.zeros(). If unspecified, the NumPy + default is used. + + order : {'C', 'F'}, optional + Whether to store multidimensional data in C- or Fortran-contiguous + (row- or column-wise) order in memory. This parameter is passed to + numpy.zeros(). If unspecified, the NumPy default is used. + + Returns + ------- + M : 2D NumPy ndarray + The attribute matrix. + + ordering : list + If `rc_order` was specified, then only the attribute matrix is returned. + However, if `rc_order` was None, then the ordering used to construct + the matrix is returned as well. + + Examples + -------- + Construct an adjacency matrix: + + >>> G = nx.Graph() + >>> G.add_edge(0, 1, thickness=1, weight=3) + >>> G.add_edge(0, 2, thickness=2) + >>> G.add_edge(1, 2, thickness=3) + >>> nx.attr_matrix(G, rc_order=[0, 1, 2]) + array([[0., 1., 1.], + [1., 0., 1.], + [1., 1., 0.]]) + + Alternatively, we can obtain the matrix describing edge thickness. + + >>> nx.attr_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2]) + array([[0., 1., 2.], + [1., 0., 3.], + [2., 3., 0.]]) + + We can also color the nodes and ask for the probability distribution over + all edges (u,v) describing: + + Pr(v has color Y | u has color X) + + >>> G.nodes[0]["color"] = "red" + >>> G.nodes[1]["color"] = "red" + >>> G.nodes[2]["color"] = "blue" + >>> rc = ["red", "blue"] + >>> nx.attr_matrix(G, node_attr="color", normalized=True, rc_order=rc) + array([[0.33333333, 0.66666667], + [1. , 0. ]]) + + For example, the above tells us that for all edges (u,v): + + Pr( v is red | u is red) = 1/3 + Pr( v is blue | u is red) = 2/3 + + Pr( v is red | u is blue) = 1 + Pr( v is blue | u is blue) = 0 + + Finally, we can obtain the total weights listed by the node colors. + + >>> nx.attr_matrix(G, edge_attr="weight", node_attr="color", rc_order=rc) + array([[3., 2.], + [2., 0.]]) + + Thus, the total weight over all edges (u,v) with u and v having colors: + + (red, red) is 3 # the sole contribution is from edge (0,1) + (red, blue) is 2 # contributions from edges (0,2) and (1,2) + (blue, red) is 2 # same as (red, blue) since graph is undirected + (blue, blue) is 0 # there are no edges with blue endpoints + + """ + import numpy as np + + edge_value = _edge_value(G, edge_attr) + node_value = _node_value(G, node_attr) + + if rc_order is None: + ordering = list({node_value(n) for n in G}) + else: + ordering = rc_order + + N = len(ordering) + undirected = not G.is_directed() + index = dict(zip(ordering, range(N))) + M = np.zeros((N, N), dtype=dtype, order=order) + + seen = set() + for u, nbrdict in G.adjacency(): + for v in nbrdict: + # Obtain the node attribute values. + i, j = index[node_value(u)], index[node_value(v)] + if v not in seen: + M[i, j] += edge_value(u, v) + if undirected: + M[j, i] = M[i, j] + + if undirected: + seen.add(u) + + if normalized: + M /= M.sum(axis=1).reshape((N, 1)) + + if rc_order is None: + return M, ordering + else: + return M + + +@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr") +def attr_sparse_matrix( + G, edge_attr=None, node_attr=None, normalized=False, rc_order=None, dtype=None +): + """Returns a SciPy sparse array using attributes from G. + + If only `G` is passed in, then the adjacency matrix is constructed. + + Let A be a discrete set of values for the node attribute `node_attr`. Then + the elements of A represent the rows and columns of the constructed matrix. + Now, iterate through every edge e=(u,v) in `G` and consider the value + of the edge attribute `edge_attr`. If ua and va are the values of the + node attribute `node_attr` for u and v, respectively, then the value of + the edge attribute is added to the matrix element at (ua, va). + + Parameters + ---------- + G : graph + The NetworkX graph used to construct the NumPy matrix. + + edge_attr : str, optional (default: number of edges for each matrix element) + Each element of the matrix represents a running total of the + specified edge attribute for edges whose node attributes correspond + to the rows/cols of the matrix. The attribute must be present for + all edges in the graph. If no attribute is specified, then we + just count the number of edges whose node attributes correspond + to the matrix element. + + node_attr : str, optional (default: use nodes of the graph) + Each row and column in the matrix represents a particular value + of the node attribute. The attribute must be present for all nodes + in the graph. Note, the values of this attribute should be reliably + hashable. So, float values are not recommended. If no attribute is + specified, then the rows and columns will be the nodes of the graph. + + normalized : bool, optional (default: False) + If True, then each row is normalized by the summation of its values. + + rc_order : list, optional (default: order of nodes in G) + A list of the node attribute values. This list specifies the ordering + of rows and columns of the array and the return value. If no ordering + is provided, then the ordering will be that of nodes in `G`. + + Other Parameters + ---------------- + dtype : NumPy data-type, optional + A valid NumPy dtype used to initialize the array. Keep in mind certain + dtypes can yield unexpected results if the array is to be normalized. + The parameter is passed to numpy.zeros(). If unspecified, the NumPy + default is used. + + Returns + ------- + M : SciPy sparse array + The attribute matrix. + + ordering : list + If `rc_order` was specified, then only the matrix is returned. + However, if `rc_order` was None, then the ordering used to construct + the matrix is returned as well. + + Examples + -------- + Construct an adjacency matrix: + + >>> G = nx.Graph() + >>> G.add_edge(0, 1, thickness=1, weight=3) + >>> G.add_edge(0, 2, thickness=2) + >>> G.add_edge(1, 2, thickness=3) + >>> M = nx.attr_sparse_matrix(G, rc_order=[0, 1, 2]) + >>> M.toarray() + array([[0., 1., 1.], + [1., 0., 1.], + [1., 1., 0.]]) + + Alternatively, we can obtain the matrix describing edge thickness. + + >>> M = nx.attr_sparse_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2]) + >>> M.toarray() + array([[0., 1., 2.], + [1., 0., 3.], + [2., 3., 0.]]) + + We can also color the nodes and ask for the probability distribution over + all edges (u,v) describing: + + Pr(v has color Y | u has color X) + + >>> G.nodes[0]["color"] = "red" + >>> G.nodes[1]["color"] = "red" + >>> G.nodes[2]["color"] = "blue" + >>> rc = ["red", "blue"] + >>> M = nx.attr_sparse_matrix(G, node_attr="color", normalized=True, rc_order=rc) + >>> M.toarray() + array([[0.33333333, 0.66666667], + [1. , 0. ]]) + + For example, the above tells us that for all edges (u,v): + + Pr( v is red | u is red) = 1/3 + Pr( v is blue | u is red) = 2/3 + + Pr( v is red | u is blue) = 1 + Pr( v is blue | u is blue) = 0 + + Finally, we can obtain the total weights listed by the node colors. + + >>> M = nx.attr_sparse_matrix(G, edge_attr="weight", node_attr="color", rc_order=rc) + >>> M.toarray() + array([[3., 2.], + [2., 0.]]) + + Thus, the total weight over all edges (u,v) with u and v having colors: + + (red, red) is 3 # the sole contribution is from edge (0,1) + (red, blue) is 2 # contributions from edges (0,2) and (1,2) + (blue, red) is 2 # same as (red, blue) since graph is undirected + (blue, blue) is 0 # there are no edges with blue endpoints + + """ + import numpy as np + import scipy as sp + + edge_value = _edge_value(G, edge_attr) + node_value = _node_value(G, node_attr) + + if rc_order is None: + ordering = list({node_value(n) for n in G}) + else: + ordering = rc_order + + N = len(ordering) + undirected = not G.is_directed() + index = dict(zip(ordering, range(N))) + M = sp.sparse.lil_array((N, N), dtype=dtype) + + seen = set() + for u, nbrdict in G.adjacency(): + for v in nbrdict: + # Obtain the node attribute values. + i, j = index[node_value(u)], index[node_value(v)] + if v not in seen: + M[i, j] += edge_value(u, v) + if undirected: + M[j, i] = M[i, j] + + if undirected: + seen.add(u) + + if normalized: + M *= 1 / M.sum(axis=1)[:, np.newaxis] # in-place mult preserves sparse + + if rc_order is None: + return M, ordering + else: + return M diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..717e24711b37060de3eb0b42a714c95184d7b694 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py @@ -0,0 +1,77 @@ +"""Bethe Hessian or deformed Laplacian matrix of graphs.""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["bethe_hessian_matrix"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable +def bethe_hessian_matrix(G, r=None, nodelist=None): + r"""Returns the Bethe Hessian matrix of G. + + The Bethe Hessian is a family of matrices parametrized by r, defined as + H(r) = (r^2 - 1) I - r A + D where A is the adjacency matrix, D is the + diagonal matrix of node degrees, and I is the identify matrix. It is equal + to the graph laplacian when the regularizer r = 1. + + The default choice of regularizer should be the ratio [2]_ + + .. math:: + r_m = \left(\sum k_i \right)^{-1}\left(\sum k_i^2 \right) - 1 + + Parameters + ---------- + G : Graph + A NetworkX graph + r : float + Regularizer parameter + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by ``G.nodes()``. + + Returns + ------- + H : scipy.sparse.csr_array + The Bethe Hessian matrix of `G`, with parameter `r`. + + Examples + -------- + >>> k = [3, 2, 2, 1, 0] + >>> G = nx.havel_hakimi_graph(k) + >>> H = nx.bethe_hessian_matrix(G) + >>> H.toarray() + array([[ 3.5625, -1.25 , -1.25 , -1.25 , 0. ], + [-1.25 , 2.5625, -1.25 , 0. , 0. ], + [-1.25 , -1.25 , 2.5625, 0. , 0. ], + [-1.25 , 0. , 0. , 1.5625, 0. ], + [ 0. , 0. , 0. , 0. , 0.5625]]) + + See Also + -------- + bethe_hessian_spectrum + adjacency_matrix + laplacian_matrix + + References + ---------- + .. [1] A. Saade, F. Krzakala and L. Zdeborová + "Spectral Clustering of Graphs with the Bethe Hessian", + Advances in Neural Information Processing Systems, 2014. + .. [2] C. M. Le, E. Levina + "Estimating the number of communities in networks by spectral methods" + arXiv:1507.00827, 2015. + """ + import scipy as sp + + if nodelist is None: + nodelist = list(G) + if r is None: + r = sum(d**2 for v, d in nx.degree(G)) / sum(d for v, d in nx.degree(G)) - 1 + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, format="csr") + n, m = A.shape + D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(m, n)).tocsr() + I = sp.sparse.eye_array(m, n, format="csr") + return (r**2 - 1) * I - r * A + D diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..9f477bcccc5b6a077b8c7f1807299d71664ebede --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py @@ -0,0 +1,168 @@ +""" +Adjacency matrix and incidence matrix of graphs. +""" + +import networkx as nx + +__all__ = ["incidence_matrix", "adjacency_matrix"] + + +@nx._dispatchable(edge_attrs="weight") +def incidence_matrix( + G, nodelist=None, edgelist=None, oriented=False, weight=None, *, dtype=None +): + """Returns incidence matrix of G. + + The incidence matrix assigns each row to a node and each column to an edge. + For a standard incidence matrix a 1 appears wherever a row's node is + incident on the column's edge. For an oriented incidence matrix each + edge is assigned an orientation (arbitrarily for undirected and aligning to + direction for directed). A -1 appears for the source (tail) of an edge and + 1 for the destination (head) of the edge. The elements are zero otherwise. + + Parameters + ---------- + G : graph + A NetworkX graph + + nodelist : list, optional (default= all nodes in G) + The rows are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + edgelist : list, optional (default= all edges in G) + The columns are ordered according to the edges in edgelist. + If edgelist is None, then the ordering is produced by G.edges(). + + oriented: bool, optional (default=False) + If True, matrix elements are +1 or -1 for the head or tail node + respectively of each edge. If False, +1 occurs at both nodes. + + weight : string or None, optional (default=None) + The edge data key used to provide each value in the matrix. + If None, then each edge has weight 1. Edge weights, if used, + should be positive so that the orientation can provide the sign. + + dtype : a NumPy dtype or None (default=None) + The dtype of the output sparse array. This type should be a compatible + type of the weight argument, eg. if weight would return a float this + argument should also be a float. + If None, then the default for SciPy is used. + + Returns + ------- + A : SciPy sparse array + The incidence matrix of G. + + Notes + ----- + For MultiGraph/MultiDiGraph, the edges in edgelist should be + (u,v,key) 3-tuples. + + "Networks are the best discrete model for so many problems in + applied mathematics" [1]_. + + References + ---------- + .. [1] Gil Strang, Network applications: A = incidence matrix, + http://videolectures.net/mit18085f07_strang_lec03/ + """ + import scipy as sp + + if nodelist is None: + nodelist = list(G) + if edgelist is None: + if G.is_multigraph(): + edgelist = list(G.edges(keys=True)) + else: + edgelist = list(G.edges()) + A = sp.sparse.lil_array((len(nodelist), len(edgelist)), dtype=dtype) + node_index = {node: i for i, node in enumerate(nodelist)} + for ei, e in enumerate(edgelist): + (u, v) = e[:2] + if u == v: + continue # self loops give zero column + try: + ui = node_index[u] + vi = node_index[v] + except KeyError as err: + raise nx.NetworkXError( + f"node {u} or {v} in edgelist but not in nodelist" + ) from err + if weight is None: + wt = 1 + else: + if G.is_multigraph(): + ekey = e[2] + wt = G[u][v][ekey].get(weight, 1) + else: + wt = G[u][v].get(weight, 1) + if oriented: + A[ui, ei] = -wt + A[vi, ei] = wt + else: + A[ui, ei] = wt + A[vi, ei] = wt + return A.asformat("csc") + + +@nx._dispatchable(edge_attrs="weight") +def adjacency_matrix(G, nodelist=None, dtype=None, weight="weight"): + """Returns adjacency matrix of `G`. + + Parameters + ---------- + G : graph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in `nodelist`. + If ``nodelist=None`` (the default), then the ordering is produced by + ``G.nodes()``. + + dtype : NumPy data-type, optional + The desired data-type for the array. + If `None`, then the NumPy default is used. + + weight : string or None, optional (default='weight') + The edge data key used to provide each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + A : SciPy sparse array + Adjacency matrix representation of G. + + Notes + ----- + For directed graphs, entry ``i, j`` corresponds to an edge from ``i`` to ``j``. + + If you want a pure Python adjacency matrix representation try + :func:`~networkx.convert.to_dict_of_dicts` which will return a + dictionary-of-dictionaries format that can be addressed as a + sparse matrix. + + For multigraphs with parallel edges the weights are summed. + See :func:`networkx.convert_matrix.to_numpy_array` for other options. + + The convention used for self-loop edges in graphs is to assign the + diagonal matrix entry value to the edge weight attribute + (or the number 1 if the edge has no weight attribute). If the + alternate convention of doubling the edge weight is desired the + resulting SciPy sparse array can be modified as follows:: + + >>> G = nx.Graph([(1, 1)]) + >>> A = nx.adjacency_matrix(G) + >>> A.toarray() + array([[1]]) + >>> A.setdiag(A.diagonal() * 2) + >>> A.toarray() + array([[2]]) + + See Also + -------- + to_numpy_array + to_scipy_sparse_array + to_dict_of_dicts + adjacency_spectrum + """ + return nx.to_scipy_sparse_array(G, nodelist=nodelist, dtype=dtype, weight=weight) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..0453f3b2dffdf2ba8706b72d81c70849a30b8b02 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py @@ -0,0 +1,512 @@ +"""Laplacian matrix of graphs. + +All calculations here are done using the out-degree. For Laplacians using +in-degree, use `G.reverse(copy=False)` instead of `G` and take the transpose. + +The `laplacian_matrix` function provides an unnormalized matrix, +while `normalized_laplacian_matrix`, `directed_laplacian_matrix`, +and `directed_combinatorial_laplacian_matrix` are all normalized. +""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = [ + "laplacian_matrix", + "normalized_laplacian_matrix", + "directed_laplacian_matrix", + "directed_combinatorial_laplacian_matrix", +] + + +@nx._dispatchable(edge_attrs="weight") +def laplacian_matrix(G, nodelist=None, weight="weight"): + """Returns the Laplacian matrix of G. + + The graph Laplacian is the matrix L = D - A, where + A is the adjacency matrix and D is the diagonal matrix of node degrees. + + Parameters + ---------- + G : graph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + L : SciPy sparse array + The Laplacian matrix of G. + + Notes + ----- + For MultiGraph, the edges weights are summed. + + This returns an unnormalized matrix. For a normalized output, + use `normalized_laplacian_matrix`, `directed_laplacian_matrix`, + or `directed_combinatorial_laplacian_matrix`. + + This calculation uses the out-degree of the graph `G`. To use the + in-degree for calculations instead, use `G.reverse(copy=False)` and + take the transpose. + + See Also + -------- + :func:`~networkx.convert_matrix.to_numpy_array` + normalized_laplacian_matrix + directed_laplacian_matrix + directed_combinatorial_laplacian_matrix + :func:`~networkx.linalg.spectrum.laplacian_spectrum` + + Examples + -------- + For graphs with multiple connected components, L is permutation-similar + to a block diagonal matrix where each block is the respective Laplacian + matrix for each component. + + >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) + >>> print(nx.laplacian_matrix(G).toarray()) + [[ 1 -1 0 0 0] + [-1 2 -1 0 0] + [ 0 -1 1 0 0] + [ 0 0 0 1 -1] + [ 0 0 0 -1 1]] + + >>> edges = [ + ... (1, 2), + ... (2, 1), + ... (2, 4), + ... (4, 3), + ... (3, 4), + ... ] + >>> DiG = nx.DiGraph(edges) + >>> print(nx.laplacian_matrix(DiG).toarray()) + [[ 1 -1 0 0] + [-1 2 -1 0] + [ 0 0 1 -1] + [ 0 0 -1 1]] + + Notice that node 4 is represented by the third column and row. This is because + by default the row/column order is the order of `G.nodes` (i.e. the node added + order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).) + To control the node order of the matrix, use the `nodelist` argument. + + >>> print(nx.laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray()) + [[ 1 -1 0 0] + [-1 2 0 -1] + [ 0 0 1 -1] + [ 0 0 -1 1]] + + This calculation uses the out-degree of the graph `G`. To use the + in-degree for calculations instead, use `G.reverse(copy=False)` and + take the transpose. + + >>> print(nx.laplacian_matrix(DiG.reverse(copy=False)).toarray().T) + [[ 1 -1 0 0] + [-1 1 -1 0] + [ 0 0 2 -1] + [ 0 0 -1 1]] + + References + ---------- + .. [1] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond: + The Science of Search Engine Rankings. Princeton University Press, 2006. + + """ + import scipy as sp + + if nodelist is None: + nodelist = list(G) + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") + n, m = A.shape + D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(m, n)).tocsr() + return D - A + + +@nx._dispatchable(edge_attrs="weight") +def normalized_laplacian_matrix(G, nodelist=None, weight="weight"): + r"""Returns the normalized Laplacian matrix of G. + + The normalized graph Laplacian is the matrix + + .. math:: + + N = D^{-1/2} L D^{-1/2} + + where `L` is the graph Laplacian and `D` is the diagonal matrix of + node degrees [1]_. + + Parameters + ---------- + G : graph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + N : SciPy sparse array + The normalized Laplacian matrix of G. + + Notes + ----- + For MultiGraph, the edges weights are summed. + See :func:`to_numpy_array` for other options. + + If the Graph contains selfloops, D is defined as ``diag(sum(A, 1))``, where A is + the adjacency matrix [2]_. + + This calculation uses the out-degree of the graph `G`. To use the + in-degree for calculations instead, use `G.reverse(copy=False)` and + take the transpose. + + For an unnormalized output, use `laplacian_matrix`. + + Examples + -------- + + >>> import numpy as np + >>> edges = [ + ... (1, 2), + ... (2, 1), + ... (2, 4), + ... (4, 3), + ... (3, 4), + ... ] + >>> DiG = nx.DiGraph(edges) + >>> print(nx.normalized_laplacian_matrix(DiG).toarray()) + [[ 1. -0.70710678 0. 0. ] + [-0.70710678 1. -0.70710678 0. ] + [ 0. 0. 1. -1. ] + [ 0. 0. -1. 1. ]] + + Notice that node 4 is represented by the third column and row. This is because + by default the row/column order is the order of `G.nodes` (i.e. the node added + order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).) + To control the node order of the matrix, use the `nodelist` argument. + + >>> print(nx.normalized_laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray()) + [[ 1. -0.70710678 0. 0. ] + [-0.70710678 1. 0. -0.70710678] + [ 0. 0. 1. -1. ] + [ 0. 0. -1. 1. ]] + >>> G = nx.Graph(edges) + >>> print(nx.normalized_laplacian_matrix(G).toarray()) + [[ 1. -0.70710678 0. 0. ] + [-0.70710678 1. -0.5 0. ] + [ 0. -0.5 1. -0.70710678] + [ 0. 0. -0.70710678 1. ]] + + See Also + -------- + laplacian_matrix + normalized_laplacian_spectrum + directed_laplacian_matrix + directed_combinatorial_laplacian_matrix + + References + ---------- + .. [1] Fan Chung-Graham, Spectral Graph Theory, + CBMS Regional Conference Series in Mathematics, Number 92, 1997. + .. [2] Steve Butler, Interlacing For Weighted Graphs Using The Normalized + Laplacian, Electronic Journal of Linear Algebra, Volume 16, pp. 90-98, + March 2007. + .. [3] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond: + The Science of Search Engine Rankings. Princeton University Press, 2006. + """ + import numpy as np + import scipy as sp + + if nodelist is None: + nodelist = list(G) + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") + n, _ = A.shape + diags = A.sum(axis=1) + D = sp.sparse.dia_array((diags, 0), shape=(n, n)).tocsr() + L = D - A + with np.errstate(divide="ignore"): + diags_sqrt = 1.0 / np.sqrt(diags) + diags_sqrt[np.isinf(diags_sqrt)] = 0 + DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(n, n)).tocsr() + return DH @ (L @ DH) + + +############################################################################### +# Code based on work from https://github.com/bjedwards + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def directed_laplacian_matrix( + G, nodelist=None, weight="weight", walk_type=None, alpha=0.95 +): + r"""Returns the directed Laplacian matrix of G. + + The graph directed Laplacian is the matrix + + .. math:: + + L = I - \frac{1}{2} \left (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} \right ) + + where `I` is the identity matrix, `P` is the transition matrix of the + graph, and `\Phi` a matrix with the Perron vector of `P` in the diagonal and + zeros elsewhere [1]_. + + Depending on the value of walk_type, `P` can be the transition matrix + induced by a random walk, a lazy random walk, or a random walk with + teleportation (PageRank). + + Parameters + ---------- + G : DiGraph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + walk_type : string or None, optional (default=None) + One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` + (the default), then a value is selected according to the properties of `G`: + - ``walk_type="random"`` if `G` is strongly connected and aperiodic + - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic + - ``walk_type="pagerank"`` for all other cases. + + alpha : real + (1 - alpha) is the teleportation probability used with pagerank + + Returns + ------- + L : NumPy matrix + Normalized Laplacian of G. + + Notes + ----- + Only implemented for DiGraphs + + The result is always a symmetric matrix. + + This calculation uses the out-degree of the graph `G`. To use the + in-degree for calculations instead, use `G.reverse(copy=False)` and + take the transpose. + + See Also + -------- + laplacian_matrix + normalized_laplacian_matrix + directed_combinatorial_laplacian_matrix + + References + ---------- + .. [1] Fan Chung (2005). + Laplacians and the Cheeger inequality for directed graphs. + Annals of Combinatorics, 9(1), 2005 + """ + import numpy as np + import scipy as sp + + # NOTE: P has type ndarray if walk_type=="pagerank", else csr_array + P = _transition_matrix( + G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha + ) + + n, m = P.shape + + evals, evecs = sp.sparse.linalg.eigs(P.T, k=1) + v = evecs.flatten().real + p = v / v.sum() + # p>=0 by Perron-Frobenius Thm. Use abs() to fix roundoff across zero gh-6865 + sqrtp = np.sqrt(np.abs(p)) + Q = ( + sp.sparse.dia_array((sqrtp, 0), shape=(n, n)).tocsr() + @ P + @ sp.sparse.dia_array((1.0 / sqrtp, 0), shape=(n, n)).tocsr() + ) + # NOTE: This could be sparsified for the non-pagerank cases + I = np.identity(len(G)) + + return I - (Q + Q.T) / 2.0 + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def directed_combinatorial_laplacian_matrix( + G, nodelist=None, weight="weight", walk_type=None, alpha=0.95 +): + r"""Return the directed combinatorial Laplacian matrix of G. + + The graph directed combinatorial Laplacian is the matrix + + .. math:: + + L = \Phi - \frac{1}{2} \left (\Phi P + P^T \Phi \right) + + where `P` is the transition matrix of the graph and `\Phi` a matrix + with the Perron vector of `P` in the diagonal and zeros elsewhere [1]_. + + Depending on the value of walk_type, `P` can be the transition matrix + induced by a random walk, a lazy random walk, or a random walk with + teleportation (PageRank). + + Parameters + ---------- + G : DiGraph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + walk_type : string or None, optional (default=None) + One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` + (the default), then a value is selected according to the properties of `G`: + - ``walk_type="random"`` if `G` is strongly connected and aperiodic + - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic + - ``walk_type="pagerank"`` for all other cases. + + alpha : real + (1 - alpha) is the teleportation probability used with pagerank + + Returns + ------- + L : NumPy matrix + Combinatorial Laplacian of G. + + Notes + ----- + Only implemented for DiGraphs + + The result is always a symmetric matrix. + + This calculation uses the out-degree of the graph `G`. To use the + in-degree for calculations instead, use `G.reverse(copy=False)` and + take the transpose. + + See Also + -------- + laplacian_matrix + normalized_laplacian_matrix + directed_laplacian_matrix + + References + ---------- + .. [1] Fan Chung (2005). + Laplacians and the Cheeger inequality for directed graphs. + Annals of Combinatorics, 9(1), 2005 + """ + import scipy as sp + + P = _transition_matrix( + G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha + ) + + n, m = P.shape + + evals, evecs = sp.sparse.linalg.eigs(P.T, k=1) + v = evecs.flatten().real + p = v / v.sum() + # NOTE: could be improved by not densifying + Phi = sp.sparse.dia_array((p, 0), shape=(n, n)).toarray() + + return Phi - (Phi @ P + P.T @ Phi) / 2.0 + + +def _transition_matrix(G, nodelist=None, weight="weight", walk_type=None, alpha=0.95): + """Returns the transition matrix of G. + + This is a row stochastic giving the transition probabilities while + performing a random walk on the graph. Depending on the value of walk_type, + P can be the transition matrix induced by a random walk, a lazy random walk, + or a random walk with teleportation (PageRank). + + Parameters + ---------- + G : DiGraph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + walk_type : string or None, optional (default=None) + One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` + (the default), then a value is selected according to the properties of `G`: + - ``walk_type="random"`` if `G` is strongly connected and aperiodic + - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic + - ``walk_type="pagerank"`` for all other cases. + + alpha : real + (1 - alpha) is the teleportation probability used with pagerank + + Returns + ------- + P : numpy.ndarray + transition matrix of G. + + Raises + ------ + NetworkXError + If walk_type not specified or alpha not in valid range + """ + import numpy as np + import scipy as sp + + if walk_type is None: + if nx.is_strongly_connected(G): + if nx.is_aperiodic(G): + walk_type = "random" + else: + walk_type = "lazy" + else: + walk_type = "pagerank" + + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float) + n, m = A.shape + if walk_type in ["random", "lazy"]: + DI = sp.sparse.dia_array((1.0 / A.sum(axis=1), 0), shape=(n, n)).tocsr() + if walk_type == "random": + P = DI @ A + else: + I = sp.sparse.eye_array(n, format="csr") + P = (I + DI @ A) / 2.0 + + elif walk_type == "pagerank": + if not (0 < alpha < 1): + raise nx.NetworkXError("alpha must be between 0 and 1") + # this is using a dense representation. NOTE: This should be sparsified! + A = A.toarray() + # add constant to dangling nodes' row + A[A.sum(axis=1) == 0, :] = 1 / n + # normalize + A = A / A.sum(axis=1)[np.newaxis, :].T + P = alpha * A + (1 - alpha) / n + else: + raise nx.NetworkXError("walk_type must be random, lazy, or pagerank") + + return P diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..0287910bdccc61263afa4d6adfa2b0d78afc4daa --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py @@ -0,0 +1,166 @@ +"""Modularity matrix of graphs.""" + +import networkx as nx +from networkx.utils import not_implemented_for + +__all__ = ["modularity_matrix", "directed_modularity_matrix"] + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def modularity_matrix(G, nodelist=None, weight=None): + r"""Returns the modularity matrix of G. + + The modularity matrix is the matrix B = A - , where A is the adjacency + matrix and is the average adjacency matrix, assuming that the graph + is described by the configuration model. + + More specifically, the element B_ij of B is defined as + + .. math:: + A_{ij} - {k_i k_j \over 2 m} + + where k_i is the degree of node i, and where m is the number of edges + in the graph. When weight is set to a name of an attribute edge, Aij, k_i, + k_j and m are computed using its value. + + Parameters + ---------- + G : Graph + A NetworkX graph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used for + the edge weight. If None then all edge weights are 1. + + Returns + ------- + B : Numpy array + The modularity matrix of G. + + Examples + -------- + >>> k = [3, 2, 2, 1, 0] + >>> G = nx.havel_hakimi_graph(k) + >>> B = nx.modularity_matrix(G) + + + See Also + -------- + to_numpy_array + modularity_spectrum + adjacency_matrix + directed_modularity_matrix + + References + ---------- + .. [1] M. E. J. Newman, "Modularity and community structure in networks", + Proc. Natl. Acad. Sci. USA, vol. 103, pp. 8577-8582, 2006. + """ + import numpy as np + + if nodelist is None: + nodelist = list(G) + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") + k = A.sum(axis=1) + m = k.sum() * 0.5 + # Expected adjacency matrix + X = np.outer(k, k) / (2 * m) + + return A - X + + +@not_implemented_for("undirected") +@not_implemented_for("multigraph") +@nx._dispatchable(edge_attrs="weight") +def directed_modularity_matrix(G, nodelist=None, weight=None): + """Returns the directed modularity matrix of G. + + The modularity matrix is the matrix B = A - , where A is the adjacency + matrix and is the expected adjacency matrix, assuming that the graph + is described by the configuration model. + + More specifically, the element B_ij of B is defined as + + .. math:: + B_{ij} = A_{ij} - k_i^{out} k_j^{in} / m + + where :math:`k_i^{in}` is the in degree of node i, and :math:`k_j^{out}` is the out degree + of node j, with m the number of edges in the graph. When weight is set + to a name of an attribute edge, Aij, k_i, k_j and m are computed using + its value. + + Parameters + ---------- + G : DiGraph + A NetworkX DiGraph + + nodelist : list, optional + The rows and columns are ordered according to the nodes in nodelist. + If nodelist is None, then the ordering is produced by G.nodes(). + + weight : string or None, optional (default=None) + The edge attribute that holds the numerical value used for + the edge weight. If None then all edge weights are 1. + + Returns + ------- + B : Numpy array + The modularity matrix of G. + + Examples + -------- + >>> G = nx.DiGraph() + >>> G.add_edges_from( + ... ( + ... (1, 2), + ... (1, 3), + ... (3, 1), + ... (3, 2), + ... (3, 5), + ... (4, 5), + ... (4, 6), + ... (5, 4), + ... (5, 6), + ... (6, 4), + ... ) + ... ) + >>> B = nx.directed_modularity_matrix(G) + + + Notes + ----- + NetworkX defines the element A_ij of the adjacency matrix as 1 if there + is a link going from node i to node j. Leicht and Newman use the opposite + definition. This explains the different expression for B_ij. + + See Also + -------- + to_numpy_array + modularity_spectrum + adjacency_matrix + modularity_matrix + + References + ---------- + .. [1] E. A. Leicht, M. E. J. Newman, + "Community structure in directed networks", + Phys. Rev Lett., vol. 100, no. 11, p. 118703, 2008. + """ + import numpy as np + + if nodelist is None: + nodelist = list(G) + A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") + k_in = A.sum(axis=0) + k_out = A.sum(axis=1) + m = k_in.sum() + # Expected adjacency matrix + X = np.outer(k_out, k_in) / m + + return A - X diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/spectrum.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/spectrum.py new file mode 100644 index 0000000000000000000000000000000000000000..079b18550ca5b1478ec3f4b12ee40e5451627355 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/spectrum.py @@ -0,0 +1,186 @@ +""" +Eigenvalue spectrum of graphs. +""" + +import networkx as nx + +__all__ = [ + "laplacian_spectrum", + "adjacency_spectrum", + "modularity_spectrum", + "normalized_laplacian_spectrum", + "bethe_hessian_spectrum", +] + + +@nx._dispatchable(edge_attrs="weight") +def laplacian_spectrum(G, weight="weight"): + """Returns eigenvalues of the Laplacian of G + + Parameters + ---------- + G : graph + A NetworkX graph + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + evals : NumPy array + Eigenvalues + + Notes + ----- + For MultiGraph/MultiDiGraph, the edges weights are summed. + See :func:`~networkx.convert_matrix.to_numpy_array` for other options. + + See Also + -------- + laplacian_matrix + + Examples + -------- + The multiplicity of 0 as an eigenvalue of the laplacian matrix is equal + to the number of connected components of G. + + >>> G = nx.Graph() # Create a graph with 5 nodes and 3 connected components + >>> G.add_nodes_from(range(5)) + >>> G.add_edges_from([(0, 2), (3, 4)]) + >>> nx.laplacian_spectrum(G) + array([0., 0., 0., 2., 2.]) + + """ + import scipy as sp + + return sp.linalg.eigvalsh(nx.laplacian_matrix(G, weight=weight).todense()) + + +@nx._dispatchable(edge_attrs="weight") +def normalized_laplacian_spectrum(G, weight="weight"): + """Return eigenvalues of the normalized Laplacian of G + + Parameters + ---------- + G : graph + A NetworkX graph + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + evals : NumPy array + Eigenvalues + + Notes + ----- + For MultiGraph/MultiDiGraph, the edges weights are summed. + See to_numpy_array for other options. + + See Also + -------- + normalized_laplacian_matrix + """ + import scipy as sp + + return sp.linalg.eigvalsh( + nx.normalized_laplacian_matrix(G, weight=weight).todense() + ) + + +@nx._dispatchable(edge_attrs="weight") +def adjacency_spectrum(G, weight="weight"): + """Returns eigenvalues of the adjacency matrix of G. + + Parameters + ---------- + G : graph + A NetworkX graph + + weight : string or None, optional (default='weight') + The edge data key used to compute each value in the matrix. + If None, then each edge has weight 1. + + Returns + ------- + evals : NumPy array + Eigenvalues + + Notes + ----- + For MultiGraph/MultiDiGraph, the edges weights are summed. + See to_numpy_array for other options. + + See Also + -------- + adjacency_matrix + """ + import scipy as sp + + return sp.linalg.eigvals(nx.adjacency_matrix(G, weight=weight).todense()) + + +@nx._dispatchable +def modularity_spectrum(G): + """Returns eigenvalues of the modularity matrix of G. + + Parameters + ---------- + G : Graph + A NetworkX Graph or DiGraph + + Returns + ------- + evals : NumPy array + Eigenvalues + + See Also + -------- + modularity_matrix + + References + ---------- + .. [1] M. E. J. Newman, "Modularity and community structure in networks", + Proc. Natl. Acad. Sci. USA, vol. 103, pp. 8577-8582, 2006. + """ + import scipy as sp + + if G.is_directed(): + return sp.linalg.eigvals(nx.directed_modularity_matrix(G)) + else: + return sp.linalg.eigvals(nx.modularity_matrix(G)) + + +@nx._dispatchable +def bethe_hessian_spectrum(G, r=None): + """Returns eigenvalues of the Bethe Hessian matrix of G. + + Parameters + ---------- + G : Graph + A NetworkX Graph or DiGraph + + r : float + Regularizer parameter + + Returns + ------- + evals : NumPy array + Eigenvalues + + See Also + -------- + bethe_hessian_matrix + + References + ---------- + .. [1] A. Saade, F. Krzakala and L. Zdeborová + "Spectral clustering of graphs with the bethe hessian", + Advances in Neural Information Processing Systems. 2014. + """ + import scipy as sp + + return sp.linalg.eigvalsh(nx.bethe_hessian_matrix(G, r).todense()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ea3d0fd2b09f570786fd3d1cc2d9ea9b71197ea Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_algebraic_connectivity.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_algebraic_connectivity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c15f9c3564954a7821dedda353d5cdca9c93ff7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_algebraic_connectivity.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_attrmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_attrmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7853e1197189ee4e5cd536988e71fc91e85742e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_attrmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_bethehessian.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_bethehessian.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdea7b509c1f6602c0806c012197088694b80383 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_bethehessian.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_graphmatrix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_graphmatrix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7421052d0814da0af2dc4d9ba2071403a271197f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_graphmatrix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_laplacian.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_laplacian.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..698a96f297d4366c1b1b263968c1ef1d05c8ef96 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_laplacian.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_modularity.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_modularity.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99423b96b556a7c43a1d1bbbf04921220681b23d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_modularity.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_spectrum.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_spectrum.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..159b57370b77ec9145f9e490d8b41fd0d5e64609 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/__pycache__/test_spectrum.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..31f911f7a2b039cd523e930d5d552ad07c77dd4d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py @@ -0,0 +1,400 @@ +from math import sqrt + +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +methods = ("tracemin_pcg", "tracemin_lu", "lanczos", "lobpcg") + + +def test_algebraic_connectivity_tracemin_chol(): + """Test that "tracemin_chol" raises an exception.""" + pytest.importorskip("scipy") + G = nx.barbell_graph(5, 4) + with pytest.raises(nx.NetworkXError): + nx.algebraic_connectivity(G, method="tracemin_chol") + + +def test_fiedler_vector_tracemin_chol(): + """Test that "tracemin_chol" raises an exception.""" + pytest.importorskip("scipy") + G = nx.barbell_graph(5, 4) + with pytest.raises(nx.NetworkXError): + nx.fiedler_vector(G, method="tracemin_chol") + + +def test_spectral_ordering_tracemin_chol(): + """Test that "tracemin_chol" raises an exception.""" + pytest.importorskip("scipy") + G = nx.barbell_graph(5, 4) + with pytest.raises(nx.NetworkXError): + nx.spectral_ordering(G, method="tracemin_chol") + + +def test_fiedler_vector_tracemin_unknown(): + """Test that "tracemin_unknown" raises an exception.""" + pytest.importorskip("scipy") + G = nx.barbell_graph(5, 4) + L = nx.laplacian_matrix(G) + X = np.asarray(np.random.normal(size=(1, L.shape[0]))).T + with pytest.raises(nx.NetworkXError, match="Unknown linear system solver"): + nx.linalg.algebraicconnectivity._tracemin_fiedler( + L, X, normalized=False, tol=1e-8, method="tracemin_unknown" + ) + + +def test_spectral_bisection(): + pytest.importorskip("scipy") + G = nx.barbell_graph(3, 0) + C = nx.spectral_bisection(G) + assert C == ({0, 1, 2}, {3, 4, 5}) + + mapping = dict(enumerate("badfec")) + G = nx.relabel_nodes(G, mapping) + C = nx.spectral_bisection(G) + assert C == ( + {mapping[0], mapping[1], mapping[2]}, + {mapping[3], mapping[4], mapping[5]}, + ) + + +def check_eigenvector(A, l, x): + nx = np.linalg.norm(x) + # Check zeroness. + assert nx != pytest.approx(0, abs=1e-07) + y = A @ x + ny = np.linalg.norm(y) + # Check collinearity. + assert x @ y == pytest.approx(nx * ny, abs=1e-7) + # Check eigenvalue. + assert ny == pytest.approx(l * nx, abs=1e-7) + + +class TestAlgebraicConnectivity: + @pytest.mark.parametrize("method", methods) + def test_directed(self, method): + G = nx.DiGraph() + pytest.raises( + nx.NetworkXNotImplemented, nx.algebraic_connectivity, G, method=method + ) + pytest.raises(nx.NetworkXNotImplemented, nx.fiedler_vector, G, method=method) + + @pytest.mark.parametrize("method", methods) + def test_null_and_singleton(self, method): + G = nx.Graph() + pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method=method) + pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method) + G.add_edge(0, 0) + pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method=method) + pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method) + + @pytest.mark.parametrize("method", methods) + def test_disconnected(self, method): + G = nx.Graph() + G.add_nodes_from(range(2)) + assert nx.algebraic_connectivity(G) == 0 + pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method) + G.add_edge(0, 1, weight=0) + assert nx.algebraic_connectivity(G) == 0 + pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method) + + def test_unrecognized_method(self): + pytest.importorskip("scipy") + G = nx.path_graph(4) + pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method="unknown") + pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method="unknown") + + @pytest.mark.parametrize("method", methods) + def test_two_nodes(self, method): + pytest.importorskip("scipy") + G = nx.Graph() + G.add_edge(0, 1, weight=1) + A = nx.laplacian_matrix(G) + assert nx.algebraic_connectivity(G, tol=1e-12, method=method) == pytest.approx( + 2, abs=1e-7 + ) + x = nx.fiedler_vector(G, tol=1e-12, method=method) + check_eigenvector(A, 2, x) + + @pytest.mark.parametrize("method", methods) + def test_two_nodes_multigraph(self, method): + pytest.importorskip("scipy") + G = nx.MultiGraph() + G.add_edge(0, 0, spam=1e8) + G.add_edge(0, 1, spam=1) + G.add_edge(0, 1, spam=-2) + A = -3 * nx.laplacian_matrix(G, weight="spam") + assert nx.algebraic_connectivity( + G, weight="spam", tol=1e-12, method=method + ) == pytest.approx(6, abs=1e-7) + x = nx.fiedler_vector(G, weight="spam", tol=1e-12, method=method) + check_eigenvector(A, 6, x) + + def test_abbreviation_of_method(self): + pytest.importorskip("scipy") + G = nx.path_graph(8) + A = nx.laplacian_matrix(G) + sigma = 2 - sqrt(2 + sqrt(2)) + ac = nx.algebraic_connectivity(G, tol=1e-12, method="tracemin") + assert ac == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, tol=1e-12, method="tracemin") + check_eigenvector(A, sigma, x) + + @pytest.mark.parametrize("method", methods) + def test_path(self, method): + pytest.importorskip("scipy") + G = nx.path_graph(8) + A = nx.laplacian_matrix(G) + sigma = 2 - sqrt(2 + sqrt(2)) + ac = nx.algebraic_connectivity(G, tol=1e-12, method=method) + assert ac == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, tol=1e-12, method=method) + check_eigenvector(A, sigma, x) + + @pytest.mark.parametrize("method", methods) + def test_problematic_graph_issue_2381(self, method): + pytest.importorskip("scipy") + G = nx.path_graph(4) + G.add_edges_from([(4, 2), (5, 1)]) + A = nx.laplacian_matrix(G) + sigma = 0.438447187191 + ac = nx.algebraic_connectivity(G, tol=1e-12, method=method) + assert ac == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, tol=1e-12, method=method) + check_eigenvector(A, sigma, x) + + @pytest.mark.parametrize("method", methods) + def test_cycle(self, method): + pytest.importorskip("scipy") + G = nx.cycle_graph(8) + A = nx.laplacian_matrix(G) + sigma = 2 - sqrt(2) + ac = nx.algebraic_connectivity(G, tol=1e-12, method=method) + assert ac == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, tol=1e-12, method=method) + check_eigenvector(A, sigma, x) + + @pytest.mark.parametrize("method", methods) + def test_seed_argument(self, method): + pytest.importorskip("scipy") + G = nx.cycle_graph(8) + A = nx.laplacian_matrix(G) + sigma = 2 - sqrt(2) + ac = nx.algebraic_connectivity(G, tol=1e-12, method=method, seed=1) + assert ac == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, tol=1e-12, method=method, seed=1) + check_eigenvector(A, sigma, x) + + @pytest.mark.parametrize( + ("normalized", "sigma", "laplacian_fn"), + ( + (False, 0.2434017461399311, nx.laplacian_matrix), + (True, 0.08113391537997749, nx.normalized_laplacian_matrix), + ), + ) + @pytest.mark.parametrize("method", methods) + def test_buckminsterfullerene(self, normalized, sigma, laplacian_fn, method): + pytest.importorskip("scipy") + G = nx.Graph( + [ + (1, 10), + (1, 41), + (1, 59), + (2, 12), + (2, 42), + (2, 60), + (3, 6), + (3, 43), + (3, 57), + (4, 8), + (4, 44), + (4, 58), + (5, 13), + (5, 56), + (5, 57), + (6, 10), + (6, 31), + (7, 14), + (7, 56), + (7, 58), + (8, 12), + (8, 32), + (9, 23), + (9, 53), + (9, 59), + (10, 15), + (11, 24), + (11, 53), + (11, 60), + (12, 16), + (13, 14), + (13, 25), + (14, 26), + (15, 27), + (15, 49), + (16, 28), + (16, 50), + (17, 18), + (17, 19), + (17, 54), + (18, 20), + (18, 55), + (19, 23), + (19, 41), + (20, 24), + (20, 42), + (21, 31), + (21, 33), + (21, 57), + (22, 32), + (22, 34), + (22, 58), + (23, 24), + (25, 35), + (25, 43), + (26, 36), + (26, 44), + (27, 51), + (27, 59), + (28, 52), + (28, 60), + (29, 33), + (29, 34), + (29, 56), + (30, 51), + (30, 52), + (30, 53), + (31, 47), + (32, 48), + (33, 45), + (34, 46), + (35, 36), + (35, 37), + (36, 38), + (37, 39), + (37, 49), + (38, 40), + (38, 50), + (39, 40), + (39, 51), + (40, 52), + (41, 47), + (42, 48), + (43, 49), + (44, 50), + (45, 46), + (45, 54), + (46, 55), + (47, 54), + (48, 55), + ] + ) + A = laplacian_fn(G) + try: + assert nx.algebraic_connectivity( + G, normalized=normalized, tol=1e-12, method=method + ) == pytest.approx(sigma, abs=1e-7) + x = nx.fiedler_vector(G, normalized=normalized, tol=1e-12, method=method) + check_eigenvector(A, sigma, x) + except nx.NetworkXError as err: + if err.args not in ( + ("Cholesky solver unavailable.",), + ("LU solver unavailable.",), + ): + raise + + +class TestSpectralOrdering: + _graphs = (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph) + + @pytest.mark.parametrize("graph", _graphs) + def test_nullgraph(self, graph): + G = graph() + pytest.raises(nx.NetworkXError, nx.spectral_ordering, G) + + @pytest.mark.parametrize("graph", _graphs) + def test_singleton(self, graph): + G = graph() + G.add_node("x") + assert nx.spectral_ordering(G) == ["x"] + G.add_edge("x", "x", weight=33) + G.add_edge("x", "x", weight=33) + assert nx.spectral_ordering(G) == ["x"] + + def test_unrecognized_method(self): + G = nx.path_graph(4) + pytest.raises(nx.NetworkXError, nx.spectral_ordering, G, method="unknown") + + @pytest.mark.parametrize("method", methods) + def test_three_nodes(self, method): + pytest.importorskip("scipy") + G = nx.Graph() + G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2), (2, 3, 1)], weight="spam") + order = nx.spectral_ordering(G, weight="spam", method=method) + assert set(order) == set(G) + assert {1, 3} in (set(order[:-1]), set(order[1:])) + + @pytest.mark.parametrize("method", methods) + def test_three_nodes_multigraph(self, method): + pytest.importorskip("scipy") + G = nx.MultiDiGraph() + G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2), (2, 3, 1), (2, 3, 2)]) + order = nx.spectral_ordering(G, method=method) + assert set(order) == set(G) + assert {2, 3} in (set(order[:-1]), set(order[1:])) + + @pytest.mark.parametrize("method", methods) + def test_path(self, method): + pytest.importorskip("scipy") + path = list(range(10)) + np.random.shuffle(path) + G = nx.Graph() + nx.add_path(G, path) + order = nx.spectral_ordering(G, method=method) + assert order in [path, list(reversed(path))] + + @pytest.mark.parametrize("method", methods) + def test_seed_argument(self, method): + pytest.importorskip("scipy") + path = list(range(10)) + np.random.shuffle(path) + G = nx.Graph() + nx.add_path(G, path) + order = nx.spectral_ordering(G, method=method, seed=1) + assert order in [path, list(reversed(path))] + + @pytest.mark.parametrize("method", methods) + def test_disconnected(self, method): + pytest.importorskip("scipy") + G = nx.Graph() + nx.add_path(G, range(0, 10, 2)) + nx.add_path(G, range(1, 10, 2)) + order = nx.spectral_ordering(G, method=method) + assert set(order) == set(G) + seqs = [ + list(range(0, 10, 2)), + list(range(8, -1, -2)), + list(range(1, 10, 2)), + list(range(9, -1, -2)), + ] + assert order[:5] in seqs + assert order[5:] in seqs + + @pytest.mark.parametrize( + ("normalized", "expected_order"), + ( + (False, [[1, 2, 0, 3, 4, 5, 6, 9, 7, 8], [8, 7, 9, 6, 5, 4, 3, 0, 2, 1]]), + (True, [[1, 2, 3, 0, 4, 5, 9, 6, 7, 8], [8, 7, 6, 9, 5, 4, 0, 3, 2, 1]]), + ), + ) + @pytest.mark.parametrize("method", methods) + def test_cycle(self, normalized, expected_order, method): + pytest.importorskip("scipy") + path = list(range(10)) + G = nx.Graph() + nx.add_path(G, path, weight=5) + G.add_edge(path[-1], path[0], weight=1) + A = nx.laplacian_matrix(G).todense() + order = nx.spectral_ordering(G, normalized=normalized, method=method) + assert order in expected_order diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..05a3b94f3a727715f3d0b9ea67af8d44676c9170 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py @@ -0,0 +1,108 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") + + +def test_attr_matrix(): + G = nx.Graph() + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 2, thickness=2) + G.add_edge(1, 2, thickness=3) + + def node_attr(u): + return G.nodes[u].get("size", 0.5) * 3 + + def edge_attr(u, v): + return G[u][v].get("thickness", 0.5) + + M = nx.attr_matrix(G, edge_attr=edge_attr, node_attr=node_attr) + np.testing.assert_equal(M[0], np.array([[6.0]])) + assert M[1] == [1.5] + + +def test_attr_matrix_directed(): + G = nx.DiGraph() + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 2, thickness=2) + G.add_edge(1, 2, thickness=3) + M = nx.attr_matrix(G, rc_order=[0, 1, 2]) + # fmt: off + data = np.array( + [[0., 1., 1.], + [0., 0., 1.], + [0., 0., 0.]] + ) + # fmt: on + np.testing.assert_equal(M, np.array(data)) + + +def test_attr_matrix_multigraph(): + G = nx.MultiGraph() + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 2, thickness=2) + G.add_edge(1, 2, thickness=3) + M = nx.attr_matrix(G, rc_order=[0, 1, 2]) + # fmt: off + data = np.array( + [[0., 3., 1.], + [3., 0., 1.], + [1., 1., 0.]] + ) + # fmt: on + np.testing.assert_equal(M, np.array(data)) + M = nx.attr_matrix(G, edge_attr="weight", rc_order=[0, 1, 2]) + # fmt: off + data = np.array( + [[0., 9., 1.], + [9., 0., 1.], + [1., 1., 0.]] + ) + # fmt: on + np.testing.assert_equal(M, np.array(data)) + M = nx.attr_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2]) + # fmt: off + data = np.array( + [[0., 3., 2.], + [3., 0., 3.], + [2., 3., 0.]] + ) + # fmt: on + np.testing.assert_equal(M, np.array(data)) + + +def test_attr_sparse_matrix(): + pytest.importorskip("scipy") + G = nx.Graph() + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 2, thickness=2) + G.add_edge(1, 2, thickness=3) + M = nx.attr_sparse_matrix(G) + mtx = M[0] + data = np.ones((3, 3), float) + np.fill_diagonal(data, 0) + np.testing.assert_equal(mtx.todense(), np.array(data)) + assert M[1] == [0, 1, 2] + + +def test_attr_sparse_matrix_directed(): + pytest.importorskip("scipy") + G = nx.DiGraph() + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 1, thickness=1, weight=3) + G.add_edge(0, 2, thickness=2) + G.add_edge(1, 2, thickness=3) + M = nx.attr_sparse_matrix(G, rc_order=[0, 1, 2]) + # fmt: off + data = np.array( + [[0., 1., 1.], + [0., 0., 1.], + [0., 0., 0.]] + ) + # fmt: on + np.testing.assert_equal(M.todense(), np.array(data)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py new file mode 100644 index 0000000000000000000000000000000000000000..92b745b882ad2eed6dea2c87b98bfcd87938a233 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py @@ -0,0 +1,40 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +class TestBetheHessian: + @classmethod + def setup_class(cls): + deg = [3, 2, 2, 1, 0] + cls.G = nx.havel_hakimi_graph(deg) + cls.P = nx.path_graph(3) + + def test_bethe_hessian(self): + "Bethe Hessian matrix" + # fmt: off + H = np.array([[4, -2, 0], + [-2, 5, -2], + [0, -2, 4]]) + # fmt: on + permutation = [2, 0, 1] + # Bethe Hessian gives expected form + np.testing.assert_equal(nx.bethe_hessian_matrix(self.P, r=2).todense(), H) + # nodelist is correctly implemented + np.testing.assert_equal( + nx.bethe_hessian_matrix(self.P, r=2, nodelist=permutation).todense(), + H[np.ix_(permutation, permutation)], + ) + # Equal to Laplacian matrix when r=1 + np.testing.assert_equal( + nx.bethe_hessian_matrix(self.G, r=1).todense(), + nx.laplacian_matrix(self.G).todense(), + ) + # Correct default for the regularizer r + np.testing.assert_equal( + nx.bethe_hessian_matrix(self.G).todense(), + nx.bethe_hessian_matrix(self.G, r=1.25).todense(), + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py new file mode 100644 index 0000000000000000000000000000000000000000..d84a397d81caa15ab3f0ecbbf6eb6afeee82cbc2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py @@ -0,0 +1,275 @@ +import pytest + +import networkx as nx +from networkx.exception import NetworkXError + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +def test_incidence_matrix_simple(): + deg = [3, 2, 2, 1, 0] + G = nx.havel_hakimi_graph(deg) + deg = [(1, 0), (1, 0), (1, 0), (2, 0), (1, 0), (2, 1), (0, 1), (0, 1)] + MG = nx.random_clustered_graph(deg, seed=42) + + I = nx.incidence_matrix(G, dtype=int).todense() + # fmt: off + expected = np.array( + [[1, 1, 1, 0], + [0, 1, 0, 1], + [1, 0, 0, 1], + [0, 0, 1, 0], + [0, 0, 0, 0]] + ) + # fmt: on + np.testing.assert_equal(I, expected) + + I = nx.incidence_matrix(MG, dtype=int).todense() + # fmt: off + expected = np.array( + [[1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 1, 0, 1]] + ) + # fmt: on + np.testing.assert_equal(I, expected) + + with pytest.raises(NetworkXError): + nx.incidence_matrix(G, nodelist=[0, 1]) + + +class TestGraphMatrix: + @classmethod + def setup_class(cls): + deg = [3, 2, 2, 1, 0] + cls.G = nx.havel_hakimi_graph(deg) + # fmt: off + cls.OI = np.array( + [[-1, -1, -1, 0], + [1, 0, 0, -1], + [0, 1, 0, 1], + [0, 0, 1, 0], + [0, 0, 0, 0]] + ) + cls.A = np.array( + [[0, 1, 1, 1, 0], + [1, 0, 1, 0, 0], + [1, 1, 0, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 0, 0, 0]] + ) + # fmt: on + cls.WG = nx.havel_hakimi_graph(deg) + cls.WG.add_edges_from( + (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges() + ) + # fmt: off + cls.WA = np.array( + [[0, 0.5, 0.5, 0.5, 0], + [0.5, 0, 0.5, 0, 0], + [0.5, 0.5, 0, 0, 0], + [0.5, 0, 0, 0, 0], + [0, 0, 0, 0, 0]] + ) + # fmt: on + cls.MG = nx.MultiGraph(cls.G) + cls.MG2 = cls.MG.copy() + cls.MG2.add_edge(0, 1) + # fmt: off + cls.MG2A = np.array( + [[0, 2, 1, 1, 0], + [2, 0, 1, 0, 0], + [1, 1, 0, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 0, 0, 0]] + ) + cls.MGOI = np.array( + [[-1, -1, -1, -1, 0], + [1, 1, 0, 0, -1], + [0, 0, 1, 0, 1], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 0]] + ) + # fmt: on + cls.no_edges_G = nx.Graph([(1, 2), (3, 2, {"weight": 8})]) + cls.no_edges_A = np.array([[0, 0], [0, 0]]) + + def test_incidence_matrix(self): + "Conversion to incidence matrix" + I = nx.incidence_matrix( + self.G, + nodelist=sorted(self.G), + edgelist=sorted(self.G.edges()), + oriented=True, + dtype=int, + ).todense() + np.testing.assert_equal(I, self.OI) + + I = nx.incidence_matrix( + self.G, + nodelist=sorted(self.G), + edgelist=sorted(self.G.edges()), + oriented=False, + dtype=int, + ).todense() + np.testing.assert_equal(I, np.abs(self.OI)) + + I = nx.incidence_matrix( + self.MG, + nodelist=sorted(self.MG), + edgelist=sorted(self.MG.edges()), + oriented=True, + dtype=int, + ).todense() + np.testing.assert_equal(I, self.OI) + + I = nx.incidence_matrix( + self.MG, + nodelist=sorted(self.MG), + edgelist=sorted(self.MG.edges()), + oriented=False, + dtype=int, + ).todense() + np.testing.assert_equal(I, np.abs(self.OI)) + + I = nx.incidence_matrix( + self.MG2, + nodelist=sorted(self.MG2), + edgelist=sorted(self.MG2.edges()), + oriented=True, + dtype=int, + ).todense() + np.testing.assert_equal(I, self.MGOI) + + I = nx.incidence_matrix( + self.MG2, + nodelist=sorted(self.MG), + edgelist=sorted(self.MG2.edges()), + oriented=False, + dtype=int, + ).todense() + np.testing.assert_equal(I, np.abs(self.MGOI)) + + I = nx.incidence_matrix(self.G, dtype=np.uint8) + assert I.dtype == np.uint8 + + def test_weighted_incidence_matrix(self): + I = nx.incidence_matrix( + self.WG, + nodelist=sorted(self.WG), + edgelist=sorted(self.WG.edges()), + oriented=True, + dtype=int, + ).todense() + np.testing.assert_equal(I, self.OI) + + I = nx.incidence_matrix( + self.WG, + nodelist=sorted(self.WG), + edgelist=sorted(self.WG.edges()), + oriented=False, + dtype=int, + ).todense() + np.testing.assert_equal(I, np.abs(self.OI)) + + # np.testing.assert_equal(nx.incidence_matrix(self.WG,oriented=True, + # weight='weight').todense(),0.5*self.OI) + # np.testing.assert_equal(nx.incidence_matrix(self.WG,weight='weight').todense(), + # np.abs(0.5*self.OI)) + # np.testing.assert_equal(nx.incidence_matrix(self.WG,oriented=True,weight='other').todense(), + # 0.3*self.OI) + + I = nx.incidence_matrix( + self.WG, + nodelist=sorted(self.WG), + edgelist=sorted(self.WG.edges()), + oriented=True, + weight="weight", + ).todense() + np.testing.assert_equal(I, 0.5 * self.OI) + + I = nx.incidence_matrix( + self.WG, + nodelist=sorted(self.WG), + edgelist=sorted(self.WG.edges()), + oriented=False, + weight="weight", + ).todense() + np.testing.assert_equal(I, np.abs(0.5 * self.OI)) + + I = nx.incidence_matrix( + self.WG, + nodelist=sorted(self.WG), + edgelist=sorted(self.WG.edges()), + oriented=True, + weight="other", + ).todense() + np.testing.assert_equal(I, 0.3 * self.OI) + + # WMG=nx.MultiGraph(self.WG) + # WMG.add_edge(0,1,weight=0.5,other=0.3) + # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='weight').todense(), + # np.abs(0.5*self.MGOI)) + # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='weight',oriented=True).todense(), + # 0.5*self.MGOI) + # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='other',oriented=True).todense(), + # 0.3*self.MGOI) + + WMG = nx.MultiGraph(self.WG) + WMG.add_edge(0, 1, weight=0.5, other=0.3) + + I = nx.incidence_matrix( + WMG, + nodelist=sorted(WMG), + edgelist=sorted(WMG.edges(keys=True)), + oriented=True, + weight="weight", + ).todense() + np.testing.assert_equal(I, 0.5 * self.MGOI) + + I = nx.incidence_matrix( + WMG, + nodelist=sorted(WMG), + edgelist=sorted(WMG.edges(keys=True)), + oriented=False, + weight="weight", + ).todense() + np.testing.assert_equal(I, np.abs(0.5 * self.MGOI)) + + I = nx.incidence_matrix( + WMG, + nodelist=sorted(WMG), + edgelist=sorted(WMG.edges(keys=True)), + oriented=True, + weight="other", + ).todense() + np.testing.assert_equal(I, 0.3 * self.MGOI) + + def test_adjacency_matrix(self): + "Conversion to adjacency matrix" + np.testing.assert_equal(nx.adjacency_matrix(self.G).todense(), self.A) + np.testing.assert_equal(nx.adjacency_matrix(self.MG).todense(), self.A) + np.testing.assert_equal(nx.adjacency_matrix(self.MG2).todense(), self.MG2A) + np.testing.assert_equal( + nx.adjacency_matrix(self.G, nodelist=[0, 1]).todense(), self.A[:2, :2] + ) + np.testing.assert_equal(nx.adjacency_matrix(self.WG).todense(), self.WA) + np.testing.assert_equal( + nx.adjacency_matrix(self.WG, weight=None).todense(), self.A + ) + np.testing.assert_equal( + nx.adjacency_matrix(self.MG2, weight=None).todense(), self.MG2A + ) + np.testing.assert_equal( + nx.adjacency_matrix(self.WG, weight="other").todense(), 0.6 * self.WA + ) + np.testing.assert_equal( + nx.adjacency_matrix(self.no_edges_G, nodelist=[1, 3]).todense(), + self.no_edges_A, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py new file mode 100644 index 0000000000000000000000000000000000000000..b1d5c13e8d676c6b9d3e87f148051b7bccf9da30 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py @@ -0,0 +1,334 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +class TestLaplacian: + @classmethod + def setup_class(cls): + deg = [3, 2, 2, 1, 0] + cls.G = nx.havel_hakimi_graph(deg) + cls.WG = nx.Graph( + (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges() + ) + cls.WG.add_node(4) + cls.MG = nx.MultiGraph(cls.G) + + # Graph with clsloops + cls.Gsl = cls.G.copy() + for node in cls.Gsl.nodes(): + cls.Gsl.add_edge(node, node) + + # Graph used as an example in Sec. 4.1 of Langville and Meyer, + # "Google's PageRank and Beyond". + cls.DiG = nx.DiGraph() + cls.DiG.add_edges_from( + ( + (1, 2), + (1, 3), + (3, 1), + (3, 2), + (3, 5), + (4, 5), + (4, 6), + (5, 4), + (5, 6), + (6, 4), + ) + ) + cls.DiMG = nx.MultiDiGraph(cls.DiG) + cls.DiWG = nx.DiGraph( + (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.DiG.edges() + ) + cls.DiGsl = cls.DiG.copy() + for node in cls.DiGsl.nodes(): + cls.DiGsl.add_edge(node, node) + + def test_laplacian(self): + "Graph Laplacian" + # fmt: off + NL = np.array([[ 3, -1, -1, -1, 0], + [-1, 2, -1, 0, 0], + [-1, -1, 2, 0, 0], + [-1, 0, 0, 1, 0], + [ 0, 0, 0, 0, 0]]) + # fmt: on + WL = 0.5 * NL + OL = 0.3 * NL + # fmt: off + DiNL = np.array([[ 2, -1, -1, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0], + [-1, -1, 3, -1, 0, 0], + [ 0, 0, 0, 2, -1, -1], + [ 0, 0, 0, -1, 2, -1], + [ 0, 0, 0, 0, -1, 1]]) + # fmt: on + DiWL = 0.5 * DiNL + DiOL = 0.3 * DiNL + np.testing.assert_equal(nx.laplacian_matrix(self.G).todense(), NL) + np.testing.assert_equal(nx.laplacian_matrix(self.MG).todense(), NL) + np.testing.assert_equal( + nx.laplacian_matrix(self.G, nodelist=[0, 1]).todense(), + np.array([[1, -1], [-1, 1]]), + ) + np.testing.assert_equal(nx.laplacian_matrix(self.WG).todense(), WL) + np.testing.assert_equal(nx.laplacian_matrix(self.WG, weight=None).todense(), NL) + np.testing.assert_equal( + nx.laplacian_matrix(self.WG, weight="other").todense(), OL + ) + + np.testing.assert_equal(nx.laplacian_matrix(self.DiG).todense(), DiNL) + np.testing.assert_equal(nx.laplacian_matrix(self.DiMG).todense(), DiNL) + np.testing.assert_equal( + nx.laplacian_matrix(self.DiG, nodelist=[1, 2]).todense(), + np.array([[1, -1], [0, 0]]), + ) + np.testing.assert_equal(nx.laplacian_matrix(self.DiWG).todense(), DiWL) + np.testing.assert_equal( + nx.laplacian_matrix(self.DiWG, weight=None).todense(), DiNL + ) + np.testing.assert_equal( + nx.laplacian_matrix(self.DiWG, weight="other").todense(), DiOL + ) + + def test_normalized_laplacian(self): + "Generalized Graph Laplacian" + # fmt: off + G = np.array([[ 1. , -0.408, -0.408, -0.577, 0.], + [-0.408, 1. , -0.5 , 0. , 0.], + [-0.408, -0.5 , 1. , 0. , 0.], + [-0.577, 0. , 0. , 1. , 0.], + [ 0. , 0. , 0. , 0. , 0.]]) + GL = np.array([[ 1. , -0.408, -0.408, -0.577, 0. ], + [-0.408, 1. , -0.5 , 0. , 0. ], + [-0.408, -0.5 , 1. , 0. , 0. ], + [-0.577, 0. , 0. , 1. , 0. ], + [ 0. , 0. , 0. , 0. , 0. ]]) + Lsl = np.array([[ 0.75 , -0.2887, -0.2887, -0.3536, 0. ], + [-0.2887, 0.6667, -0.3333, 0. , 0. ], + [-0.2887, -0.3333, 0.6667, 0. , 0. ], + [-0.3536, 0. , 0. , 0.5 , 0. ], + [ 0. , 0. , 0. , 0. , 0. ]]) + + DiG = np.array([[ 1. , 0. , -0.4082, 0. , 0. , 0. ], + [ 0. , 0. , 0. , 0. , 0. , 0. ], + [-0.4082, 0. , 1. , 0. , -0.4082, 0. ], + [ 0. , 0. , 0. , 1. , -0.5 , -0.7071], + [ 0. , 0. , 0. , -0.5 , 1. , -0.7071], + [ 0. , 0. , 0. , -0.7071, 0. , 1. ]]) + DiGL = np.array([[ 1. , 0. , -0.4082, 0. , 0. , 0. ], + [ 0. , 0. , 0. , 0. , 0. , 0. ], + [-0.4082, 0. , 1. , -0.4082, 0. , 0. ], + [ 0. , 0. , 0. , 1. , -0.5 , -0.7071], + [ 0. , 0. , 0. , -0.5 , 1. , -0.7071], + [ 0. , 0. , 0. , 0. , -0.7071, 1. ]]) + DiLsl = np.array([[ 0.6667, -0.5774, -0.2887, 0. , 0. , 0. ], + [ 0. , 0. , 0. , 0. , 0. , 0. ], + [-0.2887, -0.5 , 0.75 , -0.2887, 0. , 0. ], + [ 0. , 0. , 0. , 0.6667, -0.3333, -0.4082], + [ 0. , 0. , 0. , -0.3333, 0.6667, -0.4082], + [ 0. , 0. , 0. , 0. , -0.4082, 0.5 ]]) + # fmt: on + + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.G, nodelist=range(5)).todense(), + G, + decimal=3, + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.G).todense(), GL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.MG).todense(), GL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.WG).todense(), GL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.WG, weight="other").todense(), + GL, + decimal=3, + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.Gsl).todense(), Lsl, decimal=3 + ) + + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix( + self.DiG, + nodelist=range(1, 1 + 6), + ).todense(), + DiG, + decimal=3, + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.DiG).todense(), DiGL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.DiMG).todense(), DiGL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.DiWG).todense(), DiGL, decimal=3 + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.DiWG, weight="other").todense(), + DiGL, + decimal=3, + ) + np.testing.assert_almost_equal( + nx.normalized_laplacian_matrix(self.DiGsl).todense(), DiLsl, decimal=3 + ) + + +def test_directed_laplacian(): + "Directed Laplacian" + # Graph used as an example in Sec. 4.1 of Langville and Meyer, + # "Google's PageRank and Beyond". The graph contains dangling nodes, so + # the pagerank random walk is selected by directed_laplacian + G = nx.DiGraph() + G.add_edges_from( + ( + (1, 2), + (1, 3), + (3, 1), + (3, 2), + (3, 5), + (4, 5), + (4, 6), + (5, 4), + (5, 6), + (6, 4), + ) + ) + # fmt: off + GL = np.array([[ 0.9833, -0.2941, -0.3882, -0.0291, -0.0231, -0.0261], + [-0.2941, 0.8333, -0.2339, -0.0536, -0.0589, -0.0554], + [-0.3882, -0.2339, 0.9833, -0.0278, -0.0896, -0.0251], + [-0.0291, -0.0536, -0.0278, 0.9833, -0.4878, -0.6675], + [-0.0231, -0.0589, -0.0896, -0.4878, 0.9833, -0.2078], + [-0.0261, -0.0554, -0.0251, -0.6675, -0.2078, 0.9833]]) + # fmt: on + L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G)) + np.testing.assert_almost_equal(L, GL, decimal=3) + + # Make the graph strongly connected, so we can use a random and lazy walk + G.add_edges_from(((2, 5), (6, 1))) + # fmt: off + GL = np.array([[ 1. , -0.3062, -0.4714, 0. , 0. , -0.3227], + [-0.3062, 1. , -0.1443, 0. , -0.3162, 0. ], + [-0.4714, -0.1443, 1. , 0. , -0.0913, 0. ], + [ 0. , 0. , 0. , 1. , -0.5 , -0.5 ], + [ 0. , -0.3162, -0.0913, -0.5 , 1. , -0.25 ], + [-0.3227, 0. , 0. , -0.5 , -0.25 , 1. ]]) + # fmt: on + L = nx.directed_laplacian_matrix( + G, alpha=0.9, nodelist=sorted(G), walk_type="random" + ) + np.testing.assert_almost_equal(L, GL, decimal=3) + + # fmt: off + GL = np.array([[ 0.5 , -0.1531, -0.2357, 0. , 0. , -0.1614], + [-0.1531, 0.5 , -0.0722, 0. , -0.1581, 0. ], + [-0.2357, -0.0722, 0.5 , 0. , -0.0456, 0. ], + [ 0. , 0. , 0. , 0.5 , -0.25 , -0.25 ], + [ 0. , -0.1581, -0.0456, -0.25 , 0.5 , -0.125 ], + [-0.1614, 0. , 0. , -0.25 , -0.125 , 0.5 ]]) + # fmt: on + L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G), walk_type="lazy") + np.testing.assert_almost_equal(L, GL, decimal=3) + + # Make a strongly connected periodic graph + G = nx.DiGraph() + G.add_edges_from(((1, 2), (2, 4), (4, 1), (1, 3), (3, 4))) + # fmt: off + GL = np.array([[ 0.5 , -0.176, -0.176, -0.25 ], + [-0.176, 0.5 , 0. , -0.176], + [-0.176, 0. , 0.5 , -0.176], + [-0.25 , -0.176, -0.176, 0.5 ]]) + # fmt: on + L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G)) + np.testing.assert_almost_equal(L, GL, decimal=3) + + +def test_directed_combinatorial_laplacian(): + "Directed combinatorial Laplacian" + # Graph used as an example in Sec. 4.1 of Langville and Meyer, + # "Google's PageRank and Beyond". The graph contains dangling nodes, so + # the pagerank random walk is selected by directed_laplacian + G = nx.DiGraph() + G.add_edges_from( + ( + (1, 2), + (1, 3), + (3, 1), + (3, 2), + (3, 5), + (4, 5), + (4, 6), + (5, 4), + (5, 6), + (6, 4), + ) + ) + # fmt: off + GL = np.array([[ 0.0366, -0.0132, -0.0153, -0.0034, -0.0020, -0.0027], + [-0.0132, 0.0450, -0.0111, -0.0076, -0.0062, -0.0069], + [-0.0153, -0.0111, 0.0408, -0.0035, -0.0083, -0.0027], + [-0.0034, -0.0076, -0.0035, 0.3688, -0.1356, -0.2187], + [-0.0020, -0.0062, -0.0083, -0.1356, 0.2026, -0.0505], + [-0.0027, -0.0069, -0.0027, -0.2187, -0.0505, 0.2815]]) + # fmt: on + + L = nx.directed_combinatorial_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G)) + np.testing.assert_almost_equal(L, GL, decimal=3) + + # Make the graph strongly connected, so we can use a random and lazy walk + G.add_edges_from(((2, 5), (6, 1))) + + # fmt: off + GL = np.array([[ 0.1395, -0.0349, -0.0465, 0. , 0. , -0.0581], + [-0.0349, 0.093 , -0.0116, 0. , -0.0465, 0. ], + [-0.0465, -0.0116, 0.0698, 0. , -0.0116, 0. ], + [ 0. , 0. , 0. , 0.2326, -0.1163, -0.1163], + [ 0. , -0.0465, -0.0116, -0.1163, 0.2326, -0.0581], + [-0.0581, 0. , 0. , -0.1163, -0.0581, 0.2326]]) + # fmt: on + + L = nx.directed_combinatorial_laplacian_matrix( + G, alpha=0.9, nodelist=sorted(G), walk_type="random" + ) + np.testing.assert_almost_equal(L, GL, decimal=3) + + # fmt: off + GL = np.array([[ 0.0698, -0.0174, -0.0233, 0. , 0. , -0.0291], + [-0.0174, 0.0465, -0.0058, 0. , -0.0233, 0. ], + [-0.0233, -0.0058, 0.0349, 0. , -0.0058, 0. ], + [ 0. , 0. , 0. , 0.1163, -0.0581, -0.0581], + [ 0. , -0.0233, -0.0058, -0.0581, 0.1163, -0.0291], + [-0.0291, 0. , 0. , -0.0581, -0.0291, 0.1163]]) + # fmt: on + + L = nx.directed_combinatorial_laplacian_matrix( + G, alpha=0.9, nodelist=sorted(G), walk_type="lazy" + ) + np.testing.assert_almost_equal(L, GL, decimal=3) + + E = nx.DiGraph(nx.margulis_gabber_galil_graph(2)) + L = nx.directed_combinatorial_laplacian_matrix(E) + # fmt: off + expected = np.array( + [[ 0.16666667, -0.08333333, -0.08333333, 0. ], + [-0.08333333, 0.16666667, 0. , -0.08333333], + [-0.08333333, 0. , 0.16666667, -0.08333333], + [ 0. , -0.08333333, -0.08333333, 0.16666667]] + ) + # fmt: on + np.testing.assert_almost_equal(L, expected, decimal=6) + + with pytest.raises(nx.NetworkXError): + nx.directed_combinatorial_laplacian_matrix(G, walk_type="pagerank", alpha=100) + with pytest.raises(nx.NetworkXError): + nx.directed_combinatorial_laplacian_matrix(G, walk_type="silly") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py new file mode 100644 index 0000000000000000000000000000000000000000..48dda0063e127bcaa3c47dc05089dd858623f4c1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py @@ -0,0 +1,86 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +class TestModularity: + @classmethod + def setup_class(cls): + deg = [3, 2, 2, 1, 0] + cls.G = nx.havel_hakimi_graph(deg) + # Graph used as an example in Sec. 4.1 of Langville and Meyer, + # "Google's PageRank and Beyond". (Used for test_directed_laplacian) + cls.DG = nx.DiGraph() + cls.DG.add_edges_from( + ( + (1, 2), + (1, 3), + (3, 1), + (3, 2), + (3, 5), + (4, 5), + (4, 6), + (5, 4), + (5, 6), + (6, 4), + ) + ) + + def test_modularity(self): + "Modularity matrix" + # fmt: off + B = np.array([[-1.125, 0.25, 0.25, 0.625, 0.], + [0.25, -0.5, 0.5, -0.25, 0.], + [0.25, 0.5, -0.5, -0.25, 0.], + [0.625, -0.25, -0.25, -0.125, 0.], + [0., 0., 0., 0., 0.]]) + # fmt: on + + permutation = [4, 0, 1, 2, 3] + np.testing.assert_equal(nx.modularity_matrix(self.G), B) + np.testing.assert_equal( + nx.modularity_matrix(self.G, nodelist=permutation), + B[np.ix_(permutation, permutation)], + ) + + def test_modularity_weight(self): + "Modularity matrix with weights" + # fmt: off + B = np.array([[-1.125, 0.25, 0.25, 0.625, 0.], + [0.25, -0.5, 0.5, -0.25, 0.], + [0.25, 0.5, -0.5, -0.25, 0.], + [0.625, -0.25, -0.25, -0.125, 0.], + [0., 0., 0., 0., 0.]]) + # fmt: on + + G_weighted = self.G.copy() + for n1, n2 in G_weighted.edges(): + G_weighted.edges[n1, n2]["weight"] = 0.5 + # The following test would fail in networkx 1.1 + np.testing.assert_equal(nx.modularity_matrix(G_weighted), B) + # The following test that the modularity matrix get rescaled accordingly + np.testing.assert_equal( + nx.modularity_matrix(G_weighted, weight="weight"), 0.5 * B + ) + + def test_directed_modularity(self): + "Directed Modularity matrix" + # fmt: off + B = np.array([[-0.2, 0.6, 0.8, -0.4, -0.4, -0.4], + [0., 0., 0., 0., 0., 0.], + [0.7, 0.4, -0.3, -0.6, 0.4, -0.6], + [-0.2, -0.4, -0.2, -0.4, 0.6, 0.6], + [-0.2, -0.4, -0.2, 0.6, -0.4, 0.6], + [-0.1, -0.2, -0.1, 0.8, -0.2, -0.2]]) + # fmt: on + node_permutation = [5, 1, 2, 3, 4, 6] + idx_permutation = [4, 0, 1, 2, 3, 5] + mm = nx.directed_modularity_matrix(self.DG, nodelist=sorted(self.DG)) + np.testing.assert_equal(mm, B) + np.testing.assert_equal( + nx.directed_modularity_matrix(self.DG, nodelist=node_permutation), + B[np.ix_(idx_permutation, idx_permutation)], + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py new file mode 100644 index 0000000000000000000000000000000000000000..01009f005064b8cd23d221160719c4c5cc8e33f3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py @@ -0,0 +1,70 @@ +import pytest + +import networkx as nx + +np = pytest.importorskip("numpy") +pytest.importorskip("scipy") + + +class TestSpectrum: + @classmethod + def setup_class(cls): + deg = [3, 2, 2, 1, 0] + cls.G = nx.havel_hakimi_graph(deg) + cls.P = nx.path_graph(3) + cls.WG = nx.Graph( + (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges() + ) + cls.WG.add_node(4) + cls.DG = nx.DiGraph() + nx.add_path(cls.DG, [0, 1, 2]) + + def test_laplacian_spectrum(self): + "Laplacian eigenvalues" + evals = np.array([0, 0, 1, 3, 4]) + e = sorted(nx.laplacian_spectrum(self.G)) + np.testing.assert_almost_equal(e, evals) + e = sorted(nx.laplacian_spectrum(self.WG, weight=None)) + np.testing.assert_almost_equal(e, evals) + e = sorted(nx.laplacian_spectrum(self.WG)) + np.testing.assert_almost_equal(e, 0.5 * evals) + e = sorted(nx.laplacian_spectrum(self.WG, weight="other")) + np.testing.assert_almost_equal(e, 0.3 * evals) + + def test_normalized_laplacian_spectrum(self): + "Normalized Laplacian eigenvalues" + evals = np.array([0, 0, 0.7712864461218, 1.5, 1.7287135538781]) + e = sorted(nx.normalized_laplacian_spectrum(self.G)) + np.testing.assert_almost_equal(e, evals) + e = sorted(nx.normalized_laplacian_spectrum(self.WG, weight=None)) + np.testing.assert_almost_equal(e, evals) + e = sorted(nx.normalized_laplacian_spectrum(self.WG)) + np.testing.assert_almost_equal(e, evals) + e = sorted(nx.normalized_laplacian_spectrum(self.WG, weight="other")) + np.testing.assert_almost_equal(e, evals) + + def test_adjacency_spectrum(self): + "Adjacency eigenvalues" + evals = np.array([-np.sqrt(2), 0, np.sqrt(2)]) + e = sorted(nx.adjacency_spectrum(self.P)) + np.testing.assert_almost_equal(e, evals) + + def test_modularity_spectrum(self): + "Modularity eigenvalues" + evals = np.array([-1.5, 0.0, 0.0]) + e = sorted(nx.modularity_spectrum(self.P)) + np.testing.assert_almost_equal(e, evals) + # Directed modularity eigenvalues + evals = np.array([-0.5, 0.0, 0.0]) + e = sorted(nx.modularity_spectrum(self.DG)) + np.testing.assert_almost_equal(e, evals) + + def test_bethe_hessian_spectrum(self): + "Bethe Hessian eigenvalues" + evals = np.array([0.5 * (9 - np.sqrt(33)), 4, 0.5 * (9 + np.sqrt(33))]) + e = sorted(nx.bethe_hessian_spectrum(self.P, r=2)) + np.testing.assert_almost_equal(e, evals) + # Collapses back to Laplacian: + e1 = sorted(nx.bethe_hessian_spectrum(self.P, r=1)) + e2 = sorted(nx.laplacian_spectrum(self.P)) + np.testing.assert_almost_equal(e1, e2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a805c50a7b18bc818f7bb0a8978ee1e7e90277b5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__init__.py @@ -0,0 +1,17 @@ +""" +A package for reading and writing graphs in various formats. + +""" + +from networkx.readwrite.adjlist import * +from networkx.readwrite.multiline_adjlist import * +from networkx.readwrite.edgelist import * +from networkx.readwrite.pajek import * +from networkx.readwrite.leda import * +from networkx.readwrite.sparse6 import * +from networkx.readwrite.graph6 import * +from networkx.readwrite.gml import * +from networkx.readwrite.graphml import * +from networkx.readwrite.gexf import * +from networkx.readwrite.json_graph import * +from networkx.readwrite.text import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d217cbf529f8f70cd50b6aa460636f2979b31e7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/adjlist.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/adjlist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa616dd10deb7fbeb045bc401fe08b640c55beb0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/adjlist.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/edgelist.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/edgelist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c65c60ffdde95677eec4648560a0c196fd6f782 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/edgelist.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gexf.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gexf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a23b14accccbc3225a3528311790e368ef1ca7a6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gexf.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gml.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee8b205de783a143736b94f0408521ac2b05d888 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/gml.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graph6.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graph6.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54f6cd3e7f373083f283b00d1b9b8102eb1f1a13 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graph6.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graphml.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graphml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72a15e4fbc714a310c01620a4d6f7c9d475b9e3f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/graphml.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/leda.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/leda.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba5d28924587de046965708cde0591efac3f74f8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/leda.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/multiline_adjlist.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/multiline_adjlist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31faa8428e96f82c6c3b446ae9cb2401a45795cb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/multiline_adjlist.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/p2g.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/p2g.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3da59134f4067b404dba54e662e16131e2a030d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/p2g.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/pajek.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/pajek.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2605c861346023a5a14838b8208412273323c528 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/pajek.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/sparse6.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/sparse6.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9e1884c2592d332b72c3be6d07ad00526ff7a4a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/sparse6.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/text.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/text.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3748d4fadd06fea621f161bd96e47774b11e11aa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/__pycache__/text.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/adjlist.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/adjlist.py new file mode 100644 index 0000000000000000000000000000000000000000..ba3b6e5addff8f39ae5f89610e675304d580787c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/adjlist.py @@ -0,0 +1,330 @@ +""" +************** +Adjacency List +************** +Read and write NetworkX graphs as adjacency lists. + +Adjacency list format is useful for graphs without data associated +with nodes or edges and for nodes that can be meaningfully represented +as strings. + +Format +------ +The adjacency list format consists of lines with node labels. The +first label in a line is the source node. Further labels in the line +are considered target nodes and are added to the graph along with an edge +between the source node and target node. + +The graph with edges a-b, a-c, d-e can be represented as the following +adjacency list (anything following the # in a line is a comment):: + + a b c # source target target + d e +""" + +__all__ = ["generate_adjlist", "write_adjlist", "parse_adjlist", "read_adjlist"] + +import networkx as nx +from networkx.utils import open_file + + +def generate_adjlist(G, delimiter=" "): + """Generate lines representing a graph in adjacency list format. + + Parameters + ---------- + G : NetworkX graph + + delimiter : str, default=" " + Separator for node labels. + + Yields + ------ + str + Adjacency list for a node in `G`. The first item is the node label, + followed by the labels of its neighbors. + + Examples + -------- + >>> G = nx.lollipop_graph(4, 3) + >>> for line in nx.generate_adjlist(G): + ... print(line) + 0 1 2 3 + 1 2 3 + 2 3 + 3 4 + 4 5 + 5 6 + 6 + + When `G` is undirected, each edge is only listed once. For directed graphs, + edges appear once for each direction. + + >>> G = nx.complete_graph(3, create_using=nx.DiGraph) + >>> for line in nx.generate_adjlist(G): + ... print(line) + 0 1 2 + 1 0 2 + 2 0 1 + + Node labels are shown multiple times for multiedges, but edge data (including keys) + are not included in the output. + + >>> G = nx.MultiGraph([(0, 1, {"weight": 1}), (0, 1, {"weight": 2})]) + >>> for line in nx.generate_adjlist(G): + ... print(line) + 0 1 1 + 1 + + See Also + -------- + write_adjlist, read_adjlist + + Notes + ----- + The default `delimiter=" "` will result in unexpected results if node names contain + whitespace characters. To avoid this problem, specify an alternate delimiter when spaces are + valid in node names. + + NB: This option is not available for data that isn't user-generated. + + """ + seen = set() + directed = G.is_directed() + multigraph = G.is_multigraph() + for s, nbrs in G.adjacency(): + nodes = [str(s)] + for t, data in nbrs.items(): + if t in seen: + continue + if multigraph and len(data) > 1: + nodes.extend((str(t),) * len(data)) + else: + nodes.append(str(t)) + if not directed: + seen.add(s) + yield delimiter.join(nodes) + + +@open_file(1, mode="wb") +def write_adjlist(G, path, comments="#", delimiter=" ", encoding="utf-8"): + """Write graph G in single-line adjacency-list format to path. + + + Parameters + ---------- + G : NetworkX graph + + path : string or file + Filename or file handle for data output. + Filenames ending in .gz or .bz2 will be compressed. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels + + encoding : string, optional + Text encoding. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_adjlist(G, "path4.adjlist") + + The path can be a filehandle or a string with the name of the file. If a + filehandle is provided, it has to be opened in 'wb' mode. + + >>> fh = open("path4.adjlist2", "wb") + >>> nx.write_adjlist(G, fh) + + Notes + ----- + The default `delimiter=" "` will result in unexpected results if node names contain + whitespace characters. To avoid this problem, specify an alternate delimiter when spaces are + valid in node names. + NB: This option is not available for data that isn't user-generated. + + This format does not store graph, node, or edge data. + + See Also + -------- + read_adjlist, generate_adjlist + """ + import sys + import time + + pargs = comments + " ".join(sys.argv) + "\n" + header = ( + pargs + + comments + + f" GMT {time.asctime(time.gmtime())}\n" + + comments + + f" {G.name}\n" + ) + path.write(header.encode(encoding)) + + for line in generate_adjlist(G, delimiter): + line += "\n" + path.write(line.encode(encoding)) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_adjlist( + lines, comments="#", delimiter=None, create_using=None, nodetype=None +): + """Parse lines of a graph adjacency list representation. + + Parameters + ---------- + lines : list or iterator of strings + Input data in adjlist format + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + nodetype : Python type, optional + Convert nodes to this type. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels. The default is whitespace. + + Returns + ------- + G: NetworkX graph + The graph corresponding to the lines in adjacency list format. + + Examples + -------- + >>> lines = ["1 2 5", "2 3 4", "3 5", "4", "5"] + >>> G = nx.parse_adjlist(lines, nodetype=int) + >>> nodes = [1, 2, 3, 4, 5] + >>> all(node in G for node in nodes) + True + >>> edges = [(1, 2), (1, 5), (2, 3), (2, 4), (3, 5)] + >>> all((u, v) in G.edges() or (v, u) in G.edges() for (u, v) in edges) + True + + See Also + -------- + read_adjlist + + """ + G = nx.empty_graph(0, create_using) + for line in lines: + p = line.find(comments) + if p >= 0: + line = line[:p] + if not len(line): + continue + vlist = line.rstrip("\n").split(delimiter) + u = vlist.pop(0) + # convert types + if nodetype is not None: + try: + u = nodetype(u) + except BaseException as err: + raise TypeError( + f"Failed to convert node ({u}) to type {nodetype}" + ) from err + G.add_node(u) + if nodetype is not None: + try: + vlist = list(map(nodetype, vlist)) + except BaseException as err: + raise TypeError( + f"Failed to convert nodes ({','.join(vlist)}) to type {nodetype}" + ) from err + G.add_edges_from([(u, v) for v in vlist]) + return G + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_adjlist( + path, + comments="#", + delimiter=None, + create_using=None, + nodetype=None, + encoding="utf-8", +): + """Read graph in adjacency list format from path. + + Parameters + ---------- + path : string or file + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + nodetype : Python type, optional + Convert nodes to this type. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels. The default is whitespace. + + Returns + ------- + G: NetworkX graph + The graph corresponding to the lines in adjacency list format. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_adjlist(G, "test.adjlist") + >>> G = nx.read_adjlist("test.adjlist") + + The path can be a filehandle or a string with the name of the file. If a + filehandle is provided, it has to be opened in 'rb' mode. + + >>> fh = open("test.adjlist", "rb") + >>> G = nx.read_adjlist(fh) + + Filenames ending in .gz or .bz2 will be compressed. + + >>> nx.write_adjlist(G, "test.adjlist.gz") + >>> G = nx.read_adjlist("test.adjlist.gz") + + The optional nodetype is a function to convert node strings to nodetype. + + For example + + >>> G = nx.read_adjlist("test.adjlist", nodetype=int) + + will attempt to convert all nodes to integer type. + + Since nodes must be hashable, the function nodetype must return hashable + types (e.g. int, float, str, frozenset - or tuples of those, etc.) + + The optional create_using parameter indicates the type of NetworkX graph + created. The default is `nx.Graph`, an undirected graph. + To read the data as a directed graph use + + >>> G = nx.read_adjlist("test.adjlist", create_using=nx.DiGraph) + + Notes + ----- + This format does not store graph or node data. + + See Also + -------- + write_adjlist + """ + lines = (line.decode(encoding) for line in path) + return parse_adjlist( + lines, + comments=comments, + delimiter=delimiter, + create_using=create_using, + nodetype=nodetype, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/edgelist.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/edgelist.py new file mode 100644 index 0000000000000000000000000000000000000000..afdb175e8cf4dfb5d8ddb81b136259d2471ad13e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/edgelist.py @@ -0,0 +1,489 @@ +""" +********** +Edge Lists +********** +Read and write NetworkX graphs as edge lists. + +The multi-line adjacency list format is useful for graphs with nodes +that can be meaningfully represented as strings. With the edgelist +format simple edge data can be stored but node or graph data is not. +There is no way of representing isolated nodes unless the node has a +self-loop edge. + +Format +------ +You can read or write three formats of edge lists with these functions. + +Node pairs with no data:: + + 1 2 + +Python dictionary as data:: + + 1 2 {'weight':7, 'color':'green'} + +Arbitrary data:: + + 1 2 7 green +""" + +__all__ = [ + "generate_edgelist", + "write_edgelist", + "parse_edgelist", + "read_edgelist", + "read_weighted_edgelist", + "write_weighted_edgelist", +] + +import networkx as nx +from networkx.utils import open_file + + +def generate_edgelist(G, delimiter=" ", data=True): + """Generate a single line of the graph G in edge list format. + + Parameters + ---------- + G : NetworkX graph + + delimiter : string, optional + Separator for node labels + + data : bool or list of keys + If False generate no edge data. If True use a dictionary + representation of edge data. If a list of keys use a list of data + values corresponding to the keys. + + Returns + ------- + lines : string + Lines of data in adjlist format. + + Examples + -------- + >>> G = nx.lollipop_graph(4, 3) + >>> G[1][2]["weight"] = 3 + >>> G[3][4]["capacity"] = 12 + >>> for line in nx.generate_edgelist(G, data=False): + ... print(line) + 0 1 + 0 2 + 0 3 + 1 2 + 1 3 + 2 3 + 3 4 + 4 5 + 5 6 + + >>> for line in nx.generate_edgelist(G): + ... print(line) + 0 1 {} + 0 2 {} + 0 3 {} + 1 2 {'weight': 3} + 1 3 {} + 2 3 {} + 3 4 {'capacity': 12} + 4 5 {} + 5 6 {} + + >>> for line in nx.generate_edgelist(G, data=["weight"]): + ... print(line) + 0 1 + 0 2 + 0 3 + 1 2 3 + 1 3 + 2 3 + 3 4 + 4 5 + 5 6 + + See Also + -------- + write_adjlist, read_adjlist + """ + if data is True: + for u, v, d in G.edges(data=True): + e = u, v, dict(d) + yield delimiter.join(map(str, e)) + elif data is False: + for u, v in G.edges(data=False): + e = u, v + yield delimiter.join(map(str, e)) + else: + for u, v, d in G.edges(data=True): + e = [u, v] + try: + e.extend(d[k] for k in data) + except KeyError: + pass # missing data for this edge, should warn? + yield delimiter.join(map(str, e)) + + +@open_file(1, mode="wb") +def write_edgelist(G, path, comments="#", delimiter=" ", data=True, encoding="utf-8"): + """Write graph as a list of edges. + + Parameters + ---------- + G : graph + A NetworkX graph + path : file or string + File or filename to write. If a file is provided, it must be + opened in 'wb' mode. Filenames ending in .gz or .bz2 will be compressed. + comments : string, optional + The character used to indicate the start of a comment + delimiter : string, optional + The string used to separate values. The default is whitespace. + data : bool or list, optional + If False write no edge data. + If True write a string representation of the edge data dictionary.. + If a list (or other iterable) is provided, write the keys specified + in the list. + encoding: string, optional + Specify which encoding to use when writing file. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_edgelist(G, "test.edgelist") + >>> G = nx.path_graph(4) + >>> fh = open("test.edgelist", "wb") + >>> nx.write_edgelist(G, fh) + >>> nx.write_edgelist(G, "test.edgelist.gz") + >>> nx.write_edgelist(G, "test.edgelist_nodata.gz", data=False) + + >>> G = nx.Graph() + >>> G.add_edge(1, 2, weight=7, color="red") + >>> nx.write_edgelist(G, "test.edgelist_bigger_nodata", data=False) + >>> nx.write_edgelist(G, "test.edgelist_color", data=["color"]) + >>> nx.write_edgelist(G, "test.edgelist_color_weight", data=["color", "weight"]) + + See Also + -------- + read_edgelist + write_weighted_edgelist + """ + + for line in generate_edgelist(G, delimiter, data): + line += "\n" + path.write(line.encode(encoding)) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_edgelist( + lines, comments="#", delimiter=None, create_using=None, nodetype=None, data=True +): + """Parse lines of an edge list representation of a graph. + + Parameters + ---------- + lines : list or iterator of strings + Input data in edgelist format + comments : string, optional + Marker for comment lines. Default is `'#'`. To specify that no character + should be treated as a comment, use ``comments=None``. + delimiter : string, optional + Separator for node labels. Default is `None`, meaning any whitespace. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + nodetype : Python type, optional + Convert nodes to this type. Default is `None`, meaning no conversion is + performed. + data : bool or list of (label,type) tuples + If `False` generate no edge data or if `True` use a dictionary + representation of edge data or a list tuples specifying dictionary + key names and types for edge data. + + Returns + ------- + G: NetworkX Graph + The graph corresponding to lines + + Examples + -------- + Edgelist with no data: + + >>> lines = ["1 2", "2 3", "3 4"] + >>> G = nx.parse_edgelist(lines, nodetype=int) + >>> list(G) + [1, 2, 3, 4] + >>> list(G.edges()) + [(1, 2), (2, 3), (3, 4)] + + Edgelist with data in Python dictionary representation: + + >>> lines = ["1 2 {'weight': 3}", "2 3 {'weight': 27}", "3 4 {'weight': 3.0}"] + >>> G = nx.parse_edgelist(lines, nodetype=int) + >>> list(G) + [1, 2, 3, 4] + >>> list(G.edges(data=True)) + [(1, 2, {'weight': 3}), (2, 3, {'weight': 27}), (3, 4, {'weight': 3.0})] + + Edgelist with data in a list: + + >>> lines = ["1 2 3", "2 3 27", "3 4 3.0"] + >>> G = nx.parse_edgelist(lines, nodetype=int, data=(("weight", float),)) + >>> list(G) + [1, 2, 3, 4] + >>> list(G.edges(data=True)) + [(1, 2, {'weight': 3.0}), (2, 3, {'weight': 27.0}), (3, 4, {'weight': 3.0})] + + See Also + -------- + read_weighted_edgelist + """ + from ast import literal_eval + + G = nx.empty_graph(0, create_using) + for line in lines: + if comments is not None: + p = line.find(comments) + if p >= 0: + line = line[:p] + if not line: + continue + # split line, should have 2 or more + s = line.rstrip("\n").split(delimiter) + if len(s) < 2: + continue + u = s.pop(0) + v = s.pop(0) + d = s + if nodetype is not None: + try: + u = nodetype(u) + v = nodetype(v) + except Exception as err: + raise TypeError( + f"Failed to convert nodes {u},{v} to type {nodetype}." + ) from err + + if len(d) == 0 or data is False: + # no data or data type specified + edgedata = {} + elif data is True: + # no edge types specified + try: # try to evaluate as dictionary + if delimiter == ",": + edgedata_str = ",".join(d) + else: + edgedata_str = " ".join(d) + edgedata = dict(literal_eval(edgedata_str.strip())) + except Exception as err: + raise TypeError( + f"Failed to convert edge data ({d}) to dictionary." + ) from err + else: + # convert edge data to dictionary with specified keys and type + if len(d) != len(data): + raise IndexError( + f"Edge data {d} and data_keys {data} are not the same length" + ) + edgedata = {} + for (edge_key, edge_type), edge_value in zip(data, d): + try: + edge_value = edge_type(edge_value) + except Exception as err: + raise TypeError( + f"Failed to convert {edge_key} data {edge_value} " + f"to type {edge_type}." + ) from err + edgedata.update({edge_key: edge_value}) + G.add_edge(u, v, **edgedata) + return G + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_edgelist( + path, + comments="#", + delimiter=None, + create_using=None, + nodetype=None, + data=True, + edgetype=None, + encoding="utf-8", +): + """Read a graph from a list of edges. + + Parameters + ---------- + path : file or string + File or filename to read. If a file is provided, it must be + opened in 'rb' mode. + Filenames ending in .gz or .bz2 will be decompressed. + comments : string, optional + The character used to indicate the start of a comment. To specify that + no character should be treated as a comment, use ``comments=None``. + delimiter : string, optional + The string used to separate values. The default is whitespace. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + nodetype : int, float, str, Python type, optional + Convert node data from strings to specified type + data : bool or list of (label,type) tuples + Tuples specifying dictionary key names and types for edge data + edgetype : int, float, str, Python type, optional OBSOLETE + Convert edge data from strings to specified type and use as 'weight' + encoding: string, optional + Specify which encoding to use when reading file. + + Returns + ------- + G : graph + A networkx Graph or other type specified with create_using + + Examples + -------- + >>> nx.write_edgelist(nx.path_graph(4), "test.edgelist_P4") + >>> G = nx.read_edgelist("test.edgelist_P4") + + >>> fh = open("test.edgelist_P4", "rb") + >>> G = nx.read_edgelist(fh) + >>> fh.close() + + >>> G = nx.read_edgelist("test.edgelist_P4", nodetype=int) + >>> G = nx.read_edgelist("test.edgelist_P4", create_using=nx.DiGraph) + + Edgelist with data in a list: + + >>> textline = "1 2 3" + >>> fh = open("test.textline", "w") + >>> d = fh.write(textline) + >>> fh.close() + >>> G = nx.read_edgelist("test.textline", nodetype=int, data=(("weight", float),)) + >>> list(G) + [1, 2] + >>> list(G.edges(data=True)) + [(1, 2, {'weight': 3.0})] + + See parse_edgelist() for more examples of formatting. + + See Also + -------- + parse_edgelist + write_edgelist + + Notes + ----- + Since nodes must be hashable, the function nodetype must return hashable + types (e.g. int, float, str, frozenset - or tuples of those, etc.) + """ + lines = (line if isinstance(line, str) else line.decode(encoding) for line in path) + return parse_edgelist( + lines, + comments=comments, + delimiter=delimiter, + create_using=create_using, + nodetype=nodetype, + data=data, + ) + + +def write_weighted_edgelist(G, path, comments="#", delimiter=" ", encoding="utf-8"): + """Write graph G as a list of edges with numeric weights. + + Parameters + ---------- + G : graph + A NetworkX graph + path : file or string + File or filename to write. If a file is provided, it must be + opened in 'wb' mode. + Filenames ending in .gz or .bz2 will be compressed. + comments : string, optional + The character used to indicate the start of a comment + delimiter : string, optional + The string used to separate values. The default is whitespace. + encoding: string, optional + Specify which encoding to use when writing file. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_edge(1, 2, weight=7) + >>> nx.write_weighted_edgelist(G, "test.weighted.edgelist") + + See Also + -------- + read_edgelist + write_edgelist + read_weighted_edgelist + """ + write_edgelist( + G, + path, + comments=comments, + delimiter=delimiter, + data=("weight",), + encoding=encoding, + ) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def read_weighted_edgelist( + path, + comments="#", + delimiter=None, + create_using=None, + nodetype=None, + encoding="utf-8", +): + """Read a graph as list of edges with numeric weights. + + Parameters + ---------- + path : file or string + File or filename to read. If a file is provided, it must be + opened in 'rb' mode. + Filenames ending in .gz or .bz2 will be decompressed. + comments : string, optional + The character used to indicate the start of a comment. + delimiter : string, optional + The string used to separate values. The default is whitespace. + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + nodetype : int, float, str, Python type, optional + Convert node data from strings to specified type + encoding: string, optional + Specify which encoding to use when reading file. + + Returns + ------- + G : graph + A networkx Graph or other type specified with create_using + + Notes + ----- + Since nodes must be hashable, the function nodetype must return hashable + types (e.g. int, float, str, frozenset - or tuples of those, etc.) + + Example edgelist file format. + + With numeric edge data:: + + # read with + # >>> G=nx.read_weighted_edgelist(fh) + # source target data + a b 1 + a c 3.14159 + d e 42 + + See Also + -------- + write_weighted_edgelist + """ + return read_edgelist( + path, + comments=comments, + delimiter=delimiter, + create_using=create_using, + nodetype=nodetype, + data=(("weight", float),), + encoding=encoding, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gexf.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gexf.py new file mode 100644 index 0000000000000000000000000000000000000000..56bc71f0e3c05ea053a10d68425168ddef23f04d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gexf.py @@ -0,0 +1,1084 @@ +"""Read and write graphs in GEXF format. + +.. warning:: + This parser uses the standard xml library present in Python, which is + insecure - see :external+python:mod:`xml` for additional information. + Only parse GEFX files you trust. + +GEXF (Graph Exchange XML Format) is a language for describing complex +network structures, their associated data and dynamics. + +This implementation does not support mixed graphs (directed and +undirected edges together). + +Format +------ +GEXF is an XML format. See http://gexf.net/schema.html for the +specification and http://gexf.net/basic.html for examples. +""" + +import itertools +import time +from xml.etree.ElementTree import ( + Element, + ElementTree, + SubElement, + register_namespace, + tostring, +) + +import networkx as nx +from networkx.utils import open_file + +__all__ = ["write_gexf", "read_gexf", "relabel_gexf_graph", "generate_gexf"] + + +@open_file(1, mode="wb") +def write_gexf(G, path, encoding="utf-8", prettyprint=True, version="1.2draft"): + """Write G in GEXF format to path. + + "GEXF (Graph Exchange XML Format) is a language for describing + complex networks structures, their associated data and dynamics" [1]_. + + Node attributes are checked according to the version of the GEXF + schemas used for parameters which are not user defined, + e.g. visualization 'viz' [2]_. See example for usage. + + .. warning:: + + The `GEXF specification `_ reserves some + keywords (e.g. ``id``, ``pid``, ``label``, etc.) for specifying node/edge + metadata in the file format. Ensure NetworkX node/edge attribute names + do not use these special keywords to guarantee all attributes are preserved + as expected when roundtripping to/from GEXF format. + + Parameters + ---------- + G : graph + A NetworkX graph + path : file or string + File or file name to write. + File names ending in .gz or .bz2 will be compressed. + encoding : string (optional, default: 'utf-8') + Encoding for text data. + prettyprint : bool (optional, default: True) + If True use line breaks and indenting in output XML. + version: string (optional, default: '1.2draft') + The version of GEXF to be used for nodes attributes checking + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_gexf(G, "test.gexf") + + # visualization data + >>> G.nodes[0]["viz"] = {"size": 54} + >>> G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1} + >>> G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256} + + + Notes + ----- + This implementation does not support mixed graphs (directed and undirected + edges together). + + The node id attribute is set to be the string of the node label. + If you want to specify an id use set it as node data, e.g. + node['a']['id']=1 to set the id of node 'a' to 1. + + References + ---------- + .. [1] GEXF File Format, http://gexf.net/ + .. [2] GEXF schema, http://gexf.net/schema.html + """ + writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version) + writer.add_graph(G) + writer.write(path) + + +def generate_gexf(G, encoding="utf-8", prettyprint=True, version="1.2draft"): + """Generate lines of GEXF format representation of G. + + "GEXF (Graph Exchange XML Format) is a language for describing + complex networks structures, their associated data and dynamics" [1]_. + + Parameters + ---------- + G : graph + A NetworkX graph + encoding : string (optional, default: 'utf-8') + Encoding for text data. + prettyprint : bool (optional, default: True) + If True use line breaks and indenting in output XML. + version : string (default: 1.2draft) + Version of GEFX File Format (see http://gexf.net/schema.html) + Supported values: "1.1draft", "1.2draft" + + + Examples + -------- + >>> G = nx.path_graph(4) + >>> linefeed = chr(10) # linefeed=\n + >>> s = linefeed.join(nx.generate_gexf(G)) + >>> for line in nx.generate_gexf(G): # doctest: +SKIP + ... print(line) + + Notes + ----- + This implementation does not support mixed graphs (directed and undirected + edges together). + + The node id attribute is set to be the string of the node label. + If you want to specify an id use set it as node data, e.g. + node['a']['id']=1 to set the id of node 'a' to 1. + + References + ---------- + .. [1] GEXF File Format, https://gephi.org/gexf/format/ + """ + writer = GEXFWriter(encoding=encoding, prettyprint=prettyprint, version=version) + writer.add_graph(G) + yield from str(writer).splitlines() + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_gexf(path, node_type=None, relabel=False, version="1.2draft"): + """Read graph in GEXF format from path. + + "GEXF (Graph Exchange XML Format) is a language for describing + complex networks structures, their associated data and dynamics" [1]_. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + node_type: Python type (default: None) + Convert node ids to this type if not None. + relabel : bool (default: False) + If True relabel the nodes to use the GEXF node "label" attribute + instead of the node "id" attribute as the NetworkX node label. + version : string (default: 1.2draft) + Version of GEFX File Format (see http://gexf.net/schema.html) + Supported values: "1.1draft", "1.2draft" + + Returns + ------- + graph: NetworkX graph + If no parallel edges are found a Graph or DiGraph is returned. + Otherwise a MultiGraph or MultiDiGraph is returned. + + Notes + ----- + This implementation does not support mixed graphs (directed and undirected + edges together). + + References + ---------- + .. [1] GEXF File Format, http://gexf.net/ + """ + reader = GEXFReader(node_type=node_type, version=version) + if relabel: + G = relabel_gexf_graph(reader(path)) + else: + G = reader(path) + return G + + +class GEXF: + versions = { + "1.1draft": { + "NS_GEXF": "http://www.gexf.net/1.1draft", + "NS_VIZ": "http://www.gexf.net/1.1draft/viz", + "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance", + "SCHEMALOCATION": " ".join( + [ + "http://www.gexf.net/1.1draft", + "http://www.gexf.net/1.1draft/gexf.xsd", + ] + ), + "VERSION": "1.1", + }, + "1.2draft": { + "NS_GEXF": "http://www.gexf.net/1.2draft", + "NS_VIZ": "http://www.gexf.net/1.2draft/viz", + "NS_XSI": "http://www.w3.org/2001/XMLSchema-instance", + "SCHEMALOCATION": " ".join( + [ + "http://www.gexf.net/1.2draft", + "http://www.gexf.net/1.2draft/gexf.xsd", + ] + ), + "VERSION": "1.2", + }, + "1.3": { + "NS_GEXF": "http://gexf.net/1.3", + "NS_VIZ": "http://gexf.net/1.3/viz", + "NS_XSI": "http://w3.org/2001/XMLSchema-instance", + "SCHEMALOCATION": " ".join( + [ + "http://gexf.net/1.3", + "http://gexf.net/1.3/gexf.xsd", + ] + ), + "VERSION": "1.3", + }, + } + + def construct_types(self): + types = [ + (int, "integer"), + (float, "float"), + (float, "double"), + (bool, "boolean"), + (list, "string"), + (dict, "string"), + (int, "long"), + (str, "liststring"), + (str, "anyURI"), + (str, "string"), + ] + + # These additions to types allow writing numpy types + try: + import numpy as np + except ImportError: + pass + else: + # prepend so that python types are created upon read (last entry wins) + types = [ + (np.float64, "float"), + (np.float32, "float"), + (np.float16, "float"), + (np.int_, "int"), + (np.int8, "int"), + (np.int16, "int"), + (np.int32, "int"), + (np.int64, "int"), + (np.uint8, "int"), + (np.uint16, "int"), + (np.uint32, "int"), + (np.uint64, "int"), + (np.int_, "int"), + (np.intc, "int"), + (np.intp, "int"), + ] + types + + self.xml_type = dict(types) + self.python_type = dict(reversed(a) for a in types) + + # http://www.w3.org/TR/xmlschema-2/#boolean + convert_bool = { + "true": True, + "false": False, + "True": True, + "False": False, + "0": False, + 0: False, + "1": True, + 1: True, + } + + def set_version(self, version): + d = self.versions.get(version) + if d is None: + raise nx.NetworkXError(f"Unknown GEXF version {version}.") + self.NS_GEXF = d["NS_GEXF"] + self.NS_VIZ = d["NS_VIZ"] + self.NS_XSI = d["NS_XSI"] + self.SCHEMALOCATION = d["SCHEMALOCATION"] + self.VERSION = d["VERSION"] + self.version = version + + +class GEXFWriter(GEXF): + # class for writing GEXF format files + # use write_gexf() function + def __init__( + self, graph=None, encoding="utf-8", prettyprint=True, version="1.2draft" + ): + self.construct_types() + self.prettyprint = prettyprint + self.encoding = encoding + self.set_version(version) + self.xml = Element( + "gexf", + { + "xmlns": self.NS_GEXF, + "xmlns:xsi": self.NS_XSI, + "xsi:schemaLocation": self.SCHEMALOCATION, + "version": self.VERSION, + }, + ) + + # Make meta element a non-graph element + # Also add lastmodifieddate as attribute, not tag + meta_element = Element("meta") + subelement_text = f"NetworkX {nx.__version__}" + SubElement(meta_element, "creator").text = subelement_text + meta_element.set("lastmodifieddate", time.strftime("%Y-%m-%d")) + self.xml.append(meta_element) + + register_namespace("viz", self.NS_VIZ) + + # counters for edge and attribute identifiers + self.edge_id = itertools.count() + self.attr_id = itertools.count() + self.all_edge_ids = set() + # default attributes are stored in dictionaries + self.attr = {} + self.attr["node"] = {} + self.attr["edge"] = {} + self.attr["node"]["dynamic"] = {} + self.attr["node"]["static"] = {} + self.attr["edge"]["dynamic"] = {} + self.attr["edge"]["static"] = {} + + if graph is not None: + self.add_graph(graph) + + def __str__(self): + if self.prettyprint: + self.indent(self.xml) + s = tostring(self.xml).decode(self.encoding) + return s + + def add_graph(self, G): + # first pass through G collecting edge ids + for u, v, dd in G.edges(data=True): + eid = dd.get("id") + if eid is not None: + self.all_edge_ids.add(str(eid)) + # set graph attributes + if G.graph.get("mode") == "dynamic": + mode = "dynamic" + else: + mode = "static" + # Add a graph element to the XML + if G.is_directed(): + default = "directed" + else: + default = "undirected" + name = G.graph.get("name", "") + graph_element = Element("graph", defaultedgetype=default, mode=mode, name=name) + self.graph_element = graph_element + self.add_nodes(G, graph_element) + self.add_edges(G, graph_element) + self.xml.append(graph_element) + + def add_nodes(self, G, graph_element): + nodes_element = Element("nodes") + for node, data in G.nodes(data=True): + node_data = data.copy() + node_id = str(node_data.pop("id", node)) + kw = {"id": node_id} + label = str(node_data.pop("label", node)) + kw["label"] = label + try: + pid = node_data.pop("pid") + kw["pid"] = str(pid) + except KeyError: + pass + try: + start = node_data.pop("start") + kw["start"] = str(start) + self.alter_graph_mode_timeformat(start) + except KeyError: + pass + try: + end = node_data.pop("end") + kw["end"] = str(end) + self.alter_graph_mode_timeformat(end) + except KeyError: + pass + # add node element with attributes + node_element = Element("node", **kw) + # add node element and attr subelements + default = G.graph.get("node_default", {}) + node_data = self.add_parents(node_element, node_data) + if self.VERSION == "1.1": + node_data = self.add_slices(node_element, node_data) + else: + node_data = self.add_spells(node_element, node_data) + node_data = self.add_viz(node_element, node_data) + node_data = self.add_attributes("node", node_element, node_data, default) + nodes_element.append(node_element) + graph_element.append(nodes_element) + + def add_edges(self, G, graph_element): + def edge_key_data(G): + # helper function to unify multigraph and graph edge iterator + if G.is_multigraph(): + for u, v, key, data in G.edges(data=True, keys=True): + edge_data = data.copy() + edge_data.update(key=key) + edge_id = edge_data.pop("id", None) + if edge_id is None: + edge_id = next(self.edge_id) + while str(edge_id) in self.all_edge_ids: + edge_id = next(self.edge_id) + self.all_edge_ids.add(str(edge_id)) + yield u, v, edge_id, edge_data + else: + for u, v, data in G.edges(data=True): + edge_data = data.copy() + edge_id = edge_data.pop("id", None) + if edge_id is None: + edge_id = next(self.edge_id) + while str(edge_id) in self.all_edge_ids: + edge_id = next(self.edge_id) + self.all_edge_ids.add(str(edge_id)) + yield u, v, edge_id, edge_data + + edges_element = Element("edges") + for u, v, key, edge_data in edge_key_data(G): + kw = {"id": str(key)} + try: + edge_label = edge_data.pop("label") + kw["label"] = str(edge_label) + except KeyError: + pass + try: + edge_weight = edge_data.pop("weight") + kw["weight"] = str(edge_weight) + except KeyError: + pass + try: + edge_type = edge_data.pop("type") + kw["type"] = str(edge_type) + except KeyError: + pass + try: + start = edge_data.pop("start") + kw["start"] = str(start) + self.alter_graph_mode_timeformat(start) + except KeyError: + pass + try: + end = edge_data.pop("end") + kw["end"] = str(end) + self.alter_graph_mode_timeformat(end) + except KeyError: + pass + source_id = str(G.nodes[u].get("id", u)) + target_id = str(G.nodes[v].get("id", v)) + edge_element = Element("edge", source=source_id, target=target_id, **kw) + default = G.graph.get("edge_default", {}) + if self.VERSION == "1.1": + edge_data = self.add_slices(edge_element, edge_data) + else: + edge_data = self.add_spells(edge_element, edge_data) + edge_data = self.add_viz(edge_element, edge_data) + edge_data = self.add_attributes("edge", edge_element, edge_data, default) + edges_element.append(edge_element) + graph_element.append(edges_element) + + def add_attributes(self, node_or_edge, xml_obj, data, default): + # Add attrvalues to node or edge + attvalues = Element("attvalues") + if len(data) == 0: + return data + mode = "static" + for k, v in data.items(): + # rename generic multigraph key to avoid any name conflict + if k == "key": + k = "networkx_key" + val_type = type(v) + if val_type not in self.xml_type: + raise TypeError(f"attribute value type is not allowed: {val_type}") + if isinstance(v, list): + # dynamic data + for val, start, end in v: + val_type = type(val) + if start is not None or end is not None: + mode = "dynamic" + self.alter_graph_mode_timeformat(start) + self.alter_graph_mode_timeformat(end) + break + attr_id = self.get_attr_id( + str(k), self.xml_type[val_type], node_or_edge, default, mode + ) + for val, start, end in v: + e = Element("attvalue") + e.attrib["for"] = attr_id + e.attrib["value"] = str(val) + # Handle nan, inf, -inf differently + if val_type is float: + if e.attrib["value"] == "inf": + e.attrib["value"] = "INF" + elif e.attrib["value"] == "nan": + e.attrib["value"] = "NaN" + elif e.attrib["value"] == "-inf": + e.attrib["value"] = "-INF" + if start is not None: + e.attrib["start"] = str(start) + if end is not None: + e.attrib["end"] = str(end) + attvalues.append(e) + else: + # static data + mode = "static" + attr_id = self.get_attr_id( + str(k), self.xml_type[val_type], node_or_edge, default, mode + ) + e = Element("attvalue") + e.attrib["for"] = attr_id + if isinstance(v, bool): + e.attrib["value"] = str(v).lower() + else: + e.attrib["value"] = str(v) + # Handle float nan, inf, -inf differently + if val_type is float: + if e.attrib["value"] == "inf": + e.attrib["value"] = "INF" + elif e.attrib["value"] == "nan": + e.attrib["value"] = "NaN" + elif e.attrib["value"] == "-inf": + e.attrib["value"] = "-INF" + attvalues.append(e) + xml_obj.append(attvalues) + return data + + def get_attr_id(self, title, attr_type, edge_or_node, default, mode): + # find the id of the attribute or generate a new id + try: + return self.attr[edge_or_node][mode][title] + except KeyError: + # generate new id + new_id = str(next(self.attr_id)) + self.attr[edge_or_node][mode][title] = new_id + attr_kwargs = {"id": new_id, "title": title, "type": attr_type} + attribute = Element("attribute", **attr_kwargs) + # add subelement for data default value if present + default_title = default.get(title) + if default_title is not None: + default_element = Element("default") + default_element.text = str(default_title) + attribute.append(default_element) + # new insert it into the XML + attributes_element = None + for a in self.graph_element.findall("attributes"): + # find existing attributes element by class and mode + a_class = a.get("class") + a_mode = a.get("mode", "static") + if a_class == edge_or_node and a_mode == mode: + attributes_element = a + if attributes_element is None: + # create new attributes element + attr_kwargs = {"mode": mode, "class": edge_or_node} + attributes_element = Element("attributes", **attr_kwargs) + self.graph_element.insert(0, attributes_element) + attributes_element.append(attribute) + return new_id + + def add_viz(self, element, node_data): + viz = node_data.pop("viz", False) + if viz: + color = viz.get("color") + if color is not None: + if self.VERSION == "1.1": + e = Element( + f"{{{self.NS_VIZ}}}color", + r=str(color.get("r")), + g=str(color.get("g")), + b=str(color.get("b")), + ) + else: + e = Element( + f"{{{self.NS_VIZ}}}color", + r=str(color.get("r")), + g=str(color.get("g")), + b=str(color.get("b")), + a=str(color.get("a", 1.0)), + ) + element.append(e) + + size = viz.get("size") + if size is not None: + e = Element(f"{{{self.NS_VIZ}}}size", value=str(size)) + element.append(e) + + thickness = viz.get("thickness") + if thickness is not None: + e = Element(f"{{{self.NS_VIZ}}}thickness", value=str(thickness)) + element.append(e) + + shape = viz.get("shape") + if shape is not None: + if shape.startswith("http"): + e = Element( + f"{{{self.NS_VIZ}}}shape", value="image", uri=str(shape) + ) + else: + e = Element(f"{{{self.NS_VIZ}}}shape", value=str(shape)) + element.append(e) + + position = viz.get("position") + if position is not None: + e = Element( + f"{{{self.NS_VIZ}}}position", + x=str(position.get("x")), + y=str(position.get("y")), + z=str(position.get("z")), + ) + element.append(e) + return node_data + + def add_parents(self, node_element, node_data): + parents = node_data.pop("parents", False) + if parents: + parents_element = Element("parents") + for p in parents: + e = Element("parent") + e.attrib["for"] = str(p) + parents_element.append(e) + node_element.append(parents_element) + return node_data + + def add_slices(self, node_or_edge_element, node_or_edge_data): + slices = node_or_edge_data.pop("slices", False) + if slices: + slices_element = Element("slices") + for start, end in slices: + e = Element("slice", start=str(start), end=str(end)) + slices_element.append(e) + node_or_edge_element.append(slices_element) + return node_or_edge_data + + def add_spells(self, node_or_edge_element, node_or_edge_data): + spells = node_or_edge_data.pop("spells", False) + if spells: + spells_element = Element("spells") + for start, end in spells: + e = Element("spell") + if start is not None: + e.attrib["start"] = str(start) + self.alter_graph_mode_timeformat(start) + if end is not None: + e.attrib["end"] = str(end) + self.alter_graph_mode_timeformat(end) + spells_element.append(e) + node_or_edge_element.append(spells_element) + return node_or_edge_data + + def alter_graph_mode_timeformat(self, start_or_end): + # If 'start' or 'end' appears, set timeformat + if start_or_end is not None: + if isinstance(start_or_end, str): + timeformat = "date" + elif isinstance(start_or_end, float): + timeformat = "double" + elif isinstance(start_or_end, int): + timeformat = "long" + else: + raise nx.NetworkXError( + "timeformat should be of the type int, float or str" + ) + self.graph_element.set("timeformat", timeformat) + # If Graph mode is static, alter to dynamic + if self.graph_element.get("mode") == "static": + self.graph_element.set("mode", "dynamic") + + def write(self, fh): + # Serialize graph G in GEXF to the open fh + if self.prettyprint: + self.indent(self.xml) + document = ElementTree(self.xml) + document.write(fh, encoding=self.encoding, xml_declaration=True) + + def indent(self, elem, level=0): + # in-place prettyprint formatter + i = "\n" + " " * level + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class GEXFReader(GEXF): + # Class to read GEXF format files + # use read_gexf() function + def __init__(self, node_type=None, version="1.2draft"): + self.construct_types() + self.node_type = node_type + # assume simple graph and test for multigraph on read + self.simple_graph = True + self.set_version(version) + + def __call__(self, stream): + self.xml = ElementTree(file=stream) + g = self.xml.find(f"{{{self.NS_GEXF}}}graph") + if g is not None: + return self.make_graph(g) + # try all the versions + for version in self.versions: + self.set_version(version) + g = self.xml.find(f"{{{self.NS_GEXF}}}graph") + if g is not None: + return self.make_graph(g) + raise nx.NetworkXError("No element in GEXF file.") + + def make_graph(self, graph_xml): + # start with empty DiGraph or MultiDiGraph + edgedefault = graph_xml.get("defaultedgetype", None) + if edgedefault == "directed": + G = nx.MultiDiGraph() + else: + G = nx.MultiGraph() + + # graph attributes + graph_name = graph_xml.get("name", "") + if graph_name != "": + G.graph["name"] = graph_name + graph_start = graph_xml.get("start") + if graph_start is not None: + G.graph["start"] = graph_start + graph_end = graph_xml.get("end") + if graph_end is not None: + G.graph["end"] = graph_end + graph_mode = graph_xml.get("mode", "") + if graph_mode == "dynamic": + G.graph["mode"] = "dynamic" + else: + G.graph["mode"] = "static" + + # timeformat + self.timeformat = graph_xml.get("timeformat") + if self.timeformat == "date": + self.timeformat = "string" + + # node and edge attributes + attributes_elements = graph_xml.findall(f"{{{self.NS_GEXF}}}attributes") + # dictionaries to hold attributes and attribute defaults + node_attr = {} + node_default = {} + edge_attr = {} + edge_default = {} + for a in attributes_elements: + attr_class = a.get("class") + if attr_class == "node": + na, nd = self.find_gexf_attributes(a) + node_attr.update(na) + node_default.update(nd) + G.graph["node_default"] = node_default + elif attr_class == "edge": + ea, ed = self.find_gexf_attributes(a) + edge_attr.update(ea) + edge_default.update(ed) + G.graph["edge_default"] = edge_default + else: + raise # unknown attribute class + + # Hack to handle Gephi0.7beta bug + # add weight attribute + ea = {"weight": {"type": "double", "mode": "static", "title": "weight"}} + ed = {} + edge_attr.update(ea) + edge_default.update(ed) + G.graph["edge_default"] = edge_default + + # add nodes + nodes_element = graph_xml.find(f"{{{self.NS_GEXF}}}nodes") + if nodes_element is not None: + for node_xml in nodes_element.findall(f"{{{self.NS_GEXF}}}node"): + self.add_node(G, node_xml, node_attr) + + # add edges + edges_element = graph_xml.find(f"{{{self.NS_GEXF}}}edges") + if edges_element is not None: + for edge_xml in edges_element.findall(f"{{{self.NS_GEXF}}}edge"): + self.add_edge(G, edge_xml, edge_attr) + + # switch to Graph or DiGraph if no parallel edges were found. + if self.simple_graph: + if G.is_directed(): + G = nx.DiGraph(G) + else: + G = nx.Graph(G) + return G + + def add_node(self, G, node_xml, node_attr, node_pid=None): + # add a single node with attributes to the graph + + # get attributes and subattributues for node + data = self.decode_attr_elements(node_attr, node_xml) + data = self.add_parents(data, node_xml) # add any parents + if self.VERSION == "1.1": + data = self.add_slices(data, node_xml) # add slices + else: + data = self.add_spells(data, node_xml) # add spells + data = self.add_viz(data, node_xml) # add viz + data = self.add_start_end(data, node_xml) # add start/end + + # find the node id and cast it to the appropriate type + node_id = node_xml.get("id") + if self.node_type is not None: + node_id = self.node_type(node_id) + + # every node should have a label + node_label = node_xml.get("label") + data["label"] = node_label + + # parent node id + node_pid = node_xml.get("pid", node_pid) + if node_pid is not None: + data["pid"] = node_pid + + # check for subnodes, recursive + subnodes = node_xml.find(f"{{{self.NS_GEXF}}}nodes") + if subnodes is not None: + for node_xml in subnodes.findall(f"{{{self.NS_GEXF}}}node"): + self.add_node(G, node_xml, node_attr, node_pid=node_id) + + G.add_node(node_id, **data) + + def add_start_end(self, data, xml): + # start and end times + ttype = self.timeformat + node_start = xml.get("start") + if node_start is not None: + data["start"] = self.python_type[ttype](node_start) + node_end = xml.get("end") + if node_end is not None: + data["end"] = self.python_type[ttype](node_end) + return data + + def add_viz(self, data, node_xml): + # add viz element for node + viz = {} + color = node_xml.find(f"{{{self.NS_VIZ}}}color") + if color is not None: + if self.VERSION == "1.1": + viz["color"] = { + "r": int(color.get("r")), + "g": int(color.get("g")), + "b": int(color.get("b")), + } + else: + viz["color"] = { + "r": int(color.get("r")), + "g": int(color.get("g")), + "b": int(color.get("b")), + "a": float(color.get("a", 1)), + } + + size = node_xml.find(f"{{{self.NS_VIZ}}}size") + if size is not None: + viz["size"] = float(size.get("value")) + + thickness = node_xml.find(f"{{{self.NS_VIZ}}}thickness") + if thickness is not None: + viz["thickness"] = float(thickness.get("value")) + + shape = node_xml.find(f"{{{self.NS_VIZ}}}shape") + if shape is not None: + viz["shape"] = shape.get("shape") + if viz["shape"] == "image": + viz["shape"] = shape.get("uri") + + position = node_xml.find(f"{{{self.NS_VIZ}}}position") + if position is not None: + viz["position"] = { + "x": float(position.get("x", 0)), + "y": float(position.get("y", 0)), + "z": float(position.get("z", 0)), + } + + if len(viz) > 0: + data["viz"] = viz + return data + + def add_parents(self, data, node_xml): + parents_element = node_xml.find(f"{{{self.NS_GEXF}}}parents") + if parents_element is not None: + data["parents"] = [] + for p in parents_element.findall(f"{{{self.NS_GEXF}}}parent"): + parent = p.get("for") + data["parents"].append(parent) + return data + + def add_slices(self, data, node_or_edge_xml): + slices_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}slices") + if slices_element is not None: + data["slices"] = [] + for s in slices_element.findall(f"{{{self.NS_GEXF}}}slice"): + start = s.get("start") + end = s.get("end") + data["slices"].append((start, end)) + return data + + def add_spells(self, data, node_or_edge_xml): + spells_element = node_or_edge_xml.find(f"{{{self.NS_GEXF}}}spells") + if spells_element is not None: + data["spells"] = [] + ttype = self.timeformat + for s in spells_element.findall(f"{{{self.NS_GEXF}}}spell"): + start = self.python_type[ttype](s.get("start")) + end = self.python_type[ttype](s.get("end")) + data["spells"].append((start, end)) + return data + + def add_edge(self, G, edge_element, edge_attr): + # add an edge to the graph + + # raise error if we find mixed directed and undirected edges + edge_direction = edge_element.get("type") + if G.is_directed() and edge_direction == "undirected": + raise nx.NetworkXError("Undirected edge found in directed graph.") + if (not G.is_directed()) and edge_direction == "directed": + raise nx.NetworkXError("Directed edge found in undirected graph.") + + # Get source and target and recast type if required + source = edge_element.get("source") + target = edge_element.get("target") + if self.node_type is not None: + source = self.node_type(source) + target = self.node_type(target) + + data = self.decode_attr_elements(edge_attr, edge_element) + data = self.add_start_end(data, edge_element) + + if self.VERSION == "1.1": + data = self.add_slices(data, edge_element) # add slices + else: + data = self.add_spells(data, edge_element) # add spells + + # GEXF stores edge ids as an attribute + # NetworkX uses them as keys in multigraphs + # if networkx_key is not specified as an attribute + edge_id = edge_element.get("id") + if edge_id is not None: + data["id"] = edge_id + + # check if there is a 'multigraph_key' and use that as edge_id + multigraph_key = data.pop("networkx_key", None) + if multigraph_key is not None: + edge_id = multigraph_key + + weight = edge_element.get("weight") + if weight is not None: + data["weight"] = float(weight) + + edge_label = edge_element.get("label") + if edge_label is not None: + data["label"] = edge_label + + if G.has_edge(source, target): + # seen this edge before - this is a multigraph + self.simple_graph = False + G.add_edge(source, target, key=edge_id, **data) + if edge_direction == "mutual": + G.add_edge(target, source, key=edge_id, **data) + + def decode_attr_elements(self, gexf_keys, obj_xml): + # Use the key information to decode the attr XML + attr = {} + # look for outer '' element + attr_element = obj_xml.find(f"{{{self.NS_GEXF}}}attvalues") + if attr_element is not None: + # loop over elements + for a in attr_element.findall(f"{{{self.NS_GEXF}}}attvalue"): + key = a.get("for") # for is required + try: # should be in our gexf_keys dictionary + title = gexf_keys[key]["title"] + except KeyError as err: + raise nx.NetworkXError(f"No attribute defined for={key}.") from err + atype = gexf_keys[key]["type"] + value = a.get("value") + if atype == "boolean": + value = self.convert_bool[value] + else: + value = self.python_type[atype](value) + if gexf_keys[key]["mode"] == "dynamic": + # for dynamic graphs use list of three-tuples + # [(value1,start1,end1), (value2,start2,end2), etc] + ttype = self.timeformat + start = self.python_type[ttype](a.get("start")) + end = self.python_type[ttype](a.get("end")) + if title in attr: + attr[title].append((value, start, end)) + else: + attr[title] = [(value, start, end)] + else: + # for static graphs just assign the value + attr[title] = value + return attr + + def find_gexf_attributes(self, attributes_element): + # Extract all the attributes and defaults + attrs = {} + defaults = {} + mode = attributes_element.get("mode") + for k in attributes_element.findall(f"{{{self.NS_GEXF}}}attribute"): + attr_id = k.get("id") + title = k.get("title") + atype = k.get("type") + attrs[attr_id] = {"title": title, "type": atype, "mode": mode} + # check for the 'default' subelement of key element and add + default = k.find(f"{{{self.NS_GEXF}}}default") + if default is not None: + if atype == "boolean": + value = self.convert_bool[default.text] + else: + value = self.python_type[atype](default.text) + defaults[title] = value + return attrs, defaults + + +def relabel_gexf_graph(G): + """Relabel graph using "label" node keyword for node label. + + Parameters + ---------- + G : graph + A NetworkX graph read from GEXF data + + Returns + ------- + H : graph + A NetworkX graph with relabeled nodes + + Raises + ------ + NetworkXError + If node labels are missing or not unique while relabel=True. + + Notes + ----- + This function relabels the nodes in a NetworkX graph with the + "label" attribute. It also handles relabeling the specific GEXF + node attributes "parents", and "pid". + """ + # build mapping of node labels, do some error checking + try: + mapping = [(u, G.nodes[u]["label"]) for u in G] + except KeyError as err: + raise nx.NetworkXError( + "Failed to relabel nodes: missing node labels found. Use relabel=False." + ) from err + x, y = zip(*mapping) + if len(set(y)) != len(G): + raise nx.NetworkXError( + "Failed to relabel nodes: duplicate node labels found. Use relabel=False." + ) + mapping = dict(mapping) + H = nx.relabel_nodes(G, mapping) + # relabel attributes + for n in G: + m = mapping[n] + H.nodes[m]["id"] = n + H.nodes[m].pop("label") + if "pid" in H.nodes[m]: + H.nodes[m]["pid"] = mapping[G.nodes[n]["pid"]] + if "parents" in H.nodes[m]: + H.nodes[m]["parents"] = [mapping[p] for p in G.nodes[n]["parents"]] + return H diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gml.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gml.py new file mode 100644 index 0000000000000000000000000000000000000000..c53496c3e7fd2797ce2f786e665d0ad069d8184c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/gml.py @@ -0,0 +1,879 @@ +""" +Read graphs in GML format. + +"GML, the Graph Modelling Language, is our proposal for a portable +file format for graphs. GML's key features are portability, simple +syntax, extensibility and flexibility. A GML file consists of a +hierarchical key-value lists. Graphs can be annotated with arbitrary +data structures. The idea for a common file format was born at the +GD'95; this proposal is the outcome of many discussions. GML is the +standard file format in the Graphlet graph editor system. It has been +overtaken and adapted by several other systems for drawing graphs." + +GML files are stored using a 7-bit ASCII encoding with any extended +ASCII characters (iso8859-1) appearing as HTML character entities. +You will need to give some thought into how the exported data should +interact with different languages and even different Python versions. +Re-importing from gml is also a concern. + +Without specifying a `stringizer`/`destringizer`, the code is capable of +writing `int`/`float`/`str`/`dict`/`list` data as required by the GML +specification. For writing other data types, and for reading data other +than `str` you need to explicitly supply a `stringizer`/`destringizer`. + +For additional documentation on the GML file format, please see the +`GML website `_. + +Several example graphs in GML format may be found on Mark Newman's +`Network data page `_. +""" + +import html.entities as htmlentitydefs +import re +from ast import literal_eval +from collections import defaultdict +from enum import Enum +from io import StringIO +from typing import Any, NamedTuple + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.utils import open_file + +__all__ = ["read_gml", "parse_gml", "generate_gml", "write_gml"] + + +def escape(text): + """Use XML character references to escape characters. + + Use XML character references for unprintable or non-ASCII + characters, double quotes and ampersands in a string + """ + + def fixup(m): + ch = m.group(0) + return "&#" + str(ord(ch)) + ";" + + text = re.sub('[^ -~]|[&"]', fixup, text) + return text if isinstance(text, str) else str(text) + + +def unescape(text): + """Replace XML character references with the referenced characters""" + + def fixup(m): + text = m.group(0) + if text[1] == "#": + # Character reference + if text[2] == "x": + code = int(text[3:-1], 16) + else: + code = int(text[2:-1]) + else: + # Named entity + try: + code = htmlentitydefs.name2codepoint[text[1:-1]] + except KeyError: + return text # leave unchanged + try: + return chr(code) + except (ValueError, OverflowError): + return text # leave unchanged + + return re.sub("&(?:[0-9A-Za-z]+|#(?:[0-9]+|x[0-9A-Fa-f]+));", fixup, text) + + +def literal_destringizer(rep): + """Convert a Python literal to the value it represents. + + Parameters + ---------- + rep : string + A Python literal. + + Returns + ------- + value : object + The value of the Python literal. + + Raises + ------ + ValueError + If `rep` is not a Python literal. + """ + if isinstance(rep, str): + orig_rep = rep + try: + return literal_eval(rep) + except SyntaxError as err: + raise ValueError(f"{orig_rep!r} is not a valid Python literal") from err + else: + raise ValueError(f"{rep!r} is not a string") + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_gml(path, label="label", destringizer=None): + """Read graph in GML format from `path`. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + label : string, optional + If not None, the parsed nodes will be renamed according to node + attributes indicated by `label`. Default value: 'label'. + + destringizer : callable, optional + A `destringizer` that recovers values stored as strings in GML. If it + cannot convert a string to a value, a `ValueError` is raised. Default + value : None. + + Returns + ------- + G : NetworkX graph + The parsed graph. + + Raises + ------ + NetworkXError + If the input cannot be parsed. + + See Also + -------- + write_gml, parse_gml + literal_destringizer + + Notes + ----- + GML files are stored using a 7-bit ASCII encoding with any extended + ASCII characters (iso8859-1) appearing as HTML character entities. + Without specifying a `stringizer`/`destringizer`, the code is capable of + writing `int`/`float`/`str`/`dict`/`list` data as required by the GML + specification. For writing other data types, and for reading data other + than `str` you need to explicitly supply a `stringizer`/`destringizer`. + + For additional documentation on the GML file format, please see the + `GML url `_. + + See the module docstring :mod:`networkx.readwrite.gml` for more details. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_gml(G, "test_path4.gml") + + GML values are interpreted as strings by default: + + >>> H = nx.read_gml("test_path4.gml") + >>> H.nodes + NodeView(('0', '1', '2', '3')) + + When a `destringizer` is provided, GML values are converted to the provided type. + For example, integer nodes can be recovered as shown below: + + >>> J = nx.read_gml("test_path4.gml", destringizer=int) + >>> J.nodes + NodeView((0, 1, 2, 3)) + + """ + + def filter_lines(lines): + for line in lines: + try: + line = line.decode("ascii") + except UnicodeDecodeError as err: + raise NetworkXError("input is not ASCII-encoded") from err + if not isinstance(line, str): + lines = str(lines) + if line and line[-1] == "\n": + line = line[:-1] + yield line + + G = parse_gml_lines(filter_lines(path), label, destringizer) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_gml(lines, label="label", destringizer=None): + """Parse GML graph from a string or iterable. + + Parameters + ---------- + lines : string or iterable of strings + Data in GML format. + + label : string, optional + If not None, the parsed nodes will be renamed according to node + attributes indicated by `label`. Default value: 'label'. + + destringizer : callable, optional + A `destringizer` that recovers values stored as strings in GML. If it + cannot convert a string to a value, a `ValueError` is raised. Default + value : None. + + Returns + ------- + G : NetworkX graph + The parsed graph. + + Raises + ------ + NetworkXError + If the input cannot be parsed. + + See Also + -------- + write_gml, read_gml + + Notes + ----- + This stores nested GML attributes as dictionaries in the NetworkX graph, + node, and edge attribute structures. + + GML files are stored using a 7-bit ASCII encoding with any extended + ASCII characters (iso8859-1) appearing as HTML character entities. + Without specifying a `stringizer`/`destringizer`, the code is capable of + writing `int`/`float`/`str`/`dict`/`list` data as required by the GML + specification. For writing other data types, and for reading data other + than `str` you need to explicitly supply a `stringizer`/`destringizer`. + + For additional documentation on the GML file format, please see the + `GML url `_. + + See the module docstring :mod:`networkx.readwrite.gml` for more details. + """ + + def decode_line(line): + if isinstance(line, bytes): + try: + line.decode("ascii") + except UnicodeDecodeError as err: + raise NetworkXError("input is not ASCII-encoded") from err + if not isinstance(line, str): + line = str(line) + return line + + def filter_lines(lines): + if isinstance(lines, str): + lines = decode_line(lines) + lines = lines.splitlines() + yield from lines + else: + for line in lines: + line = decode_line(line) + if line and line[-1] == "\n": + line = line[:-1] + if line.find("\n") != -1: + raise NetworkXError("input line contains newline") + yield line + + G = parse_gml_lines(filter_lines(lines), label, destringizer) + return G + + +class Pattern(Enum): + """encodes the index of each token-matching pattern in `tokenize`.""" + + KEYS = 0 + REALS = 1 + INTS = 2 + STRINGS = 3 + DICT_START = 4 + DICT_END = 5 + COMMENT_WHITESPACE = 6 + + +class Token(NamedTuple): + category: Pattern + value: Any + line: int + position: int + + +LIST_START_VALUE = "_networkx_list_start" + + +def parse_gml_lines(lines, label, destringizer): + """Parse GML `lines` into a graph.""" + + def tokenize(): + patterns = [ + r"[A-Za-z][0-9A-Za-z_]*\b", # keys + # reals + r"[+-]?(?:[0-9]*\.[0-9]+|[0-9]+\.[0-9]*|INF)(?:[Ee][+-]?[0-9]+)?", + r"[+-]?[0-9]+", # ints + r'".*?"', # strings + r"\[", # dict start + r"\]", # dict end + r"#.*$|\s+", # comments and whitespaces + ] + tokens = re.compile("|".join(f"({pattern})" for pattern in patterns)) + lineno = 0 + multilines = [] # entries spread across multiple lines + for line in lines: + pos = 0 + + # deal with entries spread across multiple lines + # + # should we actually have to deal with escaped "s then do it here + if multilines: + multilines.append(line.strip()) + if line[-1] == '"': # closing multiline entry + # multiline entries will be joined by space. cannot + # reintroduce newlines as this will break the tokenizer + line = " ".join(multilines) + multilines = [] + else: # continued multiline entry + lineno += 1 + continue + else: + if line.count('"') == 1: # opening multiline entry + if line.strip()[0] != '"' and line.strip()[-1] != '"': + # since we expect something like key "value", the " should not be found at ends + # otherwise tokenizer will pick up the formatting mistake. + multilines = [line.rstrip()] + lineno += 1 + continue + + length = len(line) + + while pos < length: + match = tokens.match(line, pos) + if match is None: + m = f"cannot tokenize {line[pos:]} at ({lineno + 1}, {pos + 1})" + raise NetworkXError(m) + for i in range(len(patterns)): + group = match.group(i + 1) + if group is not None: + if i == 0: # keys + value = group.rstrip() + elif i == 1: # reals + value = float(group) + elif i == 2: # ints + value = int(group) + else: + value = group + if i != 6: # comments and whitespaces + yield Token(Pattern(i), value, lineno + 1, pos + 1) + pos += len(group) + break + lineno += 1 + yield Token(None, None, lineno + 1, 1) # EOF + + def unexpected(curr_token, expected): + category, value, lineno, pos = curr_token + value = repr(value) if value is not None else "EOF" + raise NetworkXError(f"expected {expected}, found {value} at ({lineno}, {pos})") + + def consume(curr_token, category, expected): + if curr_token.category == category: + return next(tokens) + unexpected(curr_token, expected) + + def parse_kv(curr_token): + dct = defaultdict(list) + while curr_token.category == Pattern.KEYS: + key = curr_token.value + curr_token = next(tokens) + category = curr_token.category + if category == Pattern.REALS or category == Pattern.INTS: + value = curr_token.value + curr_token = next(tokens) + elif category == Pattern.STRINGS: + value = unescape(curr_token.value[1:-1]) + if destringizer: + try: + value = destringizer(value) + except ValueError: + pass + # Special handling for empty lists and tuples + if value == "()": + value = () + if value == "[]": + value = [] + curr_token = next(tokens) + elif category == Pattern.DICT_START: + curr_token, value = parse_dict(curr_token) + else: + # Allow for string convertible id and label values + if key in ("id", "label", "source", "target"): + try: + # String convert the token value + value = unescape(str(curr_token.value)) + if destringizer: + try: + value = destringizer(value) + except ValueError: + pass + curr_token = next(tokens) + except Exception: + msg = ( + "an int, float, string, '[' or string" + + " convertible ASCII value for node id or label" + ) + unexpected(curr_token, msg) + # Special handling for nan and infinity. Since the gml language + # defines unquoted strings as keys, the numeric and string branches + # are skipped and we end up in this special branch, so we need to + # convert the current token value to a float for NAN and plain INF. + # +/-INF are handled in the pattern for 'reals' in tokenize(). This + # allows labels and values to be nan or infinity, but not keys. + elif curr_token.value in {"NAN", "INF"}: + value = float(curr_token.value) + curr_token = next(tokens) + else: # Otherwise error out + unexpected(curr_token, "an int, float, string or '['") + dct[key].append(value) + + def clean_dict_value(value): + if not isinstance(value, list): + return value + if len(value) == 1: + return value[0] + if value[0] == LIST_START_VALUE: + return value[1:] + return value + + dct = {key: clean_dict_value(value) for key, value in dct.items()} + return curr_token, dct + + def parse_dict(curr_token): + # dict start + curr_token = consume(curr_token, Pattern.DICT_START, "'['") + # dict contents + curr_token, dct = parse_kv(curr_token) + # dict end + curr_token = consume(curr_token, Pattern.DICT_END, "']'") + return curr_token, dct + + def parse_graph(): + curr_token, dct = parse_kv(next(tokens)) + if curr_token.category is not None: # EOF + unexpected(curr_token, "EOF") + if "graph" not in dct: + raise NetworkXError("input contains no graph") + graph = dct["graph"] + if isinstance(graph, list): + raise NetworkXError("input contains more than one graph") + return graph + + tokens = tokenize() + graph = parse_graph() + + directed = graph.pop("directed", False) + multigraph = graph.pop("multigraph", False) + if not multigraph: + G = nx.DiGraph() if directed else nx.Graph() + else: + G = nx.MultiDiGraph() if directed else nx.MultiGraph() + graph_attr = {k: v for k, v in graph.items() if k not in ("node", "edge")} + G.graph.update(graph_attr) + + def pop_attr(dct, category, attr, i): + try: + return dct.pop(attr) + except KeyError as err: + raise NetworkXError(f"{category} #{i} has no {attr!r} attribute") from err + + nodes = graph.get("node", []) + mapping = {} + node_labels = set() + for i, node in enumerate(nodes if isinstance(nodes, list) else [nodes]): + id = pop_attr(node, "node", "id", i) + if id in G: + raise NetworkXError(f"node id {id!r} is duplicated") + if label is not None and label != "id": + node_label = pop_attr(node, "node", label, i) + if node_label in node_labels: + raise NetworkXError(f"node label {node_label!r} is duplicated") + node_labels.add(node_label) + mapping[id] = node_label + G.add_node(id, **node) + + edges = graph.get("edge", []) + for i, edge in enumerate(edges if isinstance(edges, list) else [edges]): + source = pop_attr(edge, "edge", "source", i) + target = pop_attr(edge, "edge", "target", i) + if source not in G: + raise NetworkXError(f"edge #{i} has undefined source {source!r}") + if target not in G: + raise NetworkXError(f"edge #{i} has undefined target {target!r}") + if not multigraph: + if not G.has_edge(source, target): + G.add_edge(source, target, **edge) + else: + arrow = "->" if directed else "--" + msg = f"edge #{i} ({source!r}{arrow}{target!r}) is duplicated" + raise nx.NetworkXError(msg) + else: + key = edge.pop("key", None) + if key is not None and G.has_edge(source, target, key): + arrow = "->" if directed else "--" + msg = f"edge #{i} ({source!r}{arrow}{target!r}, {key!r})" + msg2 = 'Hint: If multigraph add "multigraph 1" to file header.' + raise nx.NetworkXError(msg + " is duplicated\n" + msg2) + G.add_edge(source, target, key, **edge) + + if label is not None and label != "id": + G = nx.relabel_nodes(G, mapping) + return G + + +def literal_stringizer(value): + """Convert a `value` to a Python literal in GML representation. + + Parameters + ---------- + value : object + The `value` to be converted to GML representation. + + Returns + ------- + rep : string + A double-quoted Python literal representing value. Unprintable + characters are replaced by XML character references. + + Raises + ------ + ValueError + If `value` cannot be converted to GML. + + Notes + ----- + The original value can be recovered using the + :func:`networkx.readwrite.gml.literal_destringizer` function. + """ + + def stringize(value): + if isinstance(value, int | bool) or value is None: + if value is True: # GML uses 1/0 for boolean values. + buf.write(str(1)) + elif value is False: + buf.write(str(0)) + else: + buf.write(str(value)) + elif isinstance(value, str): + text = repr(value) + if text[0] != "u": + try: + value.encode("latin1") + except UnicodeEncodeError: + text = "u" + text + buf.write(text) + elif isinstance(value, float | complex | str | bytes): + buf.write(repr(value)) + elif isinstance(value, list): + buf.write("[") + first = True + for item in value: + if not first: + buf.write(",") + else: + first = False + stringize(item) + buf.write("]") + elif isinstance(value, tuple): + if len(value) > 1: + buf.write("(") + first = True + for item in value: + if not first: + buf.write(",") + else: + first = False + stringize(item) + buf.write(")") + elif value: + buf.write("(") + stringize(value[0]) + buf.write(",)") + else: + buf.write("()") + elif isinstance(value, dict): + buf.write("{") + first = True + for key, value in value.items(): + if not first: + buf.write(",") + else: + first = False + stringize(key) + buf.write(":") + stringize(value) + buf.write("}") + elif isinstance(value, set): + buf.write("{") + first = True + for item in value: + if not first: + buf.write(",") + else: + first = False + stringize(item) + buf.write("}") + else: + msg = f"{value!r} cannot be converted into a Python literal" + raise ValueError(msg) + + buf = StringIO() + stringize(value) + return buf.getvalue() + + +def generate_gml(G, stringizer=None): + r"""Generate a single entry of the graph `G` in GML format. + + Parameters + ---------- + G : NetworkX graph + The graph to be converted to GML. + + stringizer : callable, optional + A `stringizer` which converts non-int/non-float/non-dict values into + strings. If it cannot convert a value into a string, it should raise a + `ValueError` to indicate that. Default value: None. + + Returns + ------- + lines: generator of strings + Lines of GML data. Newlines are not appended. + + Raises + ------ + NetworkXError + If `stringizer` cannot convert a value into a string, or the value to + convert is not a string while `stringizer` is None. + + See Also + -------- + literal_stringizer + + Notes + ----- + Graph attributes named 'directed', 'multigraph', 'node' or + 'edge', node attributes named 'id' or 'label', edge attributes + named 'source' or 'target' (or 'key' if `G` is a multigraph) + are ignored because these attribute names are used to encode the graph + structure. + + GML files are stored using a 7-bit ASCII encoding with any extended + ASCII characters (iso8859-1) appearing as HTML character entities. + Without specifying a `stringizer`/`destringizer`, the code is capable of + writing `int`/`float`/`str`/`dict`/`list` data as required by the GML + specification. For writing other data types, and for reading data other + than `str` you need to explicitly supply a `stringizer`/`destringizer`. + + For additional documentation on the GML file format, please see the + `GML url `_. + + See the module docstring :mod:`networkx.readwrite.gml` for more details. + + Examples + -------- + >>> G = nx.Graph() + >>> G.add_node("1") + >>> print("\n".join(nx.generate_gml(G))) + graph [ + node [ + id 0 + label "1" + ] + ] + >>> G = nx.MultiGraph([("a", "b"), ("a", "b")]) + >>> print("\n".join(nx.generate_gml(G))) + graph [ + multigraph 1 + node [ + id 0 + label "a" + ] + node [ + id 1 + label "b" + ] + edge [ + source 0 + target 1 + key 0 + ] + edge [ + source 0 + target 1 + key 1 + ] + ] + """ + valid_keys = re.compile("^[A-Za-z][0-9A-Za-z_]*$") + + def stringize(key, value, ignored_keys, indent, in_list=False): + if not isinstance(key, str): + raise NetworkXError(f"{key!r} is not a string") + if not valid_keys.match(key): + raise NetworkXError(f"{key!r} is not a valid key") + if not isinstance(key, str): + key = str(key) + if key not in ignored_keys: + if isinstance(value, int | bool): + if key == "label": + yield indent + key + ' "' + str(value) + '"' + elif value is True: + # python bool is an instance of int + yield indent + key + " 1" + elif value is False: + yield indent + key + " 0" + # GML only supports signed 32-bit integers + elif value < -(2**31) or value >= 2**31: + yield indent + key + ' "' + str(value) + '"' + else: + yield indent + key + " " + str(value) + elif isinstance(value, float): + text = repr(value).upper() + # GML matches INF to keys, so prepend + to INF. Use repr(float(*)) + # instead of string literal to future proof against changes to repr. + if text == repr(float("inf")).upper(): + text = "+" + text + else: + # GML requires that a real literal contain a decimal point, but + # repr may not output a decimal point when the mantissa is + # integral and hence needs fixing. + epos = text.rfind("E") + if epos != -1 and text.find(".", 0, epos) == -1: + text = text[:epos] + "." + text[epos:] + if key == "label": + yield indent + key + ' "' + text + '"' + else: + yield indent + key + " " + text + elif isinstance(value, dict): + yield indent + key + " [" + next_indent = indent + " " + for key, value in value.items(): + yield from stringize(key, value, (), next_indent) + yield indent + "]" + elif isinstance(value, tuple) and key == "label": + yield indent + key + f' "({",".join(repr(v) for v in value)})"' + elif isinstance(value, list | tuple) and key != "label" and not in_list: + if len(value) == 0: + yield indent + key + " " + f'"{value!r}"' + if len(value) == 1: + yield indent + key + " " + f'"{LIST_START_VALUE}"' + for val in value: + yield from stringize(key, val, (), indent, True) + else: + if stringizer: + try: + value = stringizer(value) + except ValueError as err: + raise NetworkXError( + f"{value!r} cannot be converted into a string" + ) from err + if not isinstance(value, str): + raise NetworkXError(f"{value!r} is not a string") + yield indent + key + ' "' + escape(value) + '"' + + multigraph = G.is_multigraph() + yield "graph [" + + # Output graph attributes + if G.is_directed(): + yield " directed 1" + if multigraph: + yield " multigraph 1" + ignored_keys = {"directed", "multigraph", "node", "edge"} + for attr, value in G.graph.items(): + yield from stringize(attr, value, ignored_keys, " ") + + # Output node data + node_id = dict(zip(G, range(len(G)))) + ignored_keys = {"id", "label"} + for node, attrs in G.nodes.items(): + yield " node [" + yield " id " + str(node_id[node]) + yield from stringize("label", node, (), " ") + for attr, value in attrs.items(): + yield from stringize(attr, value, ignored_keys, " ") + yield " ]" + + # Output edge data + ignored_keys = {"source", "target"} + kwargs = {"data": True} + if multigraph: + ignored_keys.add("key") + kwargs["keys"] = True + for e in G.edges(**kwargs): + yield " edge [" + yield " source " + str(node_id[e[0]]) + yield " target " + str(node_id[e[1]]) + if multigraph: + yield from stringize("key", e[2], (), " ") + for attr, value in e[-1].items(): + yield from stringize(attr, value, ignored_keys, " ") + yield " ]" + yield "]" + + +@open_file(1, mode="wb") +def write_gml(G, path, stringizer=None): + """Write a graph `G` in GML format to the file or file handle `path`. + + Parameters + ---------- + G : NetworkX graph + The graph to be converted to GML. + + path : string or file + Filename or file handle to write to. + Filenames ending in .gz or .bz2 will be compressed. + + stringizer : callable, optional + A `stringizer` which converts non-int/non-float/non-dict values into + strings. If it cannot convert a value into a string, it should raise a + `ValueError` to indicate that. Default value: None. + + Raises + ------ + NetworkXError + If `stringizer` cannot convert a value into a string, or the value to + convert is not a string while `stringizer` is None. + + See Also + -------- + read_gml, generate_gml + literal_stringizer + + Notes + ----- + Graph attributes named 'directed', 'multigraph', 'node' or + 'edge', node attributes named 'id' or 'label', edge attributes + named 'source' or 'target' (or 'key' if `G` is a multigraph) + are ignored because these attribute names are used to encode the graph + structure. + + GML files are stored using a 7-bit ASCII encoding with any extended + ASCII characters (iso8859-1) appearing as HTML character entities. + Without specifying a `stringizer`/`destringizer`, the code is capable of + writing `int`/`float`/`str`/`dict`/`list` data as required by the GML + specification. For writing other data types, and for reading data other + than `str` you need to explicitly supply a `stringizer`/`destringizer`. + + Note that while we allow non-standard GML to be read from a file, we make + sure to write GML format. In particular, underscores are not allowed in + attribute names. + For additional documentation on the GML file format, please see the + `GML url `_. + + See the module docstring :mod:`networkx.readwrite.gml` for more details. + + Examples + -------- + >>> G = nx.path_graph(5) + >>> nx.write_gml(G, "test_path5.gml") + + Filenames ending in .gz or .bz2 will be compressed. + + >>> nx.write_gml(G, "test_path5.gml.gz") + """ + for line in generate_gml(G, stringizer): + path.write((line + "\n").encode("ascii")) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graph6.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graph6.py new file mode 100644 index 0000000000000000000000000000000000000000..cdc2925e5fb264a3d3e3235571682e38530d7f69 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graph6.py @@ -0,0 +1,427 @@ +# Original author: D. Eppstein, UC Irvine, August 12, 2003. +# The original code at http://www.ics.uci.edu/~eppstein/PADS/ is public domain. +"""Functions for reading and writing graphs in the *graph6* format. + +The *graph6* file format is suitable for small graphs or large dense +graphs. For large sparse graphs, use the *sparse6* format. + +For more information, see the `graph6`_ homepage. + +.. _graph6: http://users.cecs.anu.edu.au/~bdm/data/formats.html + +""" + +from itertools import islice + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.utils import not_implemented_for, open_file + +__all__ = ["from_graph6_bytes", "read_graph6", "to_graph6_bytes", "write_graph6"] + + +def _generate_graph6_bytes(G, nodes, header): + """Yield bytes in the graph6 encoding of a graph. + + `G` is an undirected simple graph. `nodes` is the list of nodes for + which the node-induced subgraph will be encoded; if `nodes` is the + list of all nodes in the graph, the entire graph will be + encoded. `header` is a Boolean that specifies whether to generate + the header ``b'>>graph6<<'`` before the remaining data. + + This function generates `bytes` objects in the following order: + + 1. the header (if requested), + 2. the encoding of the number of nodes, + 3. each character, one-at-a-time, in the encoding of the requested + node-induced subgraph, + 4. a newline character. + + This function raises :exc:`ValueError` if the graph is too large for + the graph6 format (that is, greater than ``2 ** 36`` nodes). + + """ + n = len(G) + if n >= 2**36: + raise ValueError( + "graph6 is only defined if number of nodes is less than 2 ** 36" + ) + if header: + yield b">>graph6<<" + for d in n_to_data(n): + yield str.encode(chr(d + 63)) + # This generates the same as `(v in G[u] for u, v in combinations(G, 2))`, + # but in "column-major" order instead of "row-major" order. + bits = (nodes[j] in G[nodes[i]] for j in range(1, n) for i in range(j)) + chunk = list(islice(bits, 6)) + while chunk: + d = sum(b << 5 - i for i, b in enumerate(chunk)) + yield str.encode(chr(d + 63)) + chunk = list(islice(bits, 6)) + yield b"\n" + + +@nx._dispatchable(graphs=None, returns_graph=True) +def from_graph6_bytes(bytes_in): + """Read a simple undirected graph in graph6 format from bytes. + + Parameters + ---------- + bytes_in : bytes + Data in graph6 format + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If `bytes_in` is unable to be parsed in graph6 format + + ValueError + If any character ``c`` in bytes_in does not satisfy + ``63 <= ord(c) < 127``. + + Examples + -------- + >>> G = nx.from_graph6_bytes(b"A_") + >>> sorted(G.edges()) + [(0, 1)] + + Notes + ----- + Per the graph6 spec, the header (e.g. ``b'>>graph6<<'``) must not be + followed by a newline character. + + See Also + -------- + read_graph6, write_graph6 + + References + ---------- + .. [1] Graph6 specification + + + """ + + def bits(): + """Returns sequence of individual bits from 6-bit-per-value + list of data values.""" + for d in data: + for i in [5, 4, 3, 2, 1, 0]: + yield (d >> i) & 1 + + # Ignore trailing newline + bytes_in = bytes_in.rstrip(b"\n") + + if bytes_in.startswith(b">>graph6<<"): + bytes_in = bytes_in[10:] + + data = [c - 63 for c in bytes_in] + if any(c > 63 for c in data): + raise ValueError("each input character must be in range(63, 127)") + + n, data = data_to_n(data) + nd = (n * (n - 1) // 2 + 5) // 6 + if len(data) != nd: + raise NetworkXError( + f"Expected {n * (n - 1) // 2} bits but got {len(data) * 6} in graph6" + ) + + G = nx.Graph() + G.add_nodes_from(range(n)) + for (i, j), b in zip(((i, j) for j in range(1, n) for i in range(j)), bits()): + if b: + G.add_edge(i, j) + + return G + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +def to_graph6_bytes(G, nodes=None, header=True): + """Convert a simple undirected graph to bytes in graph6 format. + + Parameters + ---------- + G : Graph (undirected) + + nodes: list or iterable + Nodes are labeled 0...n-1 in the order provided. If None the ordering + given by ``G.nodes()`` is used. + + header: bool + If True add '>>graph6<<' bytes to head of data. + + Raises + ------ + NetworkXNotImplemented + If the graph is directed or is a multigraph. + + ValueError + If the graph has at least ``2 ** 36`` nodes; the graph6 format + is only defined for graphs of order less than ``2 ** 36``. + + Examples + -------- + >>> nx.to_graph6_bytes(nx.path_graph(2)) + b'>>graph6< + + """ + if nodes is not None: + G = G.subgraph(nodes) + H = nx.convert_node_labels_to_integers(G) + nodes = sorted(H.nodes()) + return b"".join(_generate_graph6_bytes(H, nodes, header)) + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_graph6(path): + """Read simple undirected graphs in graph6 format from path. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + G : Graph or list of Graphs + If the file contains multiple lines then a list of graphs is returned + + Raises + ------ + NetworkXError + If the string is unable to be parsed in graph6 format + + Examples + -------- + You can read a graph6 file by giving the path to the file:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile(delete=False) as f: + ... _ = f.write(b">>graph6<>> list(G.edges()) + [(0, 1)] + + You can also read a graph6 file by giving an open file-like object:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile() as f: + ... _ = f.write(b">>graph6<>> list(G.edges()) + [(0, 1)] + + See Also + -------- + from_graph6_bytes, write_graph6 + + References + ---------- + .. [1] Graph6 specification + + + """ + glist = [] + for line in path: + line = line.strip() + if not len(line): + continue + glist.append(from_graph6_bytes(line)) + if len(glist) == 1: + return glist[0] + else: + return glist + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +@open_file(1, mode="wb") +def write_graph6(G, path, nodes=None, header=True): + """Write a simple undirected graph to a path in graph6 format. + + Parameters + ---------- + G : Graph (undirected) + + path : file or string + File or filename to write. + Filenames ending in .gz or .bz2 will be compressed. + + nodes: list or iterable + Nodes are labeled 0...n-1 in the order provided. If None the ordering + given by ``G.nodes()`` is used. + + header: bool + If True add '>>graph6<<' string to head of data + + Raises + ------ + NetworkXNotImplemented + If the graph is directed or is a multigraph. + + ValueError + If the graph has at least ``2 ** 36`` nodes; the graph6 format + is only defined for graphs of order less than ``2 ** 36``. + + Examples + -------- + You can write a graph6 file by giving the path to a file:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile(delete=False) as f: + ... nx.write_graph6(nx.path_graph(2), f.name) + ... _ = f.seek(0) + ... print(f.read()) + b'>>graph6< + + """ + return write_graph6_file(G, path, nodes=nodes, header=header) + + +@not_implemented_for("directed") +@not_implemented_for("multigraph") +def write_graph6_file(G, f, nodes=None, header=True): + """Write a simple undirected graph to a file-like object in graph6 format. + + Parameters + ---------- + G : Graph (undirected) + + f : file-like object + The file to write. + + nodes: list or iterable + Nodes are labeled 0...n-1 in the order provided. If None the ordering + given by ``G.nodes()`` is used. + + header: bool + If True add '>>graph6<<' string to head of data + + Raises + ------ + NetworkXNotImplemented + If the graph is directed or is a multigraph. + + ValueError + If the graph has at least ``2 ** 36`` nodes; the graph6 format + is only defined for graphs of order less than ``2 ** 36``. + + Examples + -------- + You can write a graph6 file by giving an open file-like object:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile() as f: + ... nx.write_graph6(nx.path_graph(2), f) + ... _ = f.seek(0) + ... print(f.read()) + b'>>graph6< + + """ + if nodes is not None: + G = G.subgraph(nodes) + H = nx.convert_node_labels_to_integers(G) + nodes = sorted(H.nodes()) + for b in _generate_graph6_bytes(H, nodes, header): + f.write(b) + + +def data_to_n(data): + """Read initial one-, four- or eight-unit value from graph6 + integer sequence. + + Return (value, rest of seq.)""" + if data[0] <= 62: + return data[0], data[1:] + if data[1] <= 62: + return (data[1] << 12) + (data[2] << 6) + data[3], data[4:] + return ( + (data[2] << 30) + + (data[3] << 24) + + (data[4] << 18) + + (data[5] << 12) + + (data[6] << 6) + + data[7], + data[8:], + ) + + +def n_to_data(n): + """Convert an integer to one-, four- or eight-unit graph6 sequence. + + This function is undefined if `n` is not in ``range(2 ** 36)``. + + """ + if n <= 62: + return [n] + elif n <= 258047: + return [63, (n >> 12) & 0x3F, (n >> 6) & 0x3F, n & 0x3F] + else: # if n <= 68719476735: + return [ + 63, + 63, + (n >> 30) & 0x3F, + (n >> 24) & 0x3F, + (n >> 18) & 0x3F, + (n >> 12) & 0x3F, + (n >> 6) & 0x3F, + n & 0x3F, + ] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graphml.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graphml.py new file mode 100644 index 0000000000000000000000000000000000000000..6ca1741452c963d99ffb8bfad2fb09eab8badd6b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/graphml.py @@ -0,0 +1,1053 @@ +""" +******* +GraphML +******* +Read and write graphs in GraphML format. + +.. warning:: + + This parser uses the standard xml library present in Python, which is + insecure - see :external+python:mod:`xml` for additional information. + Only parse GraphML files you trust. + +This implementation does not support mixed graphs (directed and unidirected +edges together), hyperedges, nested graphs, or ports. + +"GraphML is a comprehensive and easy-to-use file format for graphs. It +consists of a language core to describe the structural properties of a +graph and a flexible extension mechanism to add application-specific +data. Its main features include support of + + * directed, undirected, and mixed graphs, + * hypergraphs, + * hierarchical graphs, + * graphical representations, + * references to external data, + * application-specific attribute data, and + * light-weight parsers. + +Unlike many other file formats for graphs, GraphML does not use a +custom syntax. Instead, it is based on XML and hence ideally suited as +a common denominator for all kinds of services generating, archiving, +or processing graphs." + +http://graphml.graphdrawing.org/ + +Format +------ +GraphML is an XML format. See +http://graphml.graphdrawing.org/specification.html for the specification and +http://graphml.graphdrawing.org/primer/graphml-primer.html +for examples. +""" + +import warnings +from collections import defaultdict + +import networkx as nx +from networkx.utils import open_file + +__all__ = [ + "write_graphml", + "read_graphml", + "generate_graphml", + "write_graphml_xml", + "write_graphml_lxml", + "parse_graphml", + "GraphMLWriter", + "GraphMLReader", +] + + +@open_file(1, mode="wb") +def write_graphml_xml( + G, + path, + encoding="utf-8", + prettyprint=True, + infer_numeric_types=False, + named_key_ids=False, + edge_id_from_attribute=None, +): + """Write G in GraphML XML format to path + + Parameters + ---------- + G : graph + A networkx graph + path : file or string + File or filename to write. + Filenames ending in .gz or .bz2 will be compressed. + encoding : string (optional) + Encoding for text data. + prettyprint : bool (optional) + If True use line breaks and indenting in output XML. + infer_numeric_types : boolean + Determine if numeric types should be generalized. + For example, if edges have both int and float 'weight' attributes, + we infer in GraphML that both are floats. + named_key_ids : bool (optional) + If True use attr.name as value for key elements' id attribute. + edge_id_from_attribute : dict key (optional) + If provided, the graphml edge id is set by looking up the corresponding + edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, + the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_graphml(G, "test.graphml") + + Notes + ----- + This implementation does not support mixed graphs (directed + and unidirected edges together) hyperedges, nested graphs, or ports. + """ + writer = GraphMLWriter( + encoding=encoding, + prettyprint=prettyprint, + infer_numeric_types=infer_numeric_types, + named_key_ids=named_key_ids, + edge_id_from_attribute=edge_id_from_attribute, + ) + writer.add_graph_element(G) + writer.dump(path) + + +@open_file(1, mode="wb") +def write_graphml_lxml( + G, + path, + encoding="utf-8", + prettyprint=True, + infer_numeric_types=False, + named_key_ids=False, + edge_id_from_attribute=None, +): + """Write G in GraphML XML format to path + + This function uses the LXML framework and should be faster than + the version using the xml library. + + Parameters + ---------- + G : graph + A networkx graph + path : file or string + File or filename to write. + Filenames ending in .gz or .bz2 will be compressed. + encoding : string (optional) + Encoding for text data. + prettyprint : bool (optional) + If True use line breaks and indenting in output XML. + infer_numeric_types : boolean + Determine if numeric types should be generalized. + For example, if edges have both int and float 'weight' attributes, + we infer in GraphML that both are floats. + named_key_ids : bool (optional) + If True use attr.name as value for key elements' id attribute. + edge_id_from_attribute : dict key (optional) + If provided, the graphml edge id is set by looking up the corresponding + edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, + the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_graphml_lxml(G, "fourpath.graphml") + + Notes + ----- + This implementation does not support mixed graphs (directed + and unidirected edges together) hyperedges, nested graphs, or ports. + """ + try: + import lxml.etree as lxmletree + except ImportError: + return write_graphml_xml( + G, + path, + encoding, + prettyprint, + infer_numeric_types, + named_key_ids, + edge_id_from_attribute, + ) + + writer = GraphMLWriterLxml( + path, + graph=G, + encoding=encoding, + prettyprint=prettyprint, + infer_numeric_types=infer_numeric_types, + named_key_ids=named_key_ids, + edge_id_from_attribute=edge_id_from_attribute, + ) + writer.dump() + + +def generate_graphml( + G, + encoding="utf-8", + prettyprint=True, + named_key_ids=False, + edge_id_from_attribute=None, +): + """Generate GraphML lines for G + + Parameters + ---------- + G : graph + A networkx graph + encoding : string (optional) + Encoding for text data. + prettyprint : bool (optional) + If True use line breaks and indenting in output XML. + named_key_ids : bool (optional) + If True use attr.name as value for key elements' id attribute. + edge_id_from_attribute : dict key (optional) + If provided, the graphml edge id is set by looking up the corresponding + edge data attribute keyed by this parameter. If `None` or the key does not exist in edge data, + the edge id is set by the edge key if `G` is a MultiGraph, else the edge id is left unset. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> linefeed = chr(10) # linefeed = \n + >>> s = linefeed.join(nx.generate_graphml(G)) + >>> for line in nx.generate_graphml(G): # doctest: +SKIP + ... print(line) + + Notes + ----- + This implementation does not support mixed graphs (directed and unidirected + edges together) hyperedges, nested graphs, or ports. + """ + writer = GraphMLWriter( + encoding=encoding, + prettyprint=prettyprint, + named_key_ids=named_key_ids, + edge_id_from_attribute=edge_id_from_attribute, + ) + writer.add_graph_element(G) + yield from str(writer).splitlines() + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_graphml(path, node_type=str, edge_key_type=int, force_multigraph=False): + """Read graph in GraphML format from path. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + node_type: Python type (default: str) + Convert node ids to this type + + edge_key_type: Python type (default: int) + Convert graphml edge ids to this type. Multigraphs use id as edge key. + Non-multigraphs add to edge attribute dict with name "id". + + force_multigraph : bool (default: False) + If True, return a multigraph with edge keys. If False (the default) + return a multigraph when multiedges are in the graph. + + Returns + ------- + graph: NetworkX graph + If parallel edges are present or `force_multigraph=True` then + a MultiGraph or MultiDiGraph is returned. Otherwise a Graph/DiGraph. + The returned graph is directed if the file indicates it should be. + + Notes + ----- + Default node and edge attributes are not propagated to each node and edge. + They can be obtained from `G.graph` and applied to node and edge attributes + if desired using something like this: + + >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP + >>> for node, data in G.nodes(data=True): # doctest: +SKIP + ... if "color" not in data: + ... data["color"] = default_color + >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP + >>> for u, v, data in G.edges(data=True): # doctest: +SKIP + ... if "color" not in data: + ... data["color"] = default_color + + This implementation does not support mixed graphs (directed and unidirected + edges together), hypergraphs, nested graphs, or ports. + + For multigraphs the GraphML edge "id" will be used as the edge + key. If not specified then they "key" attribute will be used. If + there is no "key" attribute a default NetworkX multigraph edge key + will be provided. + + Files with the yEd "yfiles" extension can be read. The type of the node's + shape is preserved in the `shape_type` node attribute. + + yEd compressed files ("file.graphmlz" extension) can be read by renaming + the file to "file.graphml.gz". + + """ + reader = GraphMLReader(node_type, edge_key_type, force_multigraph) + # need to check for multiple graphs + glist = list(reader(path=path)) + if len(glist) == 0: + # If no graph comes back, try looking for an incomplete header + header = b'' + path.seek(0) + old_bytes = path.read() + new_bytes = old_bytes.replace(b"", header) + glist = list(reader(string=new_bytes)) + if len(glist) == 0: + raise nx.NetworkXError("file not successfully read as graphml") + return glist[0] + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_graphml( + graphml_string, node_type=str, edge_key_type=int, force_multigraph=False +): + """Read graph in GraphML format from string. + + Parameters + ---------- + graphml_string : string + String containing graphml information + (e.g., contents of a graphml file). + + node_type: Python type (default: str) + Convert node ids to this type + + edge_key_type: Python type (default: int) + Convert graphml edge ids to this type. Multigraphs use id as edge key. + Non-multigraphs add to edge attribute dict with name "id". + + force_multigraph : bool (default: False) + If True, return a multigraph with edge keys. If False (the default) + return a multigraph when multiedges are in the graph. + + + Returns + ------- + graph: NetworkX graph + If no parallel edges are found a Graph or DiGraph is returned. + Otherwise a MultiGraph or MultiDiGraph is returned. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> linefeed = chr(10) # linefeed = \n + >>> s = linefeed.join(nx.generate_graphml(G)) + >>> H = nx.parse_graphml(s) + + Notes + ----- + Default node and edge attributes are not propagated to each node and edge. + They can be obtained from `G.graph` and applied to node and edge attributes + if desired using something like this: + + >>> default_color = G.graph["node_default"]["color"] # doctest: +SKIP + >>> for node, data in G.nodes(data=True): # doctest: +SKIP + ... if "color" not in data: + ... data["color"] = default_color + >>> default_color = G.graph["edge_default"]["color"] # doctest: +SKIP + >>> for u, v, data in G.edges(data=True): # doctest: +SKIP + ... if "color" not in data: + ... data["color"] = default_color + + This implementation does not support mixed graphs (directed and unidirected + edges together), hypergraphs, nested graphs, or ports. + + For multigraphs the GraphML edge "id" will be used as the edge + key. If not specified then they "key" attribute will be used. If + there is no "key" attribute a default NetworkX multigraph edge key + will be provided. + + """ + reader = GraphMLReader(node_type, edge_key_type, force_multigraph) + # need to check for multiple graphs + glist = list(reader(string=graphml_string)) + if len(glist) == 0: + # If no graph comes back, try looking for an incomplete header + header = '' + new_string = graphml_string.replace("", header) + glist = list(reader(string=new_string)) + if len(glist) == 0: + raise nx.NetworkXError("file not successfully read as graphml") + return glist[0] + + +class GraphML: + NS_GRAPHML = "http://graphml.graphdrawing.org/xmlns" + NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" + # xmlns:y="http://www.yworks.com/xml/graphml" + NS_Y = "http://www.yworks.com/xml/graphml" + SCHEMALOCATION = " ".join( + [ + "http://graphml.graphdrawing.org/xmlns", + "http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd", + ] + ) + + def construct_types(self): + types = [ + (int, "integer"), # for Gephi GraphML bug + (str, "yfiles"), + (str, "string"), + (int, "int"), + (int, "long"), + (float, "float"), + (float, "double"), + (bool, "boolean"), + ] + + # These additions to types allow writing numpy types + try: + import numpy as np + except: + pass + else: + # prepend so that python types are created upon read (last entry wins) + types = [ + (np.float64, "float"), + (np.float32, "float"), + (np.float16, "float"), + (np.int_, "int"), + (np.int8, "int"), + (np.int16, "int"), + (np.int32, "int"), + (np.int64, "int"), + (np.uint8, "int"), + (np.uint16, "int"), + (np.uint32, "int"), + (np.uint64, "int"), + (np.int_, "int"), + (np.intc, "int"), + (np.intp, "int"), + ] + types + + self.xml_type = dict(types) + self.python_type = dict(reversed(a) for a in types) + + # This page says that data types in GraphML follow Java(TM). + # http://graphml.graphdrawing.org/primer/graphml-primer.html#AttributesDefinition + # true and false are the only boolean literals: + # http://en.wikibooks.org/wiki/Java_Programming/Literals#Boolean_Literals + convert_bool = { + # We use data.lower() in actual use. + "true": True, + "false": False, + # Include integer strings for convenience. + "0": False, + 0: False, + "1": True, + 1: True, + } + + def get_xml_type(self, key): + """Wrapper around the xml_type dict that raises a more informative + exception message when a user attempts to use data of a type not + supported by GraphML.""" + try: + return self.xml_type[key] + except KeyError as err: + raise TypeError( + f"GraphML does not support type {key} as data values." + ) from err + + +class GraphMLWriter(GraphML): + def __init__( + self, + graph=None, + encoding="utf-8", + prettyprint=True, + infer_numeric_types=False, + named_key_ids=False, + edge_id_from_attribute=None, + ): + self.construct_types() + from xml.etree.ElementTree import Element + + self.myElement = Element + + self.infer_numeric_types = infer_numeric_types + self.prettyprint = prettyprint + self.named_key_ids = named_key_ids + self.edge_id_from_attribute = edge_id_from_attribute + self.encoding = encoding + self.xml = self.myElement( + "graphml", + { + "xmlns": self.NS_GRAPHML, + "xmlns:xsi": self.NS_XSI, + "xsi:schemaLocation": self.SCHEMALOCATION, + }, + ) + self.keys = {} + self.attributes = defaultdict(list) + self.attribute_types = defaultdict(set) + + if graph is not None: + self.add_graph_element(graph) + + def __str__(self): + from xml.etree.ElementTree import tostring + + if self.prettyprint: + self.indent(self.xml) + s = tostring(self.xml).decode(self.encoding) + return s + + def attr_type(self, name, scope, value): + """Infer the attribute type of data named name. Currently this only + supports inference of numeric types. + + If self.infer_numeric_types is false, type is used. Otherwise, pick the + most general of types found across all values with name and scope. This + means edges with data named 'weight' are treated separately from nodes + with data named 'weight'. + """ + if self.infer_numeric_types: + types = self.attribute_types[(name, scope)] + + if len(types) > 1: + types = {self.get_xml_type(t) for t in types} + if "string" in types: + return str + elif "float" in types or "double" in types: + return float + else: + return int + else: + return list(types)[0] + else: + return type(value) + + def get_key(self, name, attr_type, scope, default): + keys_key = (name, attr_type, scope) + try: + return self.keys[keys_key] + except KeyError: + if self.named_key_ids: + new_id = name + else: + new_id = f"d{len(list(self.keys))}" + + self.keys[keys_key] = new_id + key_kwargs = { + "id": new_id, + "for": scope, + "attr.name": name, + "attr.type": attr_type, + } + key_element = self.myElement("key", **key_kwargs) + # add subelement for data default value if present + if default is not None: + default_element = self.myElement("default") + default_element.text = str(default) + key_element.append(default_element) + self.xml.insert(0, key_element) + return new_id + + def add_data(self, name, element_type, value, scope="all", default=None): + """ + Make a data element for an edge or a node. Keep a log of the + type in the keys table. + """ + if element_type not in self.xml_type: + raise nx.NetworkXError( + f"GraphML writer does not support {element_type} as data values." + ) + keyid = self.get_key(name, self.get_xml_type(element_type), scope, default) + data_element = self.myElement("data", key=keyid) + data_element.text = str(value) + return data_element + + def add_attributes(self, scope, xml_obj, data, default): + """Appends attribute data to edges or nodes, and stores type information + to be added later. See add_graph_element. + """ + for k, v in data.items(): + self.attribute_types[(str(k), scope)].add(type(v)) + self.attributes[xml_obj].append([k, v, scope, default.get(k)]) + + def add_nodes(self, G, graph_element): + default = G.graph.get("node_default", {}) + for node, data in G.nodes(data=True): + node_element = self.myElement("node", id=str(node)) + self.add_attributes("node", node_element, data, default) + graph_element.append(node_element) + + def add_edges(self, G, graph_element): + if G.is_multigraph(): + for u, v, key, data in G.edges(data=True, keys=True): + edge_element = self.myElement( + "edge", + source=str(u), + target=str(v), + id=str(data.get(self.edge_id_from_attribute)) + if self.edge_id_from_attribute + and self.edge_id_from_attribute in data + else str(key), + ) + default = G.graph.get("edge_default", {}) + self.add_attributes("edge", edge_element, data, default) + graph_element.append(edge_element) + else: + for u, v, data in G.edges(data=True): + if self.edge_id_from_attribute and self.edge_id_from_attribute in data: + # select attribute to be edge id + edge_element = self.myElement( + "edge", + source=str(u), + target=str(v), + id=str(data.get(self.edge_id_from_attribute)), + ) + else: + # default: no edge id + edge_element = self.myElement("edge", source=str(u), target=str(v)) + default = G.graph.get("edge_default", {}) + self.add_attributes("edge", edge_element, data, default) + graph_element.append(edge_element) + + def add_graph_element(self, G): + """ + Serialize graph G in GraphML to the stream. + """ + if G.is_directed(): + default_edge_type = "directed" + else: + default_edge_type = "undirected" + + graphid = G.graph.pop("id", None) + if graphid is None: + graph_element = self.myElement("graph", edgedefault=default_edge_type) + else: + graph_element = self.myElement( + "graph", edgedefault=default_edge_type, id=graphid + ) + default = {} + data = { + k: v + for (k, v) in G.graph.items() + if k not in ["node_default", "edge_default"] + } + self.add_attributes("graph", graph_element, data, default) + self.add_nodes(G, graph_element) + self.add_edges(G, graph_element) + + # self.attributes contains a mapping from XML Objects to a list of + # data that needs to be added to them. + # We postpone processing in order to do type inference/generalization. + # See self.attr_type + for xml_obj, data in self.attributes.items(): + for k, v, scope, default in data: + xml_obj.append( + self.add_data( + str(k), self.attr_type(k, scope, v), str(v), scope, default + ) + ) + self.xml.append(graph_element) + + def add_graphs(self, graph_list): + """Add many graphs to this GraphML document.""" + for G in graph_list: + self.add_graph_element(G) + + def dump(self, stream): + from xml.etree.ElementTree import ElementTree + + if self.prettyprint: + self.indent(self.xml) + document = ElementTree(self.xml) + document.write(stream, encoding=self.encoding, xml_declaration=True) + + def indent(self, elem, level=0): + # in-place prettyprint formatter + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class IncrementalElement: + """Wrapper for _IncrementalWriter providing an Element like interface. + + This wrapper does not intend to be a complete implementation but rather to + deal with those calls used in GraphMLWriter. + """ + + def __init__(self, xml, prettyprint): + self.xml = xml + self.prettyprint = prettyprint + + def append(self, element): + self.xml.write(element, pretty_print=self.prettyprint) + + +class GraphMLWriterLxml(GraphMLWriter): + def __init__( + self, + path, + graph=None, + encoding="utf-8", + prettyprint=True, + infer_numeric_types=False, + named_key_ids=False, + edge_id_from_attribute=None, + ): + self.construct_types() + import lxml.etree as lxmletree + + self.myElement = lxmletree.Element + + self._encoding = encoding + self._prettyprint = prettyprint + self.named_key_ids = named_key_ids + self.edge_id_from_attribute = edge_id_from_attribute + self.infer_numeric_types = infer_numeric_types + + self._xml_base = lxmletree.xmlfile(path, encoding=encoding) + self._xml = self._xml_base.__enter__() + self._xml.write_declaration() + + # We need to have a xml variable that support insertion. This call is + # used for adding the keys to the document. + # We will store those keys in a plain list, and then after the graph + # element is closed we will add them to the main graphml element. + self.xml = [] + self._keys = self.xml + self._graphml = self._xml.element( + "graphml", + { + "xmlns": self.NS_GRAPHML, + "xmlns:xsi": self.NS_XSI, + "xsi:schemaLocation": self.SCHEMALOCATION, + }, + ) + self._graphml.__enter__() + self.keys = {} + self.attribute_types = defaultdict(set) + + if graph is not None: + self.add_graph_element(graph) + + def add_graph_element(self, G): + """ + Serialize graph G in GraphML to the stream. + """ + if G.is_directed(): + default_edge_type = "directed" + else: + default_edge_type = "undirected" + + graphid = G.graph.pop("id", None) + if graphid is None: + graph_element = self._xml.element("graph", edgedefault=default_edge_type) + else: + graph_element = self._xml.element( + "graph", edgedefault=default_edge_type, id=graphid + ) + + # gather attributes types for the whole graph + # to find the most general numeric format needed. + # Then pass through attributes to create key_id for each. + graphdata = { + k: v + for k, v in G.graph.items() + if k not in ("node_default", "edge_default") + } + node_default = G.graph.get("node_default", {}) + edge_default = G.graph.get("edge_default", {}) + # Graph attributes + for k, v in graphdata.items(): + self.attribute_types[(str(k), "graph")].add(type(v)) + for k, v in graphdata.items(): + element_type = self.get_xml_type(self.attr_type(k, "graph", v)) + self.get_key(str(k), element_type, "graph", None) + # Nodes and data + for node, d in G.nodes(data=True): + for k, v in d.items(): + self.attribute_types[(str(k), "node")].add(type(v)) + for node, d in G.nodes(data=True): + for k, v in d.items(): + T = self.get_xml_type(self.attr_type(k, "node", v)) + self.get_key(str(k), T, "node", node_default.get(k)) + # Edges and data + if G.is_multigraph(): + for u, v, ekey, d in G.edges(keys=True, data=True): + for k, v in d.items(): + self.attribute_types[(str(k), "edge")].add(type(v)) + for u, v, ekey, d in G.edges(keys=True, data=True): + for k, v in d.items(): + T = self.get_xml_type(self.attr_type(k, "edge", v)) + self.get_key(str(k), T, "edge", edge_default.get(k)) + else: + for u, v, d in G.edges(data=True): + for k, v in d.items(): + self.attribute_types[(str(k), "edge")].add(type(v)) + for u, v, d in G.edges(data=True): + for k, v in d.items(): + T = self.get_xml_type(self.attr_type(k, "edge", v)) + self.get_key(str(k), T, "edge", edge_default.get(k)) + + # Now add attribute keys to the xml file + for key in self.xml: + self._xml.write(key, pretty_print=self._prettyprint) + + # The incremental_writer writes each node/edge as it is created + incremental_writer = IncrementalElement(self._xml, self._prettyprint) + with graph_element: + self.add_attributes("graph", incremental_writer, graphdata, {}) + self.add_nodes(G, incremental_writer) # adds attributes too + self.add_edges(G, incremental_writer) # adds attributes too + + def add_attributes(self, scope, xml_obj, data, default): + """Appends attribute data.""" + for k, v in data.items(): + data_element = self.add_data( + str(k), self.attr_type(str(k), scope, v), str(v), scope, default.get(k) + ) + xml_obj.append(data_element) + + def __str__(self): + return object.__str__(self) + + def dump(self, stream=None): + self._graphml.__exit__(None, None, None) + self._xml_base.__exit__(None, None, None) + + +# default is lxml is present. +write_graphml = write_graphml_lxml + + +class GraphMLReader(GraphML): + """Read a GraphML document. Produces NetworkX graph objects.""" + + def __init__(self, node_type=str, edge_key_type=int, force_multigraph=False): + self.construct_types() + self.node_type = node_type + self.edge_key_type = edge_key_type + self.multigraph = force_multigraph # If False, test for multiedges + self.edge_ids = {} # dict mapping (u,v) tuples to edge id attributes + + def __call__(self, path=None, string=None): + from xml.etree.ElementTree import ElementTree, fromstring + + if path is not None: + self.xml = ElementTree(file=path) + elif string is not None: + self.xml = fromstring(string) + else: + raise ValueError("Must specify either 'path' or 'string' as kwarg") + (keys, defaults) = self.find_graphml_keys(self.xml) + for g in self.xml.findall(f"{{{self.NS_GRAPHML}}}graph"): + yield self.make_graph(g, keys, defaults) + + def make_graph(self, graph_xml, graphml_keys, defaults, G=None): + # set default graph type + edgedefault = graph_xml.get("edgedefault", None) + if G is None: + if edgedefault == "directed": + G = nx.MultiDiGraph() + else: + G = nx.MultiGraph() + # set defaults for graph attributes + G.graph["node_default"] = {} + G.graph["edge_default"] = {} + for key_id, value in defaults.items(): + key_for = graphml_keys[key_id]["for"] + name = graphml_keys[key_id]["name"] + python_type = graphml_keys[key_id]["type"] + if key_for == "node": + G.graph["node_default"].update({name: python_type(value)}) + if key_for == "edge": + G.graph["edge_default"].update({name: python_type(value)}) + # hyperedges are not supported + hyperedge = graph_xml.find(f"{{{self.NS_GRAPHML}}}hyperedge") + if hyperedge is not None: + raise nx.NetworkXError("GraphML reader doesn't support hyperedges") + # add nodes + for node_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}node"): + self.add_node(G, node_xml, graphml_keys, defaults) + # add edges + for edge_xml in graph_xml.findall(f"{{{self.NS_GRAPHML}}}edge"): + self.add_edge(G, edge_xml, graphml_keys) + # add graph data + data = self.decode_data_elements(graphml_keys, graph_xml) + G.graph.update(data) + + # switch to Graph or DiGraph if no parallel edges were found + if self.multigraph: + return G + + G = nx.DiGraph(G) if G.is_directed() else nx.Graph(G) + # add explicit edge "id" from file as attribute in NX graph. + nx.set_edge_attributes(G, values=self.edge_ids, name="id") + return G + + def add_node(self, G, node_xml, graphml_keys, defaults): + """Add a node to the graph.""" + # warn on finding unsupported ports tag + ports = node_xml.find(f"{{{self.NS_GRAPHML}}}port") + if ports is not None: + warnings.warn("GraphML port tag not supported.") + # find the node by id and cast it to the appropriate type + node_id = self.node_type(node_xml.get("id")) + # get data/attributes for node + data = self.decode_data_elements(graphml_keys, node_xml) + G.add_node(node_id, **data) + # get child nodes + if node_xml.attrib.get("yfiles.foldertype") == "group": + graph_xml = node_xml.find(f"{{{self.NS_GRAPHML}}}graph") + self.make_graph(graph_xml, graphml_keys, defaults, G) + + def add_edge(self, G, edge_element, graphml_keys): + """Add an edge to the graph.""" + # warn on finding unsupported ports tag + ports = edge_element.find(f"{{{self.NS_GRAPHML}}}port") + if ports is not None: + warnings.warn("GraphML port tag not supported.") + + # raise error if we find mixed directed and undirected edges + directed = edge_element.get("directed") + if G.is_directed() and directed == "false": + msg = "directed=false edge found in directed graph." + raise nx.NetworkXError(msg) + if (not G.is_directed()) and directed == "true": + msg = "directed=true edge found in undirected graph." + raise nx.NetworkXError(msg) + + source = self.node_type(edge_element.get("source")) + target = self.node_type(edge_element.get("target")) + data = self.decode_data_elements(graphml_keys, edge_element) + # GraphML stores edge ids as an attribute + # NetworkX uses them as keys in multigraphs too if no key + # attribute is specified + edge_id = edge_element.get("id") + if edge_id: + # self.edge_ids is used by `make_graph` method for non-multigraphs + self.edge_ids[source, target] = edge_id + try: + edge_id = self.edge_key_type(edge_id) + except ValueError: # Could not convert. + pass + else: + edge_id = data.get("key") + + if G.has_edge(source, target): + # mark this as a multigraph + self.multigraph = True + + # Use add_edges_from to avoid error with add_edge when `'key' in data` + # Note there is only one edge here... + G.add_edges_from([(source, target, edge_id, data)]) + + def decode_data_elements(self, graphml_keys, obj_xml): + """Use the key information to decode the data XML if present.""" + data = {} + for data_element in obj_xml.findall(f"{{{self.NS_GRAPHML}}}data"): + key = data_element.get("key") + try: + data_name = graphml_keys[key]["name"] + data_type = graphml_keys[key]["type"] + except KeyError as err: + raise nx.NetworkXError(f"Bad GraphML data: no key {key}") from err + text = data_element.text + # assume anything with subelements is a yfiles extension + if text is not None and len(list(data_element)) == 0: + if data_type is bool: + # Ignore cases. + # http://docs.oracle.com/javase/6/docs/api/java/lang/ + # Boolean.html#parseBoolean%28java.lang.String%29 + data[data_name] = self.convert_bool[text.lower()] + else: + data[data_name] = data_type(text) + elif len(list(data_element)) > 0: + # Assume yfiles as subelements, try to extract node_label + node_label = None + # set GenericNode's configuration as shape type + gn = data_element.find(f"{{{self.NS_Y}}}GenericNode") + if gn is not None: + data["shape_type"] = gn.get("configuration") + for node_type in ["GenericNode", "ShapeNode", "SVGNode", "ImageNode"]: + pref = f"{{{self.NS_Y}}}{node_type}/{{{self.NS_Y}}}" + geometry = data_element.find(f"{pref}Geometry") + if geometry is not None: + data["x"] = geometry.get("x") + data["y"] = geometry.get("y") + if node_label is None: + node_label = data_element.find(f"{pref}NodeLabel") + shape = data_element.find(f"{pref}Shape") + if shape is not None: + data["shape_type"] = shape.get("type") + if node_label is not None: + data["label"] = node_label.text + + # check all the different types of edges available in yEd. + for edge_type in [ + "PolyLineEdge", + "SplineEdge", + "QuadCurveEdge", + "BezierEdge", + "ArcEdge", + ]: + pref = f"{{{self.NS_Y}}}{edge_type}/{{{self.NS_Y}}}" + edge_label = data_element.find(f"{pref}EdgeLabel") + if edge_label is not None: + break + if edge_label is not None: + data["label"] = edge_label.text + elif text is None: + data[data_name] = "" + return data + + def find_graphml_keys(self, graph_element): + """Extracts all the keys and key defaults from the xml.""" + graphml_keys = {} + graphml_key_defaults = {} + for k in graph_element.findall(f"{{{self.NS_GRAPHML}}}key"): + attr_id = k.get("id") + attr_type = k.get("attr.type") + attr_name = k.get("attr.name") + yfiles_type = k.get("yfiles.type") + if yfiles_type is not None: + attr_name = yfiles_type + attr_type = "yfiles" + if attr_type is None: + attr_type = "string" + warnings.warn(f"No key type for id {attr_id}. Using string") + if attr_name is None: + raise nx.NetworkXError(f"Unknown key for id {attr_id}.") + graphml_keys[attr_id] = { + "name": attr_name, + "type": self.python_type[attr_type], + "for": k.get("for"), + } + # check for "default" sub-element of key element + default = k.find(f"{{{self.NS_GRAPHML}}}default") + if default is not None: + # Handle default values identically to data element values + python_type = graphml_keys[attr_id]["type"] + if python_type is bool: + graphml_key_defaults[attr_id] = self.convert_bool[ + default.text.lower() + ] + else: + graphml_key_defaults[attr_id] = python_type(default.text) + return graphml_keys, graphml_key_defaults diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..532c71d79b7b8936481be8db0defaedf9a96b3e3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__init__.py @@ -0,0 +1,19 @@ +""" +********* +JSON data +********* +Generate and parse JSON serializable data for NetworkX graphs. + +These formats are suitable for use with the d3.js examples https://d3js.org/ + +The three formats that you can generate with NetworkX are: + + - node-link like in the d3.js example https://bl.ocks.org/mbostock/4062045 + - tree like in the d3.js example https://bl.ocks.org/mbostock/4063550 + - adjacency like in the d3.js example https://bost.ocks.org/mike/miserables/ +""" + +from networkx.readwrite.json_graph.node_link import * +from networkx.readwrite.json_graph.adjacency import * +from networkx.readwrite.json_graph.tree import * +from networkx.readwrite.json_graph.cytoscape import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..314f376e6fee2a703e7742e52d615bba2f946698 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/adjacency.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/adjacency.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93696b447f9a78713833da543a485a48b4921f88 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/adjacency.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/cytoscape.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/cytoscape.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2eaefa5d4756148ed09a8de351ab6aee3033dfa2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/cytoscape.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/node_link.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/node_link.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f64966122f723fbc9e936b324df89f2a959f0caa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/node_link.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/tree.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/tree.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90001854974290b48b858a0ab192c134f8366a9b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/__pycache__/tree.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py new file mode 100644 index 0000000000000000000000000000000000000000..3b05747565e73388b0871fbb7daf0f85ad2ce98b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/adjacency.py @@ -0,0 +1,156 @@ +import networkx as nx + +__all__ = ["adjacency_data", "adjacency_graph"] + +_attrs = {"id": "id", "key": "key"} + + +def adjacency_data(G, attrs=_attrs): + """Returns data in adjacency format that is suitable for JSON serialization + and use in JavaScript documents. + + Parameters + ---------- + G : NetworkX graph + + attrs : dict + A dictionary that contains two keys 'id' and 'key'. The corresponding + values provide the attribute names for storing NetworkX-internal graph + data. The values should be unique. Default value: + :samp:`dict(id='id', key='key')`. + + If some user-defined graph data use these attribute names as data keys, + they may be silently dropped. + + Returns + ------- + data : dict + A dictionary with adjacency formatted data. + + Raises + ------ + NetworkXError + If values in attrs are not unique. + + Examples + -------- + >>> from networkx.readwrite import json_graph + >>> G = nx.Graph([(1, 2)]) + >>> data = json_graph.adjacency_data(G) + + To serialize with json + + >>> import json + >>> s = json.dumps(data) + + Notes + ----- + Graph, node, and link attributes will be written when using this format + but attribute keys must be strings if you want to serialize the resulting + data with JSON. + + The default value of attrs will be changed in a future release of NetworkX. + + See Also + -------- + adjacency_graph, node_link_data, tree_data + """ + multigraph = G.is_multigraph() + id_ = attrs["id"] + # Allow 'key' to be omitted from attrs if the graph is not a multigraph. + key = None if not multigraph else attrs["key"] + if id_ == key: + raise nx.NetworkXError("Attribute names are not unique.") + data = {} + data["directed"] = G.is_directed() + data["multigraph"] = multigraph + data["graph"] = list(G.graph.items()) + data["nodes"] = [] + data["adjacency"] = [] + for n, nbrdict in G.adjacency(): + data["nodes"].append({**G.nodes[n], id_: n}) + adj = [] + if multigraph: + for nbr, keys in nbrdict.items(): + for k, d in keys.items(): + adj.append({**d, id_: nbr, key: k}) + else: + for nbr, d in nbrdict.items(): + adj.append({**d, id_: nbr}) + data["adjacency"].append(adj) + return data + + +@nx._dispatchable(graphs=None, returns_graph=True) +def adjacency_graph(data, directed=False, multigraph=True, attrs=_attrs): + """Returns graph from adjacency data format. + + Parameters + ---------- + data : dict + Adjacency list formatted graph data + + directed : bool + If True, and direction not specified in data, return a directed graph. + + multigraph : bool + If True, and multigraph not specified in data, return a multigraph. + + attrs : dict + A dictionary that contains two keys 'id' and 'key'. The corresponding + values provide the attribute names for storing NetworkX-internal graph + data. The values should be unique. Default value: + :samp:`dict(id='id', key='key')`. + + Returns + ------- + G : NetworkX graph + A NetworkX graph object + + Examples + -------- + >>> from networkx.readwrite import json_graph + >>> G = nx.Graph([(1, 2)]) + >>> data = json_graph.adjacency_data(G) + >>> H = json_graph.adjacency_graph(data) + + Notes + ----- + The default value of attrs will be changed in a future release of NetworkX. + + See Also + -------- + adjacency_graph, node_link_data, tree_data + """ + multigraph = data.get("multigraph", multigraph) + directed = data.get("directed", directed) + if multigraph: + graph = nx.MultiGraph() + else: + graph = nx.Graph() + if directed: + graph = graph.to_directed() + id_ = attrs["id"] + # Allow 'key' to be omitted from attrs if the graph is not a multigraph. + key = None if not multigraph else attrs["key"] + graph.graph = dict(data.get("graph", [])) + mapping = [] + for d in data["nodes"]: + node_data = d.copy() + node = node_data.pop(id_) + mapping.append(node) + graph.add_node(node) + graph.nodes[node].update(node_data) + for i, d in enumerate(data["adjacency"]): + source = mapping[i] + for tdata in d: + target_data = tdata.copy() + target = target_data.pop(id_) + if not multigraph: + graph.add_edge(source, target) + graph[source][target].update(target_data) + else: + ky = target_data.pop(key, None) + graph.add_edge(source, target, key=ky) + graph[source][target][ky].update(target_data) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py new file mode 100644 index 0000000000000000000000000000000000000000..417e36f72dbb76ae0b1f0cbdc0336ebf50360b9b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/cytoscape.py @@ -0,0 +1,190 @@ +import networkx as nx + +__all__ = ["cytoscape_data", "cytoscape_graph"] + + +def cytoscape_data(G, name="name", ident="id"): + """Returns data in Cytoscape JSON format (cyjs). + + Parameters + ---------- + G : NetworkX Graph + The graph to convert to cytoscape format + name : string + A string which is mapped to the 'name' node element in cyjs format. + Must not have the same value as `ident`. + ident : string + A string which is mapped to the 'id' node element in cyjs format. + Must not have the same value as `name`. + + Returns + ------- + data: dict + A dictionary with cyjs formatted data. + + Raises + ------ + NetworkXError + If the values for `name` and `ident` are identical. + + See Also + -------- + cytoscape_graph: convert a dictionary in cyjs format to a graph + + References + ---------- + .. [1] Cytoscape user's manual: + http://manual.cytoscape.org/en/stable/index.html + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.path_graph(2) + >>> cyto_data = nx.cytoscape_data(G) + >>> pprint(cyto_data, sort_dicts=False) + {'data': [], + 'directed': False, + 'multigraph': False, + 'elements': {'nodes': [{'data': {'id': '0', 'value': 0, 'name': '0'}}, + {'data': {'id': '1', 'value': 1, 'name': '1'}}], + 'edges': [{'data': {'source': 0, 'target': 1}}]}} + + The :mod:`json` package can be used to serialize the resulting data + + >>> import io, json + >>> with io.StringIO() as fh: # replace io with `open(...)` to write to disk + ... json.dump(cyto_data, fh) + ... fh.seek(0) # doctest: +SKIP + ... print(fh.getvalue()[:64]) # View the first 64 characters + {"data": [], "directed": false, "multigraph": false, "elements": + + """ + if name == ident: + raise nx.NetworkXError("name and ident must be different.") + + jsondata = {"data": list(G.graph.items())} + jsondata["directed"] = G.is_directed() + jsondata["multigraph"] = G.is_multigraph() + jsondata["elements"] = {"nodes": [], "edges": []} + nodes = jsondata["elements"]["nodes"] + edges = jsondata["elements"]["edges"] + + for i, j in G.nodes.items(): + n = {"data": j.copy()} + n["data"]["id"] = j.get(ident) or str(i) + n["data"]["value"] = i + n["data"]["name"] = j.get(name) or str(i) + nodes.append(n) + + if G.is_multigraph(): + for e in G.edges(keys=True): + n = {"data": G.adj[e[0]][e[1]][e[2]].copy()} + n["data"]["source"] = e[0] + n["data"]["target"] = e[1] + n["data"]["key"] = e[2] + edges.append(n) + else: + for e in G.edges(): + n = {"data": G.adj[e[0]][e[1]].copy()} + n["data"]["source"] = e[0] + n["data"]["target"] = e[1] + edges.append(n) + return jsondata + + +@nx._dispatchable(graphs=None, returns_graph=True) +def cytoscape_graph(data, name="name", ident="id"): + """ + Create a NetworkX graph from a dictionary in cytoscape JSON format. + + Parameters + ---------- + data : dict + A dictionary of data conforming to cytoscape JSON format. + name : string + A string which is mapped to the 'name' node element in cyjs format. + Must not have the same value as `ident`. + ident : string + A string which is mapped to the 'id' node element in cyjs format. + Must not have the same value as `name`. + + Returns + ------- + graph : a NetworkX graph instance + The `graph` can be an instance of `Graph`, `DiGraph`, `MultiGraph`, or + `MultiDiGraph` depending on the input data. + + Raises + ------ + NetworkXError + If the `name` and `ident` attributes are identical. + + See Also + -------- + cytoscape_data: convert a NetworkX graph to a dict in cyjs format + + References + ---------- + .. [1] Cytoscape user's manual: + http://manual.cytoscape.org/en/stable/index.html + + Examples + -------- + >>> data_dict = { + ... "data": [], + ... "directed": False, + ... "multigraph": False, + ... "elements": { + ... "nodes": [ + ... {"data": {"id": "0", "value": 0, "name": "0"}}, + ... {"data": {"id": "1", "value": 1, "name": "1"}}, + ... ], + ... "edges": [{"data": {"source": 0, "target": 1}}], + ... }, + ... } + >>> G = nx.cytoscape_graph(data_dict) + >>> G.name + '' + >>> G.nodes() + NodeView((0, 1)) + >>> G.nodes(data=True)[0] + {'id': '0', 'value': 0, 'name': '0'} + >>> G.edges(data=True) + EdgeDataView([(0, 1, {'source': 0, 'target': 1})]) + """ + if name == ident: + raise nx.NetworkXError("name and ident must be different.") + + multigraph = data.get("multigraph") + directed = data.get("directed") + if multigraph: + graph = nx.MultiGraph() + else: + graph = nx.Graph() + if directed: + graph = graph.to_directed() + graph.graph = dict(data.get("data")) + for d in data["elements"]["nodes"]: + node_data = d["data"].copy() + node = d["data"]["value"] + + if d["data"].get(name): + node_data[name] = d["data"].get(name) + if d["data"].get(ident): + node_data[ident] = d["data"].get(ident) + + graph.add_node(node) + graph.nodes[node].update(node_data) + + for d in data["elements"]["edges"]: + edge_data = d["data"].copy() + sour = d["data"]["source"] + targ = d["data"]["target"] + if multigraph: + key = d["data"].get("key", 0) + graph.add_edge(sour, targ, key=key) + graph.edges[sour, targ, key].update(edge_data) + else: + graph.add_edge(sour, targ) + graph.edges[sour, targ].update(edge_data) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py new file mode 100644 index 0000000000000000000000000000000000000000..71b74f8549729ea5bc82803397f476baec4a53a9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/node_link.py @@ -0,0 +1,261 @@ +import warnings +from itertools import count + +import networkx as nx + +__all__ = ["node_link_data", "node_link_graph"] + + +def _to_tuple(x): + """Converts lists to tuples, including nested lists. + + All other non-list inputs are passed through unmodified. This function is + intended to be used to convert potentially nested lists from json files + into valid nodes. + + Examples + -------- + >>> _to_tuple([1, 2, [3, 4]]) + (1, 2, (3, 4)) + """ + if not isinstance(x, tuple | list): + return x + return tuple(map(_to_tuple, x)) + + +def node_link_data( + G, + *, + source="source", + target="target", + name="id", + key="key", + edges="edges", + nodes="nodes", +): + """Returns data in node-link format that is suitable for JSON serialization + and use in JavaScript documents. + + Parameters + ---------- + G : NetworkX graph + source : string + A string that provides the 'source' attribute name for storing NetworkX-internal graph data. + target : string + A string that provides the 'target' attribute name for storing NetworkX-internal graph data. + name : string + A string that provides the 'name' attribute name for storing NetworkX-internal graph data. + key : string + A string that provides the 'key' attribute name for storing NetworkX-internal graph data. + edges : string + A string that provides the 'edges' attribute name for storing NetworkX-internal graph data. + nodes : string + A string that provides the 'nodes' attribute name for storing NetworkX-internal graph data. + + Returns + ------- + data : dict + A dictionary with node-link formatted data. + + Raises + ------ + NetworkXError + If the values of 'source', 'target' and 'key' are not unique. + + Examples + -------- + >>> from pprint import pprint + >>> G = nx.Graph([("A", "B")]) + >>> data1 = nx.node_link_data(G) + >>> pprint(data1) + {'directed': False, + 'edges': [{'source': 'A', 'target': 'B'}], + 'graph': {}, + 'multigraph': False, + 'nodes': [{'id': 'A'}, {'id': 'B'}]} + + To serialize with JSON + + >>> import json + >>> s1 = json.dumps(data1) + >>> pprint(s1) + ('{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "A"}, ' + '{"id": "B"}], "edges": [{"source": "A", "target": "B"}]}') + + + A graph can also be serialized by passing `node_link_data` as an encoder function. + + >>> s1 = json.dumps(G, default=nx.node_link_data) + >>> pprint(s1) + ('{"directed": false, "multigraph": false, "graph": {}, "nodes": [{"id": "A"}, ' + '{"id": "B"}], "edges": [{"source": "A", "target": "B"}]}') + + The attribute names for storing NetworkX-internal graph data can + be specified as keyword options. + + >>> H = nx.gn_graph(2) + >>> data2 = nx.node_link_data( + ... H, edges="links", source="from", target="to", nodes="vertices" + ... ) + >>> pprint(data2) + {'directed': True, + 'graph': {}, + 'links': [{'from': 1, 'to': 0}], + 'multigraph': False, + 'vertices': [{'id': 0}, {'id': 1}]} + + Notes + ----- + Graph, node, and edge attributes are stored in this format. Note that + attribute keys will be converted to strings in order to comply with JSON. + + Attribute 'key' is only used for multigraphs. + + To use `node_link_data` in conjunction with `node_link_graph`, + the keyword names for the attributes must match. + + See Also + -------- + node_link_graph, adjacency_data, tree_data + """ + multigraph = G.is_multigraph() + + # Allow 'key' to be omitted from attrs if the graph is not a multigraph. + key = None if not multigraph else key + if len({source, target, key}) < 3: + raise nx.NetworkXError("Attribute names are not unique.") + data = { + "directed": G.is_directed(), + "multigraph": multigraph, + "graph": G.graph, + nodes: [{**G.nodes[n], name: n} for n in G], + } + if multigraph: + data[edges] = [ + {**d, source: u, target: v, key: k} + for u, v, k, d in G.edges(keys=True, data=True) + ] + else: + data[edges] = [{**d, source: u, target: v} for u, v, d in G.edges(data=True)] + return data + + +@nx._dispatchable(graphs=None, returns_graph=True) +def node_link_graph( + data, + directed=False, + multigraph=True, + *, + source="source", + target="target", + name="id", + key="key", + edges="edges", + nodes="nodes", +): + """Returns graph from node-link data format. + + Useful for de-serialization from JSON. + + Parameters + ---------- + data : dict + node-link formatted graph data + + directed : bool + If True, and direction not specified in data, return a directed graph. + + multigraph : bool + If True, and multigraph not specified in data, return a multigraph. + + source : string + A string that provides the 'source' attribute name for storing NetworkX-internal graph data. + target : string + A string that provides the 'target' attribute name for storing NetworkX-internal graph data. + name : string + A string that provides the 'name' attribute name for storing NetworkX-internal graph data. + key : string + A string that provides the 'key' attribute name for storing NetworkX-internal graph data. + edges : string + A string that provides the 'edges' attribute name for storing NetworkX-internal graph data. + nodes : string + A string that provides the 'nodes' attribute name for storing NetworkX-internal graph data. + + Returns + ------- + G : NetworkX graph + A NetworkX graph object + + Examples + -------- + + Create data in node-link format by converting a graph. + + >>> from pprint import pprint + >>> G = nx.Graph([("A", "B")]) + >>> data = nx.node_link_data(G) + >>> pprint(data) + {'directed': False, + 'edges': [{'source': 'A', 'target': 'B'}], + 'graph': {}, + 'multigraph': False, + 'nodes': [{'id': 'A'}, {'id': 'B'}]} + + Revert data in node-link format to a graph. + + >>> H = nx.node_link_graph(data) + >>> print(H.edges) + [('A', 'B')] + + To serialize and deserialize a graph with JSON, + + >>> import json + >>> d = json.dumps(nx.node_link_data(G)) + >>> H = nx.node_link_graph(json.loads(d)) + >>> print(G.edges, H.edges) + [('A', 'B')] [('A', 'B')] + + + Notes + ----- + Attribute 'key' is only used for multigraphs. + + To use `node_link_data` in conjunction with `node_link_graph`, + the keyword names for the attributes must match. + + See Also + -------- + node_link_data, adjacency_data, tree_data + """ + multigraph = data.get("multigraph", multigraph) + directed = data.get("directed", directed) + if multigraph: + graph = nx.MultiGraph() + else: + graph = nx.Graph() + if directed: + graph = graph.to_directed() + + # Allow 'key' to be omitted from attrs if the graph is not a multigraph. + key = None if not multigraph else key + graph.graph = data.get("graph", {}) + c = count() + for d in data[nodes]: + node = _to_tuple(d.get(name, next(c))) + nodedata = {str(k): v for k, v in d.items() if k != name} + graph.add_node(node, **nodedata) + for d in data[edges]: + src = tuple(d[source]) if isinstance(d[source], list) else d[source] + tgt = tuple(d[target]) if isinstance(d[target], list) else d[target] + if not multigraph: + edgedata = {str(k): v for k, v in d.items() if k != source and k != target} + graph.add_edge(src, tgt, **edgedata) + else: + ky = d.get(key, None) + edgedata = { + str(k): v + for k, v in d.items() + if k != source and k != target and k != key + } + graph.add_edge(src, tgt, ky, **edgedata) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c109a15baf2d0dfcc79784c4d886e796aff2f4e5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_adjacency.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_adjacency.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f95cb7b97725c21d5e6cabb0d7ab76429d5aa1b3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_adjacency.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_cytoscape.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_cytoscape.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d11c34d44508b7396ec732c6ce0fcfcebcf8e01 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_cytoscape.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_node_link.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_node_link.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0aabac019f0f7aca0894ecb369194c3c5c413fc9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_node_link.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_tree.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_tree.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e851c6aaff1bc2eeb25c40b646449c749e7ffab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/__pycache__/test_tree.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py new file mode 100644 index 0000000000000000000000000000000000000000..37506382c55a110b26fdba32a268545d23f4474b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_adjacency.py @@ -0,0 +1,78 @@ +import copy +import json + +import pytest + +import networkx as nx +from networkx.readwrite.json_graph import adjacency_data, adjacency_graph +from networkx.utils import graphs_equal + + +class TestAdjacency: + def test_graph(self): + G = nx.path_graph(4) + H = adjacency_graph(adjacency_data(G)) + assert graphs_equal(G, H) + + def test_graph_attributes(self): + G = nx.path_graph(4) + G.add_node(1, color="red") + G.add_edge(1, 2, width=7) + G.graph["foo"] = "bar" + G.graph[1] = "one" + + H = adjacency_graph(adjacency_data(G)) + assert graphs_equal(G, H) + assert H.graph["foo"] == "bar" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + + d = json.dumps(adjacency_data(G)) + H = adjacency_graph(json.loads(d)) + assert graphs_equal(G, H) + assert H.graph["foo"] == "bar" + assert H.graph[1] == "one" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + + def test_digraph(self): + G = nx.DiGraph() + nx.add_path(G, [1, 2, 3]) + H = adjacency_graph(adjacency_data(G)) + assert H.is_directed() + assert graphs_equal(G, H) + + def test_multidigraph(self): + G = nx.MultiDiGraph() + nx.add_path(G, [1, 2, 3]) + H = adjacency_graph(adjacency_data(G)) + assert H.is_directed() + assert H.is_multigraph() + assert graphs_equal(G, H) + + def test_multigraph(self): + G = nx.MultiGraph() + G.add_edge(1, 2, key="first") + G.add_edge(1, 2, key="second", color="blue") + H = adjacency_graph(adjacency_data(G)) + assert graphs_equal(G, H) + assert H[1][2]["second"]["color"] == "blue" + + def test_input_data_is_not_modified_when_building_graph(self): + G = nx.path_graph(4) + input_data = adjacency_data(G) + orig_data = copy.deepcopy(input_data) + # Ensure input is unmodified by deserialisation + assert graphs_equal(G, adjacency_graph(input_data)) + assert input_data == orig_data + + def test_adjacency_form_json_serialisable(self): + G = nx.path_graph(4) + H = adjacency_graph(json.loads(json.dumps(adjacency_data(G)))) + assert graphs_equal(G, H) + + def test_exception(self): + with pytest.raises(nx.NetworkXError): + G = nx.MultiDiGraph() + attrs = {"id": "node", "key": "node"} + adjacency_data(G, attrs) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py new file mode 100644 index 0000000000000000000000000000000000000000..5d47f21f4217d1997165c4f19feb67d283d2dab2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_cytoscape.py @@ -0,0 +1,78 @@ +import copy +import json + +import pytest + +import networkx as nx +from networkx.readwrite.json_graph import cytoscape_data, cytoscape_graph + + +def test_graph(): + G = nx.path_graph(4) + H = cytoscape_graph(cytoscape_data(G)) + assert nx.is_isomorphic(G, H) + + +def test_input_data_is_not_modified_when_building_graph(): + G = nx.path_graph(4) + input_data = cytoscape_data(G) + orig_data = copy.deepcopy(input_data) + # Ensure input is unmodified by cytoscape_graph (gh-4173) + cytoscape_graph(input_data) + assert input_data == orig_data + + +def test_graph_attributes(): + G = nx.path_graph(4) + G.add_node(1, color="red") + G.add_edge(1, 2, width=7) + G.graph["foo"] = "bar" + G.graph[1] = "one" + G.add_node(3, name="node", id="123") + + H = cytoscape_graph(cytoscape_data(G)) + assert H.graph["foo"] == "bar" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + assert H.nodes[3]["name"] == "node" + assert H.nodes[3]["id"] == "123" + + d = json.dumps(cytoscape_data(G)) + H = cytoscape_graph(json.loads(d)) + assert H.graph["foo"] == "bar" + assert H.graph[1] == "one" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + assert H.nodes[3]["name"] == "node" + assert H.nodes[3]["id"] == "123" + + +def test_digraph(): + G = nx.DiGraph() + nx.add_path(G, [1, 2, 3]) + H = cytoscape_graph(cytoscape_data(G)) + assert H.is_directed() + assert nx.is_isomorphic(G, H) + + +def test_multidigraph(): + G = nx.MultiDiGraph() + nx.add_path(G, [1, 2, 3]) + H = cytoscape_graph(cytoscape_data(G)) + assert H.is_directed() + assert H.is_multigraph() + + +def test_multigraph(): + G = nx.MultiGraph() + G.add_edge(1, 2, key="first") + G.add_edge(1, 2, key="second", color="blue") + H = cytoscape_graph(cytoscape_data(G)) + assert nx.is_isomorphic(G, H) + assert H[1][2]["second"]["color"] == "blue" + + +def test_exception(): + with pytest.raises(nx.NetworkXError): + G = nx.MultiDiGraph() + cytoscape_data(G, name="foo", ident="foo") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py new file mode 100644 index 0000000000000000000000000000000000000000..f075bb6dd12a5cab7940f79505ccb79ae4aadacc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_node_link.py @@ -0,0 +1,109 @@ +import json + +import pytest + +import networkx as nx +from networkx.readwrite.json_graph import node_link_data, node_link_graph + + +class TestNodeLink: + def test_exception_dep(self): + G = nx.MultiDiGraph() + with pytest.raises(nx.NetworkXError): + node_link_data(G, name="node", source="node", target="node", key="node") + + def test_graph(self): + G = nx.path_graph(4) + H = node_link_graph(node_link_data(G)) + assert nx.is_isomorphic(G, H) + + def test_graph_attributes(self): + G = nx.path_graph(4) + G.add_node(1, color="red") + G.add_edge(1, 2, width=7) + G.graph[1] = "one" + G.graph["foo"] = "bar" + + H = node_link_graph(node_link_data(G)) + assert H.graph["foo"] == "bar" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + + d = json.dumps(node_link_data(G)) + H = node_link_graph(json.loads(d)) + assert H.graph["foo"] == "bar" + assert H.graph["1"] == "one" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 + + def test_digraph(self): + G = nx.DiGraph() + H = node_link_graph(node_link_data(G)) + assert H.is_directed() + + def test_multigraph(self): + G = nx.MultiGraph() + G.add_edge(1, 2, key="first") + G.add_edge(1, 2, key="second", color="blue") + H = node_link_graph(node_link_data(G)) + assert nx.is_isomorphic(G, H) + assert H[1][2]["second"]["color"] == "blue" + + def test_graph_with_tuple_nodes(self): + G = nx.Graph() + G.add_edge((0, 0), (1, 0), color=[255, 255, 0]) + d = node_link_data(G) + dumped_d = json.dumps(d) + dd = json.loads(dumped_d) + H = node_link_graph(dd) + assert H.nodes[(0, 0)] == G.nodes[(0, 0)] + assert H[(0, 0)][(1, 0)]["color"] == [255, 255, 0] + + def test_unicode_keys(self): + q = "qualité" + G = nx.Graph() + G.add_node(1, **{q: q}) + s = node_link_data(G) + output = json.dumps(s, ensure_ascii=False) + data = json.loads(output) + H = node_link_graph(data) + assert H.nodes[1][q] == q + + def test_exception(self): + G = nx.MultiDiGraph() + attrs = {"name": "node", "source": "node", "target": "node", "key": "node"} + with pytest.raises(nx.NetworkXError): + node_link_data(G, **attrs) + + def test_string_ids(self): + q = "qualité" + G = nx.DiGraph() + G.add_node("A") + G.add_node(q) + G.add_edge("A", q) + data = node_link_data(G) + assert data["edges"][0]["source"] == "A" + assert data["edges"][0]["target"] == q + H = node_link_graph(data) + assert nx.is_isomorphic(G, H) + + def test_custom_attrs(self): + G = nx.path_graph(4) + G.add_node(1, color="red") + G.add_edge(1, 2, width=7) + G.graph[1] = "one" + G.graph["foo"] = "bar" + + attrs = { + "source": "c_source", + "target": "c_target", + "name": "c_id", + "key": "c_key", + "edges": "c_links", + } + + H = node_link_graph(node_link_data(G, **attrs), multigraph=False, **attrs) + assert nx.is_isomorphic(G, H) + assert H.graph["foo"] == "bar" + assert H.nodes[1]["color"] == "red" + assert H[1][2]["width"] == 7 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..643a14d89b5211f2d97b98f2e227e68361781b97 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tests/test_tree.py @@ -0,0 +1,48 @@ +import json + +import pytest + +import networkx as nx +from networkx.readwrite.json_graph import tree_data, tree_graph + + +def test_graph(): + G = nx.DiGraph() + G.add_nodes_from([1, 2, 3], color="red") + G.add_edge(1, 2, foo=7) + G.add_edge(1, 3, foo=10) + G.add_edge(3, 4, foo=10) + H = tree_graph(tree_data(G, 1)) + assert nx.is_isomorphic(G, H) + + +def test_graph_attributes(): + G = nx.DiGraph() + G.add_nodes_from([1, 2, 3], color="red") + G.add_edge(1, 2, foo=7) + G.add_edge(1, 3, foo=10) + G.add_edge(3, 4, foo=10) + H = tree_graph(tree_data(G, 1)) + assert H.nodes[1]["color"] == "red" + + d = json.dumps(tree_data(G, 1)) + H = tree_graph(json.loads(d)) + assert H.nodes[1]["color"] == "red" + + +def test_exceptions(): + with pytest.raises(TypeError, match="is not a tree."): + G = nx.complete_graph(3) + tree_data(G, 0) + with pytest.raises(TypeError, match="is not directed."): + G = nx.path_graph(3) + tree_data(G, 0) + with pytest.raises(TypeError, match="is not weakly connected."): + G = nx.path_graph(3, create_using=nx.DiGraph) + G.add_edge(2, 0) + G.add_node(3) + tree_data(G, 0) + with pytest.raises(nx.NetworkXError, match="must be different."): + G = nx.MultiDiGraph() + G.add_node(0) + tree_data(G, 0, ident="node", children="node") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py new file mode 100644 index 0000000000000000000000000000000000000000..22b07b09d277815e824b1dd8c5b82a149ed14e1b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/json_graph/tree.py @@ -0,0 +1,137 @@ +from itertools import chain + +import networkx as nx + +__all__ = ["tree_data", "tree_graph"] + + +def tree_data(G, root, ident="id", children="children"): + """Returns data in tree format that is suitable for JSON serialization + and use in JavaScript documents. + + Parameters + ---------- + G : NetworkX graph + G must be an oriented tree + + root : node + The root of the tree + + ident : string + Attribute name for storing NetworkX-internal graph data. `ident` must + have a different value than `children`. The default is 'id'. + + children : string + Attribute name for storing NetworkX-internal graph data. `children` + must have a different value than `ident`. The default is 'children'. + + Returns + ------- + data : dict + A dictionary with node-link formatted data. + + Raises + ------ + NetworkXError + If `children` and `ident` attributes are identical. + + Examples + -------- + >>> from networkx.readwrite import json_graph + >>> G = nx.DiGraph([(1, 2)]) + >>> data = json_graph.tree_data(G, root=1) + + To serialize with json + + >>> import json + >>> s = json.dumps(data) + + Notes + ----- + Node attributes are stored in this format but keys + for attributes must be strings if you want to serialize with JSON. + + Graph and edge attributes are not stored. + + See Also + -------- + tree_graph, node_link_data, adjacency_data + """ + if G.number_of_nodes() != G.number_of_edges() + 1: + raise TypeError("G is not a tree.") + if not G.is_directed(): + raise TypeError("G is not directed.") + if not nx.is_weakly_connected(G): + raise TypeError("G is not weakly connected.") + + if ident == children: + raise nx.NetworkXError("The values for `id` and `children` must be different.") + + def add_children(n, G): + nbrs = G[n] + if len(nbrs) == 0: + return [] + children_ = [] + for child in nbrs: + d = {**G.nodes[child], ident: child} + c = add_children(child, G) + if c: + d[children] = c + children_.append(d) + return children_ + + return {**G.nodes[root], ident: root, children: add_children(root, G)} + + +@nx._dispatchable(graphs=None, returns_graph=True) +def tree_graph(data, ident="id", children="children"): + """Returns graph from tree data format. + + Parameters + ---------- + data : dict + Tree formatted graph data + + ident : string + Attribute name for storing NetworkX-internal graph data. `ident` must + have a different value than `children`. The default is 'id'. + + children : string + Attribute name for storing NetworkX-internal graph data. `children` + must have a different value than `ident`. The default is 'children'. + + Returns + ------- + G : NetworkX DiGraph + + Examples + -------- + >>> from networkx.readwrite import json_graph + >>> G = nx.DiGraph([(1, 2)]) + >>> data = json_graph.tree_data(G, root=1) + >>> H = json_graph.tree_graph(data) + + See Also + -------- + tree_data, node_link_data, adjacency_data + """ + graph = nx.DiGraph() + + def add_children(parent, children_): + for data in children_: + child = data[ident] + graph.add_edge(parent, child) + grandchildren = data.get(children, []) + if grandchildren: + add_children(child, grandchildren) + nodedata = { + str(k): v for k, v in data.items() if k != ident and k != children + } + graph.add_node(child, **nodedata) + + root = data[ident] + children_ = data.get(children, []) + nodedata = {str(k): v for k, v in data.items() if k != ident and k != children} + graph.add_node(root, **nodedata) + add_children(root, children_) + return graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/leda.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/leda.py new file mode 100644 index 0000000000000000000000000000000000000000..8d88a67d39d7ff27bf4af36895c52c9546cca329 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/leda.py @@ -0,0 +1,108 @@ +""" +Read graphs in LEDA format. + +LEDA is a C++ class library for efficient data types and algorithms. + +Format +------ +See http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html + +""" +# Original author: D. Eppstein, UC Irvine, August 12, 2003. +# The original code at http://www.ics.uci.edu/~eppstein/PADS/ is public domain. + +__all__ = ["read_leda", "parse_leda"] + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.utils import open_file + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_leda(path, encoding="UTF-8"): + """Read graph in LEDA format from path. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + G : NetworkX graph + + Examples + -------- + >>> G = nx.read_leda("file.leda") # doctest: +SKIP + + References + ---------- + .. [1] http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html + """ + lines = (line.decode(encoding) for line in path) + G = parse_leda(lines) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_leda(lines): + """Read graph in LEDA format from string or iterable. + + Parameters + ---------- + lines : string or iterable + Data in LEDA format. + + Returns + ------- + G : NetworkX graph + + Examples + -------- + >>> G = nx.parse_leda(string) # doctest: +SKIP + + References + ---------- + .. [1] http://www.algorithmic-solutions.info/leda_guide/graphs/leda_native_graph_fileformat.html + """ + if isinstance(lines, str): + lines = iter(lines.split("\n")) + lines = iter( + [ + line.rstrip("\n") + for line in lines + if not (line.startswith(("#", "\n")) or line == "") + ] + ) + for i in range(3): + next(lines) + # Graph + du = int(next(lines)) # -1=directed, -2=undirected + if du == -1: + G = nx.DiGraph() + else: + G = nx.Graph() + + # Nodes + n = int(next(lines)) # number of nodes + node = {} + for i in range(1, n + 1): # LEDA counts from 1 to n + symbol = next(lines).rstrip().strip("|{}| ") + if symbol == "": + symbol = str(i) # use int if no label - could be trouble + node[i] = symbol + + G.add_nodes_from([s for i, s in node.items()]) + + # Edges + m = int(next(lines)) # number of edges + for i in range(m): + try: + s, t, reversal, label = next(lines).split() + except BaseException as err: + raise NetworkXError(f"Too few fields in LEDA.GRAPH edge {i + 1}") from err + # BEWARE: no handling of reversal edges + G.add_edge(node[int(s)], node[int(t)], label=label[2:-2]) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py new file mode 100644 index 0000000000000000000000000000000000000000..f5b0b1c153b2f55f22f0fb0e8db9df8bdc0da18d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/multiline_adjlist.py @@ -0,0 +1,393 @@ +""" +************************* +Multi-line Adjacency List +************************* +Read and write NetworkX graphs as multi-line adjacency lists. + +The multi-line adjacency list format is useful for graphs with +nodes that can be meaningfully represented as strings. With this format +simple edge data can be stored but node or graph data is not. + +Format +------ +The first label in a line is the source node label followed by the node degree +d. The next d lines are target node labels and optional edge data. +That pattern repeats for all nodes in the graph. + +The graph with edges a-b, a-c, d-e can be represented as the following +adjacency list (anything following the # in a line is a comment):: + + # example.multiline-adjlist + a 2 + b + c + d 1 + e +""" + +__all__ = [ + "generate_multiline_adjlist", + "write_multiline_adjlist", + "parse_multiline_adjlist", + "read_multiline_adjlist", +] + +import networkx as nx +from networkx.utils import open_file + + +def generate_multiline_adjlist(G, delimiter=" "): + """Generate a single line of the graph G in multiline adjacency list format. + + Parameters + ---------- + G : NetworkX graph + + delimiter : string, optional + Separator for node labels + + Returns + ------- + lines : string + Lines of data in multiline adjlist format. + + Examples + -------- + >>> G = nx.lollipop_graph(4, 3) + >>> for line in nx.generate_multiline_adjlist(G): + ... print(line) + 0 3 + 1 {} + 2 {} + 3 {} + 1 2 + 2 {} + 3 {} + 2 1 + 3 {} + 3 1 + 4 {} + 4 1 + 5 {} + 5 1 + 6 {} + 6 0 + + See Also + -------- + write_multiline_adjlist, read_multiline_adjlist + """ + if G.is_directed(): + if G.is_multigraph(): + for s, nbrs in G.adjacency(): + nbr_edges = [ + (u, data) + for u, datadict in nbrs.items() + for key, data in datadict.items() + ] + deg = len(nbr_edges) + yield str(s) + delimiter + str(deg) + for u, d in nbr_edges: + if d is None: + yield str(u) + else: + yield str(u) + delimiter + str(d) + else: # directed single edges + for s, nbrs in G.adjacency(): + deg = len(nbrs) + yield str(s) + delimiter + str(deg) + for u, d in nbrs.items(): + if d is None: + yield str(u) + else: + yield str(u) + delimiter + str(d) + else: # undirected + if G.is_multigraph(): + seen = set() # helper dict used to avoid duplicate edges + for s, nbrs in G.adjacency(): + nbr_edges = [ + (u, data) + for u, datadict in nbrs.items() + if u not in seen + for key, data in datadict.items() + ] + deg = len(nbr_edges) + yield str(s) + delimiter + str(deg) + for u, d in nbr_edges: + if d is None: + yield str(u) + else: + yield str(u) + delimiter + str(d) + seen.add(s) + else: # undirected single edges + seen = set() # helper dict used to avoid duplicate edges + for s, nbrs in G.adjacency(): + nbr_edges = [(u, d) for u, d in nbrs.items() if u not in seen] + deg = len(nbr_edges) + yield str(s) + delimiter + str(deg) + for u, d in nbr_edges: + if d is None: + yield str(u) + else: + yield str(u) + delimiter + str(d) + seen.add(s) + + +@open_file(1, mode="wb") +def write_multiline_adjlist(G, path, delimiter=" ", comments="#", encoding="utf-8"): + """Write the graph G in multiline adjacency list format to path + + Parameters + ---------- + G : NetworkX graph + + path : string or file + Filename or file handle to write to. + Filenames ending in .gz or .bz2 will be compressed. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels + + encoding : string, optional + Text encoding. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_multiline_adjlist(G, "test.multi_adjlist") + + The path can be a file handle or a string with the name of the file. If a + file handle is provided, it has to be opened in 'wb' mode. + + >>> fh = open("test.multi_adjlist2", "wb") + >>> nx.write_multiline_adjlist(G, fh) + + Filenames ending in .gz or .bz2 will be compressed. + + >>> nx.write_multiline_adjlist(G, "test.multi_adjlist.gz") + + See Also + -------- + read_multiline_adjlist + """ + import sys + import time + + pargs = comments + " ".join(sys.argv) + header = ( + f"{pargs}\n" + + comments + + f" GMT {time.asctime(time.gmtime())}\n" + + comments + + f" {G.name}\n" + ) + path.write(header.encode(encoding)) + + for multiline in generate_multiline_adjlist(G, delimiter): + multiline += "\n" + path.write(multiline.encode(encoding)) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_multiline_adjlist( + lines, comments="#", delimiter=None, create_using=None, nodetype=None, edgetype=None +): + """Parse lines of a multiline adjacency list representation of a graph. + + Parameters + ---------- + lines : list or iterator of strings + Input data in multiline adjlist format + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + nodetype : Python type, optional + Convert nodes to this type. + + edgetype : Python type, optional + Convert edges to this type. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels. The default is whitespace. + + Returns + ------- + G: NetworkX graph + The graph corresponding to the lines in multiline adjacency list format. + + Examples + -------- + >>> lines = [ + ... "1 2", + ... "2 {'weight':3, 'name': 'Frodo'}", + ... "3 {}", + ... "2 1", + ... "5 {'weight':6, 'name': 'Saruman'}", + ... ] + >>> G = nx.parse_multiline_adjlist(iter(lines), nodetype=int) + >>> list(G) + [1, 2, 3, 5] + + """ + from ast import literal_eval + + G = nx.empty_graph(0, create_using) + for line in lines: + p = line.find(comments) + if p >= 0: + line = line[:p] + if not line: + continue + try: + (u, deg) = line.rstrip("\n").split(delimiter) + deg = int(deg) + except BaseException as err: + raise TypeError(f"Failed to read node and degree on line ({line})") from err + if nodetype is not None: + try: + u = nodetype(u) + except BaseException as err: + raise TypeError( + f"Failed to convert node ({u}) to type {nodetype}" + ) from err + G.add_node(u) + for i in range(deg): + while True: + try: + line = next(lines) + except StopIteration as err: + msg = f"Failed to find neighbor for node ({u})" + raise TypeError(msg) from err + p = line.find(comments) + if p >= 0: + line = line[:p] + if line: + break + vlist = line.rstrip("\n").split(delimiter) + numb = len(vlist) + if numb < 1: + continue # isolated node + v = vlist.pop(0) + data = "".join(vlist) + if nodetype is not None: + try: + v = nodetype(v) + except BaseException as err: + raise TypeError( + f"Failed to convert node ({v}) to type {nodetype}" + ) from err + if edgetype is not None: + try: + edgedata = {"weight": edgetype(data)} + except BaseException as err: + raise TypeError( + f"Failed to convert edge data ({data}) to type {edgetype}" + ) from err + else: + try: # try to evaluate + edgedata = literal_eval(data) + except: + edgedata = {} + G.add_edge(u, v, **edgedata) + + return G + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_multiline_adjlist( + path, + comments="#", + delimiter=None, + create_using=None, + nodetype=None, + edgetype=None, + encoding="utf-8", +): + """Read graph in multi-line adjacency list format from path. + + Parameters + ---------- + path : string or file + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + create_using : NetworkX graph constructor, optional (default=nx.Graph) + Graph type to create. If graph instance, then cleared before populated. + + nodetype : Python type, optional + Convert nodes to this type. + + edgetype : Python type, optional + Convert edge data to this type. + + comments : string, optional + Marker for comment lines + + delimiter : string, optional + Separator for node labels. The default is whitespace. + + Returns + ------- + G: NetworkX graph + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_multiline_adjlist(G, "test.multi_adjlistP4") + >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4") + + The path can be a file or a string with the name of the file. If a + file s provided, it has to be opened in 'rb' mode. + + >>> fh = open("test.multi_adjlistP4", "rb") + >>> G = nx.read_multiline_adjlist(fh) + + Filenames ending in .gz or .bz2 will be compressed. + + >>> nx.write_multiline_adjlist(G, "test.multi_adjlistP4.gz") + >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4.gz") + + The optional nodetype is a function to convert node strings to nodetype. + + For example + + >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4", nodetype=int) + + will attempt to convert all nodes to integer type. + + The optional edgetype is a function to convert edge data strings to + edgetype. + + >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4") + + The optional create_using parameter is a NetworkX graph container. + The default is Graph(), an undirected graph. To read the data as + a directed graph use + + >>> G = nx.read_multiline_adjlist("test.multi_adjlistP4", create_using=nx.DiGraph) + + Notes + ----- + This format does not store graph, node, or edge data. + + See Also + -------- + write_multiline_adjlist + """ + lines = (line.decode(encoding) for line in path) + return parse_multiline_adjlist( + lines, + comments=comments, + delimiter=delimiter, + create_using=create_using, + nodetype=nodetype, + edgetype=edgetype, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/p2g.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/p2g.py new file mode 100644 index 0000000000000000000000000000000000000000..4bde362ea8d511e603ce3b8d95a6206d74091663 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/p2g.py @@ -0,0 +1,113 @@ +""" +This module provides the following: read and write of p2g format +used in metabolic pathway studies. + +See: + +for a description. + +The summary is included here: + +A file that describes a uniquely labeled graph (with extension ".gr") +format looks like the following: + + +name +3 4 +a +1 2 +b + +c +0 2 + +"name" is simply a description of what the graph corresponds to. The +second line displays the number of nodes and number of edges, +respectively. This sample graph contains three nodes labeled "a", "b", +and "c". The rest of the graph contains two lines for each node. The +first line for a node contains the node label. After the declaration +of the node label, the out-edges of that node in the graph are +provided. For instance, "a" is linked to nodes 1 and 2, which are +labeled "b" and "c", while the node labeled "b" has no outgoing +edges. Observe that node labeled "c" has an outgoing edge to +itself. Indeed, self-loops are allowed. Node index starts from 0. + +""" + +import networkx as nx +from networkx.utils import open_file + + +@open_file(1, mode="w") +def write_p2g(G, path, encoding="utf-8"): + """Write NetworkX graph in p2g format. + + Notes + ----- + This format is meant to be used with directed graphs with + possible self loops. + """ + path.write((f"{G.name}\n").encode(encoding)) + path.write((f"{G.order()} {G.size()}\n").encode(encoding)) + nodes = list(G) + # make dictionary mapping nodes to integers + nodenumber = dict(zip(nodes, range(len(nodes)))) + for n in nodes: + path.write((f"{n}\n").encode(encoding)) + for nbr in G.neighbors(n): + path.write((f"{nodenumber[nbr]} ").encode(encoding)) + path.write("\n".encode(encoding)) + + +@open_file(0, mode="r") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_p2g(path, encoding="utf-8"): + """Read graph in p2g format from path. + + Parameters + ---------- + path : string or file + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + MultiDiGraph + + Notes + ----- + If you want a DiGraph (with no self loops allowed and no edge data) + use D=nx.DiGraph(read_p2g(path)) + """ + lines = (line.decode(encoding) for line in path) + G = parse_p2g(lines) + return G + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_p2g(lines): + """Parse p2g format graph from string or iterable. + + Returns + ------- + MultiDiGraph + """ + description = next(lines).strip() + # are multiedges (parallel edges) allowed? + G = nx.MultiDiGraph(name=description, selfloops=True) + nnodes, nedges = map(int, next(lines).split()) + nodelabel = {} + nbrs = {} + # loop over the nodes keeping track of node labels and out neighbors + # defer adding edges until all node labels are known + for i in range(nnodes): + n = next(lines).strip() + nodelabel[i] = n + G.add_node(n) + nbrs[n] = map(int, next(lines).split()) + # now we know all of the node labels so we can add the edges + # with the correct labels + for n in G: + for nbr in nbrs[n]: + G.add_edge(n, nodelabel[nbr]) + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/pajek.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/pajek.py new file mode 100644 index 0000000000000000000000000000000000000000..2cab6b9ae68ba9c9869f0452394838925ff2a7db --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/pajek.py @@ -0,0 +1,286 @@ +""" +***** +Pajek +***** +Read graphs in Pajek format. + +This implementation handles directed and undirected graphs including +those with self loops and parallel edges. + +Format +------ +See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm +for format information. + +""" + +import warnings + +import networkx as nx +from networkx.utils import open_file + +__all__ = ["read_pajek", "parse_pajek", "generate_pajek", "write_pajek"] + + +def generate_pajek(G): + """Generate lines in Pajek graph format. + + Parameters + ---------- + G : graph + A Networkx graph + + References + ---------- + See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm + for format information. + """ + if G.name == "": + name = "NetworkX" + else: + name = G.name + # Apparently many Pajek format readers can't process this line + # So we'll leave it out for now. + # yield '*network %s'%name + + # write nodes with attributes + yield f"*vertices {G.order()}" + nodes = list(G) + # make dictionary mapping nodes to integers + nodenumber = dict(zip(nodes, range(1, len(nodes) + 1))) + for n in nodes: + # copy node attributes and pop mandatory attributes + # to avoid duplication. + na = G.nodes.get(n, {}).copy() + x = na.pop("x", 0.0) + y = na.pop("y", 0.0) + try: + id = int(na.pop("id", nodenumber[n])) + except ValueError as err: + err.args += ( + ( + "Pajek format requires 'id' to be an int()." + " Refer to the 'Relabeling nodes' section." + ), + ) + raise + nodenumber[n] = id + shape = na.pop("shape", "ellipse") + s = " ".join(map(make_qstr, (id, n, x, y, shape))) + # only optional attributes are left in na. + for k, v in na.items(): + if isinstance(v, str) and v.strip() != "": + s += f" {make_qstr(k)} {make_qstr(v)}" + else: + warnings.warn( + f"Node attribute {k} is not processed. {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}." + ) + yield s + + # write edges with attributes + if G.is_directed(): + yield "*arcs" + else: + yield "*edges" + for u, v, edgedata in G.edges(data=True): + d = edgedata.copy() + value = d.pop("weight", 1.0) # use 1 as default edge value + s = " ".join(map(make_qstr, (nodenumber[u], nodenumber[v], value))) + for k, v in d.items(): + if isinstance(v, str) and v.strip() != "": + s += f" {make_qstr(k)} {make_qstr(v)}" + else: + warnings.warn( + f"Edge attribute {k} is not processed. {('Empty attribute' if isinstance(v, str) else 'Non-string attribute')}." + ) + yield s + + +@open_file(1, mode="wb") +def write_pajek(G, path, encoding="UTF-8"): + """Write graph in Pajek format to path. + + Parameters + ---------- + G : graph + A Networkx graph + path : file or string + File or filename to write. + Filenames ending in .gz or .bz2 will be compressed. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_pajek(G, "test.netP4") + + Warnings + -------- + Optional node attributes and edge attributes must be non-empty strings. + Otherwise it will not be written into the file. You will need to + convert those attributes to strings if you want to keep them. + + References + ---------- + See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm + for format information. + """ + for line in generate_pajek(G): + line += "\n" + path.write(line.encode(encoding)) + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_pajek(path, encoding="UTF-8"): + """Read graph in Pajek format from path. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + G : NetworkX MultiGraph or MultiDiGraph. + + Examples + -------- + >>> G = nx.path_graph(4) + >>> nx.write_pajek(G, "test.net") + >>> G = nx.read_pajek("test.net") + + To create a Graph instead of a MultiGraph use + + >>> G1 = nx.Graph(G) + + References + ---------- + See http://vlado.fmf.uni-lj.si/pub/networks/pajek/doc/draweps.htm + for format information. + """ + lines = (line.decode(encoding) for line in path) + return parse_pajek(lines) + + +@nx._dispatchable(graphs=None, returns_graph=True) +def parse_pajek(lines): + """Parse Pajek format graph from string or iterable. + + Parameters + ---------- + lines : string or iterable + Data in Pajek format. + + Returns + ------- + G : NetworkX graph + + See Also + -------- + read_pajek + + """ + import shlex + + # multigraph=False + if isinstance(lines, str): + lines = iter(lines.split("\n")) + lines = iter([line.rstrip("\n") for line in lines]) + G = nx.MultiDiGraph() # are multiedges allowed in Pajek? assume yes + labels = [] # in the order of the file, needed for matrix + while lines: + try: + l = next(lines) + except: # EOF + break + if l.lower().startswith("*network"): + try: + label, name = l.split(None, 1) + except ValueError: + # Line was not of the form: *network NAME + pass + else: + G.graph["name"] = name + elif l.lower().startswith("*vertices"): + nodelabels = {} + l, nnodes = l.split() + for i in range(int(nnodes)): + l = next(lines) + try: + splitline = [ + x.decode("utf-8") for x in shlex.split(str(l).encode("utf-8")) + ] + except AttributeError: + splitline = shlex.split(str(l)) + id, label = splitline[0:2] + labels.append(label) + G.add_node(label) + nodelabels[id] = label + G.nodes[label]["id"] = id + try: + x, y, shape = splitline[2:5] + G.nodes[label].update( + {"x": float(x), "y": float(y), "shape": shape} + ) + except: + pass + extra_attr = zip(splitline[5::2], splitline[6::2]) + G.nodes[label].update(extra_attr) + elif l.lower().startswith("*edges") or l.lower().startswith("*arcs"): + if l.lower().startswith("*edge"): + # switch from multidigraph to multigraph + G = nx.MultiGraph(G) + if l.lower().startswith("*arcs"): + # switch to directed with multiple arcs for each existing edge + G = G.to_directed() + for l in lines: + try: + splitline = [ + x.decode("utf-8") for x in shlex.split(str(l).encode("utf-8")) + ] + except AttributeError: + splitline = shlex.split(str(l)) + + if len(splitline) < 2: + continue + ui, vi = splitline[0:2] + u = nodelabels.get(ui, ui) + v = nodelabels.get(vi, vi) + # parse the data attached to this edge and put in a dictionary + edge_data = {} + try: + # there should always be a single value on the edge? + w = splitline[2:3] + edge_data.update({"weight": float(w[0])}) + except: + pass + # if there isn't, just assign a 1 + # edge_data.update({'value':1}) + extra_attr = zip(splitline[3::2], splitline[4::2]) + edge_data.update(extra_attr) + # if G.has_edge(u,v): + # multigraph=True + G.add_edge(u, v, **edge_data) + elif l.lower().startswith("*matrix"): + G = nx.DiGraph(G) + adj_list = ( + (labels[row], labels[col], {"weight": int(data)}) + for (row, line) in enumerate(lines) + for (col, data) in enumerate(line.split()) + if int(data) != 0 + ) + G.add_edges_from(adj_list) + + return G + + +def make_qstr(t): + """Returns the string representation of t. + Add outer double-quotes if the string has a space. + """ + if not isinstance(t, str): + t = str(t) + if " " in t: + t = f'"{t}"' + return t diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/sparse6.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/sparse6.py new file mode 100644 index 0000000000000000000000000000000000000000..b82ae5c34d30e1a1e72f0e94e6845072d18296f9 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/sparse6.py @@ -0,0 +1,379 @@ +# Original author: D. Eppstein, UC Irvine, August 12, 2003. +# The original code at https://www.ics.uci.edu/~eppstein/PADS/ is public domain. +"""Functions for reading and writing graphs in the *sparse6* format. + +The *sparse6* file format is a space-efficient format for large sparse +graphs. For small graphs or large dense graphs, use the *graph6* file +format. + +For more information, see the `sparse6`_ homepage. + +.. _sparse6: https://users.cecs.anu.edu.au/~bdm/data/formats.html + +""" + +import networkx as nx +from networkx.exception import NetworkXError +from networkx.readwrite.graph6 import data_to_n, n_to_data +from networkx.utils import not_implemented_for, open_file + +__all__ = ["from_sparse6_bytes", "read_sparse6", "to_sparse6_bytes", "write_sparse6"] + + +def _generate_sparse6_bytes(G, nodes, header): + """Yield bytes in the sparse6 encoding of a graph. + + `G` is an undirected simple graph. `nodes` is the list of nodes for + which the node-induced subgraph will be encoded; if `nodes` is the + list of all nodes in the graph, the entire graph will be + encoded. `header` is a Boolean that specifies whether to generate + the header ``b'>>sparse6<<'`` before the remaining data. + + This function generates `bytes` objects in the following order: + + 1. the header (if requested), + 2. the encoding of the number of nodes, + 3. each character, one-at-a-time, in the encoding of the requested + node-induced subgraph, + 4. a newline character. + + This function raises :exc:`ValueError` if the graph is too large for + the graph6 format (that is, greater than ``2 ** 36`` nodes). + + """ + n = len(G) + if n >= 2**36: + raise ValueError( + "sparse6 is only defined if number of nodes is less than 2 ** 36" + ) + if header: + yield b">>sparse6<<" + yield b":" + for d in n_to_data(n): + yield str.encode(chr(d + 63)) + + k = 1 + while 1 << k < n: + k += 1 + + def enc(x): + """Big endian k-bit encoding of x""" + return [1 if (x & 1 << (k - 1 - i)) else 0 for i in range(k)] + + edges = sorted((max(u, v), min(u, v)) for u, v in G.edges()) + bits = [] + curv = 0 + for v, u in edges: + if v == curv: # current vertex edge + bits.append(0) + bits.extend(enc(u)) + elif v == curv + 1: # next vertex edge + curv += 1 + bits.append(1) + bits.extend(enc(u)) + else: # skip to vertex v and then add edge to u + curv = v + bits.append(1) + bits.extend(enc(v)) + bits.append(0) + bits.extend(enc(u)) + if k < 6 and n == (1 << k) and ((-len(bits)) % 6) >= k and curv < (n - 1): + # Padding special case: small k, n=2^k, + # more than k bits of padding needed, + # current vertex is not (n-1) -- + # appending 1111... would add a loop on (n-1) + bits.append(0) + bits.extend([1] * ((-len(bits)) % 6)) + else: + bits.extend([1] * ((-len(bits)) % 6)) + + data = [ + (bits[i + 0] << 5) + + (bits[i + 1] << 4) + + (bits[i + 2] << 3) + + (bits[i + 3] << 2) + + (bits[i + 4] << 1) + + (bits[i + 5] << 0) + for i in range(0, len(bits), 6) + ] + + for d in data: + yield str.encode(chr(d + 63)) + yield b"\n" + + +@nx._dispatchable(graphs=None, returns_graph=True) +def from_sparse6_bytes(string): + """Read an undirected graph in sparse6 format from string. + + Parameters + ---------- + string : string + Data in sparse6 format + + Returns + ------- + G : Graph + + Raises + ------ + NetworkXError + If the string is unable to be parsed in sparse6 format + + Examples + -------- + >>> G = nx.from_sparse6_bytes(b":A_") + >>> sorted(G.edges()) + [(0, 1), (0, 1), (0, 1)] + + See Also + -------- + read_sparse6, write_sparse6 + + References + ---------- + .. [1] Sparse6 specification + + + """ + if string.startswith(b">>sparse6<<"): + string = string[11:] + if not string.startswith(b":"): + raise NetworkXError("Expected leading colon in sparse6") + + chars = [c - 63 for c in string[1:]] + n, data = data_to_n(chars) + k = 1 + while 1 << k < n: + k += 1 + + def parseData(): + """Returns stream of pairs b[i], x[i] for sparse6 format.""" + chunks = iter(data) + d = None # partial data word + dLen = 0 # how many unparsed bits are left in d + + while 1: + if dLen < 1: + try: + d = next(chunks) + except StopIteration: + return + dLen = 6 + dLen -= 1 + b = (d >> dLen) & 1 # grab top remaining bit + + x = d & ((1 << dLen) - 1) # partially built up value of x + xLen = dLen # how many bits included so far in x + while xLen < k: # now grab full chunks until we have enough + try: + d = next(chunks) + except StopIteration: + return + dLen = 6 + x = (x << 6) + d + xLen += 6 + x = x >> (xLen - k) # shift back the extra bits + dLen = xLen - k + yield b, x + + v = 0 + + G = nx.MultiGraph() + G.add_nodes_from(range(n)) + + multigraph = False + for b, x in parseData(): + if b == 1: + v += 1 + # padding with ones can cause overlarge number here + if x >= n or v >= n: + break + elif x > v: + v = x + else: + if G.has_edge(x, v): + multigraph = True + G.add_edge(x, v) + if not multigraph: + G = nx.Graph(G) + return G + + +def to_sparse6_bytes(G, nodes=None, header=True): + """Convert an undirected graph to bytes in sparse6 format. + + Parameters + ---------- + G : Graph (undirected) + + nodes: list or iterable + Nodes are labeled 0...n-1 in the order provided. If None the ordering + given by ``G.nodes()`` is used. + + header: bool + If True add '>>sparse6<<' bytes to head of data. + + Raises + ------ + NetworkXNotImplemented + If the graph is directed. + + ValueError + If the graph has at least ``2 ** 36`` nodes; the sparse6 format + is only defined for graphs of order less than ``2 ** 36``. + + Examples + -------- + >>> nx.to_sparse6_bytes(nx.path_graph(2)) + b'>>sparse6<<:An\\n' + + See Also + -------- + to_sparse6_bytes, read_sparse6, write_sparse6_bytes + + Notes + ----- + The returned bytes end with a newline character. + + The format does not support edge or node labels. + + References + ---------- + .. [1] Graph6 specification + + + """ + if nodes is not None: + G = G.subgraph(nodes) + G = nx.convert_node_labels_to_integers(G, ordering="sorted") + return b"".join(_generate_sparse6_bytes(G, nodes, header)) + + +@open_file(0, mode="rb") +@nx._dispatchable(graphs=None, returns_graph=True) +def read_sparse6(path): + """Read an undirected graph in sparse6 format from path. + + Parameters + ---------- + path : file or string + Filename or file handle to read. + Filenames ending in .gz or .bz2 will be decompressed. + + Returns + ------- + G : Graph/Multigraph or list of Graphs/MultiGraphs + If the file contains multiple lines then a list of graphs is returned + + Raises + ------ + NetworkXError + If the string is unable to be parsed in sparse6 format + + Examples + -------- + You can read a sparse6 file by giving the path to the file:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile(delete=False) as f: + ... _ = f.write(b">>sparse6<<:An\\n") + ... _ = f.seek(0) + ... G = nx.read_sparse6(f.name) + >>> list(G.edges()) + [(0, 1)] + + You can also read a sparse6 file by giving an open file-like object:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile() as f: + ... _ = f.write(b">>sparse6<<:An\\n") + ... _ = f.seek(0) + ... G = nx.read_sparse6(f) + >>> list(G.edges()) + [(0, 1)] + + See Also + -------- + read_sparse6, from_sparse6_bytes + + References + ---------- + .. [1] Sparse6 specification + + + """ + glist = [] + for line in path: + line = line.strip() + if not len(line): + continue + glist.append(from_sparse6_bytes(line)) + if len(glist) == 1: + return glist[0] + else: + return glist + + +@not_implemented_for("directed") +@open_file(1, mode="wb") +def write_sparse6(G, path, nodes=None, header=True): + """Write graph G to given path in sparse6 format. + + Parameters + ---------- + G : Graph (undirected) + + path : file or string + File or filename to write. + Filenames ending in .gz or .bz2 will be compressed. + + nodes: list or iterable + Nodes are labeled 0...n-1 in the order provided. If None the ordering + given by G.nodes() is used. + + header: bool + If True add '>>sparse6<<' string to head of data + + Raises + ------ + NetworkXError + If the graph is directed + + Examples + -------- + You can write a sparse6 file by giving the path to the file:: + + >>> import tempfile + >>> with tempfile.NamedTemporaryFile(delete=False) as f: + ... nx.write_sparse6(nx.path_graph(2), f.name) + ... print(f.read()) + b'>>sparse6<<:An\\n' + + You can also write a sparse6 file by giving an open file-like object:: + + >>> with tempfile.NamedTemporaryFile() as f: + ... nx.write_sparse6(nx.path_graph(2), f) + ... _ = f.seek(0) + ... print(f.read()) + b'>>sparse6<<:An\\n' + + See Also + -------- + read_sparse6, from_sparse6_bytes + + Notes + ----- + The format does not support edge or node labels. + + References + ---------- + .. [1] Sparse6 specification + + + """ + if nodes is not None: + G = G.subgraph(nodes) + G = nx.convert_node_labels_to_integers(G, ordering="sorted") + for b in _generate_sparse6_bytes(G, nodes, header): + path.write(b) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da8d3db40d61cdc5f1065f7cec5eb26e199166dd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_adjlist.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_adjlist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..743107ea69dab2d2a7d95b9cd9477a7223d3c2c3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_adjlist.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_edgelist.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_edgelist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cd6bffaaefc764e12026edc19473cd9b9239a09 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_edgelist.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gexf.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gexf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6eda028a2fb2243bf8176ef79a4588cd30a5f55b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gexf.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gml.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9491600264b96796659b2325666851826208238c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_gml.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graph6.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graph6.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..876465b3ce37d6cd9c2f9e0994a932acab6d0e19 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graph6.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graphml.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graphml.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2913bf18e2c6396a44ad754f9d7da0d1502baa92 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_graphml.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_leda.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_leda.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f401cf4bce17d0f8b43739bd7adad9278c3b6879 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_leda.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_p2g.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_p2g.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e971bc095667dfcda932a69496f04126213eb2ec Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_p2g.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_pajek.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_pajek.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45fa46e00597760cf224f59f4ad24c18be617792 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_pajek.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_sparse6.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_sparse6.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f4149c53b3973d14e9745cd1772bad41b942397 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_sparse6.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_text.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_text.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dbf83c675e0137a93bfc82f4705c6375b83cdd3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/__pycache__/test_text.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py new file mode 100644 index 0000000000000000000000000000000000000000..015eaf8276a3b178b0bdb2c67b54ccfdc63256ae --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_adjlist.py @@ -0,0 +1,354 @@ +""" +Unit tests for adjlist. +""" + +import io + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, graphs_equal, nodes_equal + + +class TestGenerateAdjlist: + @pytest.mark.parametrize("graph_type", [nx.Graph, nx.MultiGraph]) + def test_undirected(self, graph_type): + G = nx.complete_graph(5, create_using=graph_type) + lines = [ + "0 1 2 3 4", + "1 2 3 4", + "2 3 4", + "3 4", + "4", + ] + assert list(nx.generate_adjlist(G)) == lines + + @pytest.mark.parametrize("graph_type", [nx.DiGraph, nx.MultiDiGraph]) + def test_directed(self, graph_type): + G = nx.complete_graph(5, create_using=graph_type) + lines = [ + "0 1 2 3 4", + "1 0 2 3 4", + "2 0 1 3 4", + "3 0 1 2 4", + "4 0 1 2 3", + ] + assert list(nx.generate_adjlist(G)) == lines + + G = nx.path_graph(5, create_using=graph_type) + G.add_edge(1, 0) + lines = [ + "0 1", + "1 2 0", + "2 3", + "3 4", + "4", + ] + assert list(nx.generate_adjlist(G)) == lines + + @pytest.mark.parametrize("delimiter", [" ", ",", "\t"]) + def test_delimiter(self, delimiter): + G = nx.complete_graph(3) + lines = [ + f"0{delimiter}1{delimiter}2", + f"1{delimiter}2", + f"2", + ] + assert list(nx.generate_adjlist(G, delimiter=delimiter)) == lines + + def test_multiple_edges_undirected(self): + G = nx.complete_graph(3, create_using=nx.MultiGraph) + G.add_edge(0, 1) + lines = [ + "0 1 1 2", + "1 2", + "2", + ] + assert list(nx.generate_adjlist(G)) == lines + + def test_multiple_edges_directed(self): + G = nx.complete_graph(3, create_using=nx.MultiDiGraph) + G.add_edge(0, 1) + lines = [ + "0 1 1 2", + "1 0 2", + "2 0 1", + ] + assert list(nx.generate_adjlist(G)) == lines + + G.add_edge(1, 0) + lines[1] = "1 0 0 2" + assert list(nx.generate_adjlist(G)) == lines + + def test_multiple_edges_with_data(self): + G = nx.complete_graph(3, create_using=nx.MultiGraph) + G.add_edge(0, 1, weight=1) + G.add_edge(0, 1, weight=2) + lines = [ + "0 1 1 1 2", + "1 2", + "2", + ] + assert list(nx.generate_adjlist(G)) == lines + + def test_with_self_loop(self): + G = nx.complete_graph(3) + G.add_edge(0, 0) + lines = [ + "0 1 2 0", + "1 2", + "2", + ] + assert list(nx.generate_adjlist(G)) == lines + + +class TestAdjlist: + @classmethod + def setup_class(cls): + cls.G = nx.Graph(name="test") + e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")] + cls.G.add_edges_from(e) + cls.G.add_node("g") + cls.DG = nx.DiGraph(cls.G) + cls.XG = nx.MultiGraph() + cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)]) + cls.XDG = nx.MultiDiGraph(cls.XG) + + def test_read_multiline_adjlist_1(self): + # Unit test for https://networkx.lanl.gov/trac/ticket/252 + s = b"""# comment line +1 2 +# comment line +2 +3 +""" + bytesIO = io.BytesIO(s) + G = nx.read_multiline_adjlist(bytesIO) + adj = {"1": {"3": {}, "2": {}}, "3": {"1": {}}, "2": {"1": {}}} + assert graphs_equal(G, nx.Graph(adj)) + + def test_unicode(self, tmp_path): + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + G.add_edge(name1, "Radiohead", **{name2: 3}) + + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist(fname) + assert graphs_equal(G, H) + + def test_latin1_err(self, tmp_path): + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + G.add_edge(name1, "Radiohead", **{name2: 3}) + fname = tmp_path / "adjlist.txt" + with pytest.raises(UnicodeEncodeError): + nx.write_multiline_adjlist(G, fname, encoding="latin-1") + + def test_latin1(self, tmp_path): + G = nx.Graph() + name1 = "Bj" + chr(246) + "rk" + name2 = chr(220) + "ber" + G.add_edge(name1, "Radiohead", **{name2: 3}) + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname, encoding="latin-1") + H = nx.read_multiline_adjlist(fname, encoding="latin-1") + assert graphs_equal(G, H) + + def test_parse_adjlist(self): + lines = ["1 2 5", "2 3 4", "3 5", "4", "5"] + nx.parse_adjlist(lines, nodetype=int) # smoke test + with pytest.raises(TypeError): + nx.parse_adjlist(lines, nodetype="int") + lines = ["1 2 5", "2 b", "c"] + with pytest.raises(TypeError): + nx.parse_adjlist(lines, nodetype=int) + + def test_adjlist_graph(self, tmp_path): + G = self.G + fname = tmp_path / "adjlist.txt" + nx.write_adjlist(G, fname) + H = nx.read_adjlist(fname) + H2 = nx.read_adjlist(fname) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_adjlist_digraph(self, tmp_path): + G = self.DG + fname = tmp_path / "adjlist.txt" + nx.write_adjlist(G, fname) + H = nx.read_adjlist(fname, create_using=nx.DiGraph()) + H2 = nx.read_adjlist(fname, create_using=nx.DiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + def test_adjlist_integers(self, tmp_path): + fname = tmp_path / "adjlist.txt" + G = nx.convert_node_labels_to_integers(self.G) + nx.write_adjlist(G, fname) + H = nx.read_adjlist(fname, nodetype=int) + H2 = nx.read_adjlist(fname, nodetype=int) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_adjlist_multigraph(self, tmp_path): + G = self.XG + fname = tmp_path / "adjlist.txt" + nx.write_adjlist(G, fname) + H = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiGraph()) + H2 = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_adjlist_multidigraph(self, tmp_path): + G = self.XDG + fname = tmp_path / "adjlist.txt" + nx.write_adjlist(G, fname) + H = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiDiGraph()) + H2 = nx.read_adjlist(fname, nodetype=int, create_using=nx.MultiDiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + def test_adjlist_delimiter(self): + fh = io.BytesIO() + G = nx.path_graph(3) + nx.write_adjlist(G, fh, delimiter=":") + fh.seek(0) + H = nx.read_adjlist(fh, nodetype=int, delimiter=":") + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + +class TestMultilineAdjlist: + @classmethod + def setup_class(cls): + cls.G = nx.Graph(name="test") + e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")] + cls.G.add_edges_from(e) + cls.G.add_node("g") + cls.DG = nx.DiGraph(cls.G) + cls.DG.remove_edge("b", "a") + cls.DG.remove_edge("b", "c") + cls.XG = nx.MultiGraph() + cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)]) + cls.XDG = nx.MultiDiGraph(cls.XG) + + def test_parse_multiline_adjlist(self): + lines = [ + "1 2", + "b {'weight':3, 'name': 'Frodo'}", + "c {}", + "d 1", + "e {'weight':6, 'name': 'Saruman'}", + ] + nx.parse_multiline_adjlist(iter(lines)) # smoke test + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines), nodetype=int) + nx.parse_multiline_adjlist(iter(lines), edgetype=str) # smoke test + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines), nodetype=int) + lines = ["1 a"] + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines)) + lines = ["a 2"] + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines), nodetype=int) + lines = ["1 2"] + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines)) + lines = ["1 2", "2 {}"] + with pytest.raises(TypeError): + nx.parse_multiline_adjlist(iter(lines)) + + def test_multiline_adjlist_graph(self, tmp_path): + G = self.G + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist(fname) + H2 = nx.read_multiline_adjlist(fname) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_multiline_adjlist_digraph(self, tmp_path): + G = self.DG + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist(fname, create_using=nx.DiGraph()) + H2 = nx.read_multiline_adjlist(fname, create_using=nx.DiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + def test_multiline_adjlist_integers(self, tmp_path): + fname = tmp_path / "adjlist.txt" + G = nx.convert_node_labels_to_integers(self.G) + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist(fname, nodetype=int) + H2 = nx.read_multiline_adjlist(fname, nodetype=int) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_multiline_adjlist_multigraph(self, tmp_path): + G = self.XG + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist(fname, nodetype=int, create_using=nx.MultiGraph()) + H2 = nx.read_multiline_adjlist( + fname, nodetype=int, create_using=nx.MultiGraph() + ) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_multiline_adjlist_multidigraph(self, tmp_path): + G = self.XDG + fname = tmp_path / "adjlist.txt" + nx.write_multiline_adjlist(G, fname) + H = nx.read_multiline_adjlist( + fname, nodetype=int, create_using=nx.MultiDiGraph() + ) + H2 = nx.read_multiline_adjlist( + fname, nodetype=int, create_using=nx.MultiDiGraph() + ) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + def test_multiline_adjlist_delimiter(self): + fh = io.BytesIO() + G = nx.path_graph(3) + nx.write_multiline_adjlist(G, fh, delimiter=":") + fh.seek(0) + H = nx.read_multiline_adjlist(fh, nodetype=int, delimiter=":") + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + +@pytest.mark.parametrize( + ("lines", "delim"), + ( + (["1 2 5", "2 3 4", "3 5", "4", "5"], None), # No extra whitespace + (["1\t2\t5", "2\t3\t4", "3\t5", "4", "5"], "\t"), # tab-delimited + ( + ["1\t2\t5", "2\t3\t4", "3\t5\t", "4\t", "5"], + "\t", + ), # tab-delimited, extra delims + ( + ["1\t2\t5", "2\t3\t4", "3\t5\t\t\n", "4\t", "5"], + "\t", + ), # extra delim+newlines + ), +) +def test_adjlist_rstrip_parsing(lines, delim): + """Regression test related to gh-7465""" + expected = nx.Graph([(1, 2), (1, 5), (2, 3), (2, 4), (3, 5)]) + nx.utils.graphs_equal(nx.parse_adjlist(lines, delimiter=delim), expected) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py new file mode 100644 index 0000000000000000000000000000000000000000..a185cf83736f0cfe901dd5c2f6cbea941e55c485 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_edgelist.py @@ -0,0 +1,318 @@ +""" +Unit tests for edgelists. +""" + +import io +import textwrap + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, graphs_equal, nodes_equal + +edges_no_data = textwrap.dedent( + """ + # comment line + 1 2 + # comment line + 2 3 + """ +) + + +edges_with_values = textwrap.dedent( + """ + # comment line + 1 2 2.0 + # comment line + 2 3 3.0 + """ +) + + +edges_with_weight = textwrap.dedent( + """ + # comment line + 1 2 {'weight':2.0} + # comment line + 2 3 {'weight':3.0} + """ +) + + +edges_with_multiple_attrs = textwrap.dedent( + """ + # comment line + 1 2 {'weight':2.0, 'color':'green'} + # comment line + 2 3 {'weight':3.0, 'color':'red'} + """ +) + + +edges_with_multiple_attrs_csv = textwrap.dedent( + """ + # comment line + 1, 2, {'weight':2.0, 'color':'green'} + # comment line + 2, 3, {'weight':3.0, 'color':'red'} + """ +) + + +_expected_edges_weights = [(1, 2, {"weight": 2.0}), (2, 3, {"weight": 3.0})] +_expected_edges_multiattr = [ + (1, 2, {"weight": 2.0, "color": "green"}), + (2, 3, {"weight": 3.0, "color": "red"}), +] + + +@pytest.mark.parametrize( + ("data", "extra_kwargs"), + ( + (edges_no_data, {}), + (edges_with_values, {}), + (edges_with_weight, {}), + (edges_with_multiple_attrs, {}), + (edges_with_multiple_attrs_csv, {"delimiter": ","}), + ), +) +def test_read_edgelist_no_data(data, extra_kwargs): + bytesIO = io.BytesIO(data.encode("utf-8")) + G = nx.read_edgelist(bytesIO, nodetype=int, data=False, **extra_kwargs) + assert edges_equal(G.edges(), [(1, 2), (2, 3)]) + + +def test_read_weighted_edgelist(): + bytesIO = io.BytesIO(edges_with_values.encode("utf-8")) + G = nx.read_weighted_edgelist(bytesIO, nodetype=int) + assert edges_equal(G.edges(data=True), _expected_edges_weights) + + +@pytest.mark.parametrize( + ("data", "extra_kwargs", "expected"), + ( + (edges_with_weight, {}, _expected_edges_weights), + (edges_with_multiple_attrs, {}, _expected_edges_multiattr), + ( + edges_with_multiple_attrs_csv, + {"delimiter": ","}, + _expected_edges_multiattr, + ), + ), +) +def test_read_edgelist_with_data(data, extra_kwargs, expected): + bytesIO = io.BytesIO(data.encode("utf-8")) + G = nx.read_edgelist(bytesIO, nodetype=int, **extra_kwargs) + assert edges_equal(G.edges(data=True), expected) + + +@pytest.fixture +def example_graph(): + G = nx.Graph() + G.add_weighted_edges_from([(1, 2, 3.0), (2, 3, 27.0), (3, 4, 3.0)]) + return G + + +def test_parse_edgelist_no_data(example_graph): + G = example_graph + H = nx.parse_edgelist(["1 2", "2 3", "3 4"], nodetype=int) + assert nodes_equal(G.nodes, H.nodes) + assert edges_equal(G.edges, H.edges) + + +def test_parse_edgelist_with_data_dict(example_graph): + G = example_graph + H = nx.parse_edgelist( + ["1 2 {'weight': 3}", "2 3 {'weight': 27}", "3 4 {'weight': 3.0}"], nodetype=int + ) + assert nodes_equal(G.nodes, H.nodes) + assert edges_equal(G.edges(data=True), H.edges(data=True)) + + +def test_parse_edgelist_with_data_list(example_graph): + G = example_graph + H = nx.parse_edgelist( + ["1 2 3", "2 3 27", "3 4 3.0"], nodetype=int, data=(("weight", float),) + ) + assert nodes_equal(G.nodes, H.nodes) + assert edges_equal(G.edges(data=True), H.edges(data=True)) + + +def test_parse_edgelist(): + # ignore lines with less than 2 nodes + lines = ["1;2", "2 3", "3 4"] + G = nx.parse_edgelist(lines, nodetype=int) + assert list(G.edges()) == [(2, 3), (3, 4)] + # unknown nodetype + with pytest.raises(TypeError, match="Failed to convert nodes"): + lines = ["1 2", "2 3", "3 4"] + nx.parse_edgelist(lines, nodetype="nope") + # lines have invalid edge format + with pytest.raises(TypeError, match="Failed to convert edge data"): + lines = ["1 2 3", "2 3", "3 4"] + nx.parse_edgelist(lines, nodetype=int) + # edge data and data_keys not the same length + with pytest.raises(IndexError, match="not the same length"): + lines = ["1 2 3", "2 3 27", "3 4 3.0"] + nx.parse_edgelist( + lines, nodetype=int, data=(("weight", float), ("capacity", int)) + ) + # edge data can't be converted to edge type + with pytest.raises(TypeError, match="Failed to convert"): + lines = ["1 2 't1'", "2 3 't3'", "3 4 't3'"] + nx.parse_edgelist(lines, nodetype=int, data=(("weight", float),)) + + +def test_comments_None(): + edgelist = ["node#1 node#2", "node#2 node#3"] + # comments=None supported to ignore all comment characters + G = nx.parse_edgelist(edgelist, comments=None) + H = nx.Graph([e.split(" ") for e in edgelist]) + assert edges_equal(G.edges, H.edges) + + +class TestEdgelist: + @classmethod + def setup_class(cls): + cls.G = nx.Graph(name="test") + e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")] + cls.G.add_edges_from(e) + cls.G.add_node("g") + cls.DG = nx.DiGraph(cls.G) + cls.XG = nx.MultiGraph() + cls.XG.add_weighted_edges_from([(1, 2, 5), (1, 2, 5), (1, 2, 1), (3, 3, 42)]) + cls.XDG = nx.MultiDiGraph(cls.XG) + + def test_write_edgelist_1(self): + fh = io.BytesIO() + G = nx.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + nx.write_edgelist(G, fh, data=False) + fh.seek(0) + assert fh.read() == b"1 2\n2 3\n" + + def test_write_edgelist_2(self): + fh = io.BytesIO() + G = nx.Graph() + G.add_edges_from([(1, 2), (2, 3)]) + nx.write_edgelist(G, fh, data=True) + fh.seek(0) + assert fh.read() == b"1 2 {}\n2 3 {}\n" + + def test_write_edgelist_3(self): + fh = io.BytesIO() + G = nx.Graph() + G.add_edge(1, 2, weight=2.0) + G.add_edge(2, 3, weight=3.0) + nx.write_edgelist(G, fh, data=True) + fh.seek(0) + assert fh.read() == b"1 2 {'weight': 2.0}\n2 3 {'weight': 3.0}\n" + + def test_write_edgelist_4(self): + fh = io.BytesIO() + G = nx.Graph() + G.add_edge(1, 2, weight=2.0) + G.add_edge(2, 3, weight=3.0) + nx.write_edgelist(G, fh, data=[("weight")]) + fh.seek(0) + assert fh.read() == b"1 2 2.0\n2 3 3.0\n" + + def test_unicode(self, tmp_path): + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + G.add_edge(name1, "Radiohead", **{name2: 3}) + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname) + assert graphs_equal(G, H) + + def test_latin1_issue(self, tmp_path): + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + G.add_edge(name1, "Radiohead", **{name2: 3}) + fname = tmp_path / "el.txt" + with pytest.raises(UnicodeEncodeError): + nx.write_edgelist(G, fname, encoding="latin-1") + + def test_latin1(self, tmp_path): + G = nx.Graph() + name1 = "Bj" + chr(246) + "rk" + name2 = chr(220) + "ber" + G.add_edge(name1, "Radiohead", **{name2: 3}) + fname = tmp_path / "el.txt" + + nx.write_edgelist(G, fname, encoding="latin-1") + H = nx.read_edgelist(fname, encoding="latin-1") + assert graphs_equal(G, H) + + def test_edgelist_graph(self, tmp_path): + G = self.G + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname) + H2 = nx.read_edgelist(fname) + assert H is not H2 # they should be different graphs + G.remove_node("g") # isolated nodes are not written in edgelist + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_edgelist_digraph(self, tmp_path): + G = self.DG + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname, create_using=nx.DiGraph()) + H2 = nx.read_edgelist(fname, create_using=nx.DiGraph()) + assert H is not H2 # they should be different graphs + G.remove_node("g") # isolated nodes are not written in edgelist + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + def test_edgelist_integers(self, tmp_path): + G = nx.convert_node_labels_to_integers(self.G) + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname, nodetype=int) + # isolated nodes are not written in edgelist + G.remove_nodes_from(list(nx.isolates(G))) + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_edgelist_multigraph(self, tmp_path): + G = self.XG + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph()) + H2 = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges())) + + def test_edgelist_multidigraph(self, tmp_path): + G = self.XDG + fname = tmp_path / "el.txt" + nx.write_edgelist(G, fname) + H = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiDiGraph()) + H2 = nx.read_edgelist(fname, nodetype=int, create_using=nx.MultiDiGraph()) + assert H is not H2 # they should be different graphs + assert nodes_equal(list(H), list(G)) + assert edges_equal(list(H.edges()), list(G.edges()), directed=True) + + +def test_edgelist_consistent_strip_handling(): + """See gh-7462 + + Input when printed looks like:: + + 1 2 3 + 2 3 + 3 4 3.0 + + Note the trailing \\t after the `3` in the second row, indicating an empty + data value. + """ + s = io.StringIO("1\t2\t3\n2\t3\t\n3\t4\t3.0") + G = nx.parse_edgelist(s, delimiter="\t", nodetype=int, data=[("value", str)]) + assert sorted(G.edges(data="value")) == [(1, 2, "3"), (2, 3, ""), (3, 4, "3.0")] diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py new file mode 100644 index 0000000000000000000000000000000000000000..4e487cc6f4afe6de138eabb04633b8427f4999da --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gexf.py @@ -0,0 +1,612 @@ +import io +import time + +import pytest + +import networkx as nx + + +def test_gexf_v1_3(tmp_path): + """'Basic graph' example from https://gexf.net/schema.html""" + # GEXF file from published example + data = """ + + + + + + + + + + + +""" + with open(fname := (tmp_path / "basic.gexf"), "w") as fh: + fh.write(data) + + # Expected output based on xml input + expected = nx.DiGraph([("0", "1")]) + nx.set_node_attributes(expected, {"0": "Hello", "1": "Word"}, name="label") + expected.graph = {"mode": "static", "edge_default": {}} + + # Load example with version explicitly set + G = nx.read_gexf(fname, version="1.3") + assert nx.utils.graphs_equal(G, expected) + + # And with the "default" version + G = nx.read_gexf(fname) + assert nx.utils.graphs_equal(G, expected) + + +@pytest.mark.parametrize("time_attr", ("start", "end")) +@pytest.mark.parametrize("dyn_attr", ("static", "dynamic")) +def test_dynamic_graph_has_timeformat(time_attr, dyn_attr, tmp_path): + """Ensure that graphs which have a 'start' or 'stop' attribute get a + 'timeformat' attribute upon parsing. See gh-7914.""" + G = nx.MultiGraph(mode=dyn_attr) + G.add_node(0) + G.nodes[0][time_attr] = 1 + # Write out + fname = tmp_path / "foo.gexf" + nx.write_gexf(G, fname) + # Check that timeformat is added to saved data + with open(fname) as fh: + assert 'timeformat="long"' in fh.read() + # Round-trip + H = nx.read_gexf(fname) + # If any node has a "start" or "end" attr, it is considered dynamic + # regardless of the graph "mode" attr + assert H.graph["mode"] == "dynamic" + assert nx.utils.nodes_equal(G.edges, H.edges) + + +class TestGEXF: + @classmethod + def setup_class(cls): + cls.simple_directed_data = """ + + + + + + + + + + + +""" + cls.simple_directed_graph = nx.DiGraph() + cls.simple_directed_graph.add_node("0", label="Hello") + cls.simple_directed_graph.add_node("1", label="World") + cls.simple_directed_graph.add_edge("0", "1", id="0") + + cls.simple_directed_fh = io.BytesIO(cls.simple_directed_data.encode("UTF-8")) + + cls.attribute_data = """\ + + + Gephi.org + A Web network + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + cls.attribute_graph = nx.DiGraph() + cls.attribute_graph.graph["node_default"] = {"frog": True} + cls.attribute_graph.add_node( + "0", label="Gephi", url="https://gephi.org", indegree=1, frog=False + ) + cls.attribute_graph.add_node( + "1", label="Webatlas", url="http://webatlas.fr", indegree=2, frog=False + ) + cls.attribute_graph.add_node( + "2", label="RTGI", url="http://rtgi.fr", indegree=1, frog=True + ) + cls.attribute_graph.add_node( + "3", + label="BarabasiLab", + url="http://barabasilab.com", + indegree=1, + frog=True, + ) + cls.attribute_graph.add_edge("0", "1", id="0", label="foo") + cls.attribute_graph.add_edge("0", "2", id="1") + cls.attribute_graph.add_edge("1", "0", id="2") + cls.attribute_graph.add_edge("2", "1", id="3") + cls.attribute_graph.add_edge("0", "3", id="4") + cls.attribute_fh = io.BytesIO(cls.attribute_data.encode("UTF-8")) + + cls.simple_undirected_data = """ + + + + + + + + + + + +""" + cls.simple_undirected_graph = nx.Graph() + cls.simple_undirected_graph.add_node("0", label="Hello") + cls.simple_undirected_graph.add_node("1", label="World") + cls.simple_undirected_graph.add_edge("0", "1", id="0") + + cls.simple_undirected_fh = io.BytesIO( + cls.simple_undirected_data.encode("UTF-8") + ) + + def test_read_simple_directed_graphml(self): + G = self.simple_directed_graph + H = nx.read_gexf(self.simple_directed_fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(G.edges()) == sorted(H.edges()) + assert sorted(G.edges(data=True)) == sorted(H.edges(data=True)) + self.simple_directed_fh.seek(0) + + def test_write_read_simple_directed_graphml(self): + G = self.simple_directed_graph + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(G.edges()) == sorted(H.edges()) + assert sorted(G.edges(data=True)) == sorted(H.edges(data=True)) + self.simple_directed_fh.seek(0) + + def test_read_simple_undirected_graphml(self): + G = self.simple_undirected_graph + H = nx.read_gexf(self.simple_undirected_fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + self.simple_undirected_fh.seek(0) + + def test_read_attribute_graphml(self): + G = self.attribute_graph + H = nx.read_gexf(self.attribute_fh) + assert sorted(G.nodes(True)) == sorted(H.nodes(data=True)) + ge = sorted(G.edges(data=True)) + he = sorted(H.edges(data=True)) + for a, b in zip(ge, he): + assert a == b + self.attribute_fh.seek(0) + + def test_directed_edge_in_undirected(self): + s = """ + + + + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_gexf, fh) + + def test_undirected_edge_in_directed(self): + s = """ + + + + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_gexf, fh) + + def test_key_raises(self): + s = """ + + + + + + + + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_gexf, fh) + + def test_relabel(self): + s = """ + + + + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_gexf(fh, relabel=True) + assert sorted(G.nodes()) == ["Hello", "Word"] + + def test_default_attribute(self): + G = nx.Graph() + G.add_node(1, label="1", color="green") + nx.add_path(G, [0, 1, 2, 3]) + G.add_edge(1, 2, foo=3) + G.graph["node_default"] = {"color": "yellow"} + G.graph["edge_default"] = {"foo": 7} + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + # Reading a gexf graph always sets mode attribute to either + # 'static' or 'dynamic'. Remove the mode attribute from the + # read graph for the sake of comparing remaining attributes. + del H.graph["mode"] + assert G.graph == H.graph + + def test_serialize_ints_to_strings(self): + G = nx.Graph() + G.add_node(1, id=7, label=77) + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert list(H) == [7] + assert H.nodes[7]["label"] == "77" + + def test_write_with_node_attributes(self): + # Addresses #673. + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (2, 3)]) + for i in range(4): + G.nodes[i]["id"] = i + G.nodes[i]["label"] = i + G.nodes[i]["pid"] = i + G.nodes[i]["start"] = i + G.nodes[i]["end"] = i + 1 + + expected = f""" + + NetworkX {nx.__version__} + + + + + + + + + + + + + + +""" + obtained = "\n".join(nx.generate_gexf(G)) + assert expected == obtained + + def test_edge_id_construct(self): + G = nx.Graph() + G.add_edges_from([(0, 1, {"id": 0}), (1, 2, {"id": 2}), (2, 3)]) + + expected = f""" + + NetworkX {nx.__version__} + + + + + + + + + + + + + + +""" + + obtained = "\n".join(nx.generate_gexf(G)) + assert expected == obtained + + def test_numpy_type(self): + np = pytest.importorskip("numpy") + G = nx.path_graph(4) + nx.set_node_attributes(G, {n: n for n in np.arange(4)}, "number") + G[0][1]["edge-number"] = np.float64(1.1) + + expected = f""" + + NetworkX {nx.__version__} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + obtained = "\n".join(nx.generate_gexf(G)) + assert expected == obtained + + def test_bool(self): + G = nx.Graph() + G.add_node(1, testattr=True) + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert H.nodes[1]["testattr"] + + # Test for NaN, INF and -INF + def test_specials(self): + from math import isnan + + inf, nan = float("inf"), float("nan") + G = nx.Graph() + G.add_node(1, testattr=inf, strdata="inf", key="a") + G.add_node(2, testattr=nan, strdata="nan", key="b") + G.add_node(3, testattr=-inf, strdata="-inf", key="c") + + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + filetext = fh.read() + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + + assert b"INF" in filetext + assert b"NaN" in filetext + assert b"-INF" in filetext + + assert H.nodes[1]["testattr"] == inf + assert isnan(H.nodes[2]["testattr"]) + assert H.nodes[3]["testattr"] == -inf + + assert H.nodes[1]["strdata"] == "inf" + assert H.nodes[2]["strdata"] == "nan" + assert H.nodes[3]["strdata"] == "-inf" + + assert H.nodes[1]["networkx_key"] == "a" + assert H.nodes[2]["networkx_key"] == "b" + assert H.nodes[3]["networkx_key"] == "c" + + def test_simple_list(self): + G = nx.Graph() + list_value = [(1, 2, 3), (9, 1, 2)] + G.add_node(1, key=list_value) + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert H.nodes[1]["networkx_key"] == list_value + + def test_dynamic_mode(self): + G = nx.Graph() + G.add_node(1, label="1", color="green") + G.graph["mode"] = "dynamic" + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + def test_multigraph_with_missing_attributes(self): + G = nx.MultiGraph() + G.add_node(0, label="1", color="green") + G.add_node(1, label="2", color="green") + G.add_edge(0, 1, id="0", weight=3, type="undirected", start=0, end=1) + G.add_edge(0, 1, id="1", label="foo", start=0, end=1) + G.add_edge(0, 1) + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + def test_missing_viz_attributes(self): + G = nx.Graph() + G.add_node(0, label="1", color="green") + G.nodes[0]["viz"] = {"size": 54} + G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1, "z": 0} + G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256} + G.nodes[0]["viz"]["shape"] = "http://random.url" + G.nodes[0]["viz"]["thickness"] = 2 + fh = io.BytesIO() + nx.write_gexf(G, fh, version="1.1draft") + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + # Test missing alpha value for version >draft1.1 - set default alpha value + # to 1.0 instead of `None` when writing for better general compatibility + fh = io.BytesIO() + # G.nodes[0]["viz"]["color"] does not have an alpha value explicitly defined + # so the default is used instead + nx.write_gexf(G, fh, version="1.2draft") + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert H.nodes[0]["viz"]["color"]["a"] == 1.0 + + # Second graph for the other branch + G = nx.Graph() + G.add_node(0, label="1", color="green") + G.nodes[0]["viz"] = {"size": 54} + G.nodes[0]["viz"]["position"] = {"x": 0, "y": 1, "z": 0} + G.nodes[0]["viz"]["color"] = {"r": 0, "g": 0, "b": 256, "a": 0.5} + G.nodes[0]["viz"]["shape"] = "ftp://random.url" + G.nodes[0]["viz"]["thickness"] = 2 + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + def test_slice_and_spell(self): + # Test spell first, so version = 1.2 + G = nx.Graph() + G.add_node(0, label="1", color="green") + G.nodes[0]["spells"] = [(1, 2)] + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + G = nx.Graph() + G.add_node(0, label="1", color="green") + G.nodes[0]["slices"] = [(1, 2)] + fh = io.BytesIO() + nx.write_gexf(G, fh, version="1.1draft") + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) + + def test_add_parent(self): + G = nx.Graph() + G.add_node(0, label="1", color="green", parents=[1, 2]) + fh = io.BytesIO() + nx.write_gexf(G, fh) + fh.seek(0) + H = nx.read_gexf(fh, node_type=int) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(sorted(e) for e in G.edges()) == sorted( + sorted(e) for e in H.edges() + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py new file mode 100644 index 0000000000000000000000000000000000000000..df250979b149b7276babed267d59cb7cbcab22c6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_gml.py @@ -0,0 +1,744 @@ +import codecs +import io +import math +from ast import literal_eval +from contextlib import contextmanager +from textwrap import dedent + +import pytest + +import networkx as nx +from networkx.readwrite.gml import literal_destringizer, literal_stringizer + + +class TestGraph: + @classmethod + def setup_class(cls): + cls.simple_data = """Creator "me" +Version "xx" +graph [ + comment "This is a sample graph" + directed 1 + IsPlanar 1 + pos [ x 0 y 1 ] + node [ + id 1 + label "Node 1" + pos [ x 1 y 1 ] + ] + node [ + id 2 + pos [ x 1 y 2 ] + label "Node 2" + ] + node [ + id 3 + label "Node 3" + pos [ x 1 y 3 ] + ] + edge [ + source 1 + target 2 + label "Edge from node 1 to node 2" + color [line "blue" thickness 3] + + ] + edge [ + source 2 + target 3 + label "Edge from node 2 to node 3" + ] + edge [ + source 3 + target 1 + label "Edge from node 3 to node 1" + ] +] +""" + + def test_parse_gml_cytoscape_bug(self): + # example from issue #321, originally #324 in trac + cytoscape_example = """ +Creator "Cytoscape" +Version 1.0 +graph [ + node [ + root_index -3 + id -3 + graphics [ + x -96.0 + y -67.0 + w 40.0 + h 40.0 + fill "#ff9999" + type "ellipse" + outline "#666666" + outline_width 1.5 + ] + label "node2" + ] + node [ + root_index -2 + id -2 + graphics [ + x 63.0 + y 37.0 + w 40.0 + h 40.0 + fill "#ff9999" + type "ellipse" + outline "#666666" + outline_width 1.5 + ] + label "node1" + ] + node [ + root_index -1 + id -1 + graphics [ + x -31.0 + y -17.0 + w 40.0 + h 40.0 + fill "#ff9999" + type "ellipse" + outline "#666666" + outline_width 1.5 + ] + label "node0" + ] + edge [ + root_index -2 + target -2 + source -1 + graphics [ + width 1.5 + fill "#0000ff" + type "line" + Line [ + ] + source_arrow 0 + target_arrow 3 + ] + label "DirectedEdge" + ] + edge [ + root_index -1 + target -1 + source -3 + graphics [ + width 1.5 + fill "#0000ff" + type "line" + Line [ + ] + source_arrow 0 + target_arrow 3 + ] + label "DirectedEdge" + ] +] +""" + nx.parse_gml(cytoscape_example) + + def test_parse_gml(self): + G = nx.parse_gml(self.simple_data, label="label") + assert sorted(G.nodes()) == ["Node 1", "Node 2", "Node 3"] + assert sorted(G.edges()) == [ + ("Node 1", "Node 2"), + ("Node 2", "Node 3"), + ("Node 3", "Node 1"), + ] + + assert sorted(G.edges(data=True)) == [ + ( + "Node 1", + "Node 2", + { + "color": {"line": "blue", "thickness": 3}, + "label": "Edge from node 1 to node 2", + }, + ), + ("Node 2", "Node 3", {"label": "Edge from node 2 to node 3"}), + ("Node 3", "Node 1", {"label": "Edge from node 3 to node 1"}), + ] + + def test_read_gml(self, tmp_path): + fname = tmp_path / "test.gml" + with open(fname, "w") as fh: + fh.write(self.simple_data) + Gin = nx.read_gml(fname, label="label") + G = nx.parse_gml(self.simple_data, label="label") + assert sorted(G.nodes(data=True)) == sorted(Gin.nodes(data=True)) + assert sorted(G.edges(data=True)) == sorted(Gin.edges(data=True)) + + def test_labels_are_strings(self): + # GML requires labels to be strings (i.e., in quotes) + answer = """graph [ + node [ + id 0 + label "1203" + ] +]""" + G = nx.Graph() + G.add_node(1203) + data = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer)) + assert data == answer + + def test_relabel_duplicate(self): + data = """ +graph +[ + label "" + directed 1 + node + [ + id 0 + label "same" + ] + node + [ + id 1 + label "same" + ] +] +""" + fh = io.BytesIO(data.encode("UTF-8")) + fh.seek(0) + pytest.raises(nx.NetworkXError, nx.read_gml, fh, label="label") + + @pytest.mark.parametrize("stringizer", (None, literal_stringizer)) + def test_tuplelabels(self, stringizer): + # https://github.com/networkx/networkx/pull/1048 + # Writing tuple labels to GML failed. + G = nx.Graph() + G.add_edge((0, 1), (1, 0)) + data = "\n".join(nx.generate_gml(G, stringizer=stringizer)) + answer = """graph [ + node [ + id 0 + label "(0,1)" + ] + node [ + id 1 + label "(1,0)" + ] + edge [ + source 0 + target 1 + ] +]""" + assert data == answer + + def test_quotes(self, tmp_path): + # https://github.com/networkx/networkx/issues/1061 + # Encoding quotes as HTML entities. + G = nx.path_graph(1) + G.name = "path_graph(1)" + attr = 'This is "quoted" and this is a copyright: ' + chr(169) + G.nodes[0]["demo"] = attr + with open(tmp_path / "test.gml", "w+b") as fobj: + nx.write_gml(G, fobj) + fobj.seek(0) + # Should be bytes in 2.x and 3.x + data = fobj.read().strip().decode("ascii") + answer = """graph [ + name "path_graph(1)" + node [ + id 0 + label "0" + demo "This is "quoted" and this is a copyright: ©" + ] +]""" + assert data == answer + + def test_unicode_node(self, tmp_path): + node = "node" + chr(169) + G = nx.Graph() + G.add_node(node) + with open(tmp_path / "test.gml", "w+b") as fobj: + nx.write_gml(G, fobj) + fobj.seek(0) + # Should be bytes in 2.x and 3.x + data = fobj.read().strip().decode("ascii") + answer = """graph [ + node [ + id 0 + label "node©" + ] +]""" + assert data == answer + + def test_float_label(self, tmp_path): + node = 1.0 + G = nx.Graph() + G.add_node(node) + with open(tmp_path / "test.gml", "w+b") as fobj: + nx.write_gml(G, fobj) + fobj.seek(0) + # Should be bytes in 2.x and 3.x + data = fobj.read().strip().decode("ascii") + answer = """graph [ + node [ + id 0 + label "1.0" + ] +]""" + assert data == answer + + def test_special_float_label(self, tmp_path): + special_floats = [float("nan"), float("+inf"), float("-inf")] + try: + import numpy as np + + special_floats += [np.nan, np.inf, np.inf * -1] + except ImportError: + special_floats += special_floats + + G = nx.cycle_graph(len(special_floats)) + attrs = dict(enumerate(special_floats)) + nx.set_node_attributes(G, attrs, "nodefloat") + edges = list(G.edges) + attrs = {edges[i]: value for i, value in enumerate(special_floats)} + nx.set_edge_attributes(G, attrs, "edgefloat") + + with open(tmp_path / "test.gml", "w+b") as fobj: + nx.write_gml(G, fobj) + fobj.seek(0) + # Should be bytes in 2.x and 3.x + data = fobj.read().strip().decode("ascii") + answer = """graph [ + node [ + id 0 + label "0" + nodefloat NAN + ] + node [ + id 1 + label "1" + nodefloat +INF + ] + node [ + id 2 + label "2" + nodefloat -INF + ] + node [ + id 3 + label "3" + nodefloat NAN + ] + node [ + id 4 + label "4" + nodefloat +INF + ] + node [ + id 5 + label "5" + nodefloat -INF + ] + edge [ + source 0 + target 1 + edgefloat NAN + ] + edge [ + source 0 + target 5 + edgefloat +INF + ] + edge [ + source 1 + target 2 + edgefloat -INF + ] + edge [ + source 2 + target 3 + edgefloat NAN + ] + edge [ + source 3 + target 4 + edgefloat +INF + ] + edge [ + source 4 + target 5 + edgefloat -INF + ] +]""" + assert data == answer + + fobj.seek(0) + graph = nx.read_gml(fobj) + for indx, value in enumerate(special_floats): + node_value = graph.nodes[str(indx)]["nodefloat"] + if math.isnan(value): + assert math.isnan(node_value) + else: + assert node_value == value + + edge = edges[indx] + string_edge = (str(edge[0]), str(edge[1])) + edge_value = graph.edges[string_edge]["edgefloat"] + if math.isnan(value): + assert math.isnan(edge_value) + else: + assert edge_value == value + + def test_name(self): + G = nx.parse_gml('graph [ name "x" node [ id 0 label "x" ] ]') + assert "x" == G.graph["name"] + G = nx.parse_gml('graph [ node [ id 0 label "x" ] ]') + assert "" == G.name + assert "name" not in G.graph + + def test_graph_types(self): + for directed in [None, False, True]: + for multigraph in [None, False, True]: + gml = "graph [" + if directed is not None: + gml += " directed " + str(int(directed)) + if multigraph is not None: + gml += " multigraph " + str(int(multigraph)) + gml += ' node [ id 0 label "0" ]' + gml += " edge [ source 0 target 0 ]" + gml += " ]" + G = nx.parse_gml(gml) + assert bool(directed) == G.is_directed() + assert bool(multigraph) == G.is_multigraph() + gml = "graph [\n" + if directed is True: + gml += " directed 1\n" + if multigraph is True: + gml += " multigraph 1\n" + gml += """ node [ + id 0 + label "0" + ] + edge [ + source 0 + target 0 +""" + if multigraph: + gml += " key 0\n" + gml += " ]\n]" + assert gml == "\n".join(nx.generate_gml(G)) + + def test_data_types(self): + data = [ + True, + False, + 10**20, + -2e33, + "'", + '"&&&""', + [{(b"\xfd",): "\x7f", chr(0x4444): (1, 2)}, (2, "3")], + ] + data.append(chr(0x14444)) + data.append(literal_eval("{2.3j, 1 - 2.3j, ()}")) + G = nx.Graph() + G.name = data + G.graph["data"] = data + G.add_node(0, int=-1, data={"data": data}) + G.add_edge(0, 0, float=-2.5, data=data) + gml = "\n".join(nx.generate_gml(G, stringizer=literal_stringizer)) + G = nx.parse_gml(gml, destringizer=literal_destringizer) + assert data == G.name + assert {"name": data, "data": data} == G.graph + assert list(G.nodes(data=True)) == [(0, {"int": -1, "data": {"data": data}})] + assert list(G.edges(data=True)) == [(0, 0, {"float": -2.5, "data": data})] + G = nx.Graph() + G.graph["data"] = "frozenset([1, 2, 3])" + G = nx.parse_gml(nx.generate_gml(G), destringizer=literal_eval) + assert G.graph["data"] == "frozenset([1, 2, 3])" + + def test_escape_unescape(self): + gml = """graph [ + name "&"䑄��&unknown;" +]""" + G = nx.parse_gml(gml) + assert ( + '&"\x0f' + chr(0x4444) + "��&unknown;" + == G.name + ) + gml = "\n".join(nx.generate_gml(G)) + alnu = "#1234567890;&#x1234567890abcdef" + answer = ( + """graph [ + name "&"䑄&""" + + alnu + + """;&unknown;" +]""" + ) + assert answer == gml + + def test_exceptions(self, tmp_path): + pytest.raises(ValueError, literal_destringizer, "(") + pytest.raises(ValueError, literal_destringizer, "frozenset([1, 2, 3])") + pytest.raises(ValueError, literal_destringizer, literal_destringizer) + pytest.raises(ValueError, literal_stringizer, frozenset([1, 2, 3])) + pytest.raises(ValueError, literal_stringizer, literal_stringizer) + with open(tmp_path / "test.gml", "w+b") as f: + f.write(codecs.BOM_UTF8 + b"graph[]") + f.seek(0) + pytest.raises(nx.NetworkXError, nx.read_gml, f) + + def assert_parse_error(gml): + pytest.raises(nx.NetworkXError, nx.parse_gml, gml) + + assert_parse_error(["graph [\n\n", "]"]) + assert_parse_error("") + assert_parse_error('Creator ""') + assert_parse_error("0") + assert_parse_error("graph ]") + assert_parse_error("graph [ 1 ]") + assert_parse_error("graph [ 1.E+2 ]") + assert_parse_error('graph [ "A" ]') + assert_parse_error("graph [ ] graph ]") + assert_parse_error("graph [ ] graph [ ]") + assert_parse_error("graph [ data [1, 2, 3] ]") + assert_parse_error("graph [ node [ ] ]") + assert_parse_error("graph [ node [ id 0 ] ]") + nx.parse_gml('graph [ node [ id "a" ] ]', label="id") + assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 0 label 1 ] ]") + assert_parse_error("graph [ node [ id 0 label 0 ] node [ id 1 label 0 ] ]") + assert_parse_error("graph [ node [ id 0 label 0 ] edge [ ] ]") + assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 ] ]") + nx.parse_gml("graph [edge [ source 0 target 0 ] node [ id 0 label 0 ] ]") + assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 1 target 0 ] ]") + assert_parse_error("graph [ node [ id 0 label 0 ] edge [ source 0 target 1 ] ]") + assert_parse_error( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 ] edge [ source 1 target 0 ] ]" + ) + nx.parse_gml( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 ] edge [ source 1 target 0 ] " + "directed 1 ]" + ) + nx.parse_gml( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 ] edge [ source 0 target 1 ]" + "multigraph 1 ]" + ) + nx.parse_gml( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 ]" + "multigraph 1 ]" + ) + assert_parse_error( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 key 0 ] edge [ source 0 target 1 key 0 ]" + "multigraph 1 ]" + ) + nx.parse_gml( + "graph [ node [ id 0 label 0 ] node [ id 1 label 1 ] " + "edge [ source 0 target 1 key 0 ] edge [ source 1 target 0 key 0 ]" + "directed 1 multigraph 1 ]" + ) + + # Tests for string convertible alphanumeric id and label values + nx.parse_gml("graph [edge [ source a target a ] node [ id a label b ] ]") + nx.parse_gml( + "graph [ node [ id n42 label 0 ] node [ id x43 label 1 ]" + "edge [ source n42 target x43 key 0 ]" + "edge [ source x43 target n42 key 0 ]" + "directed 1 multigraph 1 ]" + ) + assert_parse_error( + "graph [edge [ source '\u4200' target '\u4200' ] " + + "node [ id '\u4200' label b ] ]" + ) + + def assert_generate_error(*args, **kwargs): + pytest.raises( + nx.NetworkXError, lambda: list(nx.generate_gml(*args, **kwargs)) + ) + + G = nx.Graph() + G.graph[3] = 3 + assert_generate_error(G) + G = nx.Graph() + G.graph["3"] = 3 + assert_generate_error(G) + G = nx.Graph() + G.graph["data"] = frozenset([1, 2, 3]) + assert_generate_error(G, stringizer=literal_stringizer) + + def test_label_kwarg(self): + G = nx.parse_gml(self.simple_data, label="id") + assert sorted(G.nodes) == [1, 2, 3] + labels = [G.nodes[n]["label"] for n in sorted(G.nodes)] + assert labels == ["Node 1", "Node 2", "Node 3"] + + G = nx.parse_gml(self.simple_data, label=None) + assert sorted(G.nodes) == [1, 2, 3] + labels = [G.nodes[n]["label"] for n in sorted(G.nodes)] + assert labels == ["Node 1", "Node 2", "Node 3"] + + def test_outofrange_integers(self, tmp_path): + # GML restricts integers to 32 signed bits. + # Check that we honor this restriction on export + G = nx.Graph() + # Test export for numbers that barely fit or don't fit into 32 bits, + # and 3 numbers in the middle + numbers = { + "toosmall": (-(2**31)) - 1, + "small": -(2**31), + "med1": -4, + "med2": 0, + "med3": 17, + "big": (2**31) - 1, + "toobig": 2**31, + } + G.add_node("Node", **numbers) + + fname = tmp_path / "test.gml" + nx.write_gml(G, fname) + # Check that the export wrote the nonfitting numbers as strings + G2 = nx.read_gml(fname) + for attr, value in G2.nodes["Node"].items(): + if attr == "toosmall" or attr == "toobig": + assert isinstance(value, str) + else: + assert isinstance(value, int) + + def test_multiline(self): + # example from issue #6836 + multiline_example = """ +graph +[ + node + [ + id 0 + label "multiline node" + label2 "multiline1 + multiline2 + multiline3" + alt_name "id 0" + ] +] +""" + G = nx.parse_gml(multiline_example) + assert G.nodes["multiline node"] == { + "label2": "multiline1 multiline2 multiline3", + "alt_name": "id 0", + } + + +@contextmanager +def byte_file(): + _file_handle = io.BytesIO() + yield _file_handle + _file_handle.seek(0) + + +class TestPropertyLists: + def test_writing_graph_with_multi_element_property_list(self): + g = nx.Graph() + g.add_node("n1", properties=["element", 0, 1, 2.5, True, False]) + with byte_file() as f: + nx.write_gml(g, f) + result = f.read().decode() + + assert result == dedent( + """\ + graph [ + node [ + id 0 + label "n1" + properties "element" + properties 0 + properties 1 + properties 2.5 + properties 1 + properties 0 + ] + ] + """ + ) + + def test_writing_graph_with_one_element_property_list(self): + g = nx.Graph() + g.add_node("n1", properties=["element"]) + with byte_file() as f: + nx.write_gml(g, f) + result = f.read().decode() + + assert result == dedent( + """\ + graph [ + node [ + id 0 + label "n1" + properties "_networkx_list_start" + properties "element" + ] + ] + """ + ) + + def test_reading_graph_with_list_property(self): + with byte_file() as f: + f.write( + dedent( + """ + graph [ + node [ + id 0 + label "n1" + properties "element" + properties 0 + properties 1 + properties 2.5 + ] + ] + """ + ).encode("ascii") + ) + f.seek(0) + graph = nx.read_gml(f) + assert graph.nodes(data=True)["n1"] == {"properties": ["element", 0, 1, 2.5]} + + def test_reading_graph_with_single_element_list_property(self): + with byte_file() as f: + f.write( + dedent( + """ + graph [ + node [ + id 0 + label "n1" + properties "_networkx_list_start" + properties "element" + ] + ] + """ + ).encode("ascii") + ) + f.seek(0) + graph = nx.read_gml(f) + assert graph.nodes(data=True)["n1"] == {"properties": ["element"]} + + +@pytest.mark.parametrize("coll", ([], ())) +def test_stringize_empty_list_tuple(coll): + G = nx.path_graph(2) + G.nodes[0]["test"] = coll # test serializing an empty collection + f = io.BytesIO() + nx.write_gml(G, f) # Smoke test - should not raise + f.seek(0) + H = nx.read_gml(f) + assert H.nodes["0"]["test"] == coll # Check empty list round-trips properly + # Check full round-tripping. Note that nodes are loaded as strings by + # default, so there needs to be some remapping prior to comparison + H = nx.relabel_nodes(H, {"0": 0, "1": 1}) + assert nx.utils.graphs_equal(G, H) + # Same as above, but use destringizer for node remapping. Should have no + # effect on node attr + f.seek(0) + H = nx.read_gml(f, destringizer=int) + assert nx.utils.graphs_equal(G, H) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py new file mode 100644 index 0000000000000000000000000000000000000000..d680c2153d678250b4aa9b32f6ad7d0f6d8e80f6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_graph6.py @@ -0,0 +1,181 @@ +from io import BytesIO + +import pytest + +import networkx as nx +import networkx.readwrite.graph6 as g6 +from networkx.utils import edges_equal, nodes_equal + + +def test_from_graph6_invariant_to_trailing_newline(): + """See gh-7557""" + G = nx.from_graph6_bytes(b">>graph6<>graph6<>graph6<<\nP~~~~~~~~~~~~~~~~~~~~~~{") + + +class TestGraph6Utils: + def test_n_data_n_conversion(self): + for i in [0, 1, 42, 62, 63, 64, 258047, 258048, 7744773, 68719476735]: + assert g6.data_to_n(g6.n_to_data(i))[0] == i + assert g6.data_to_n(g6.n_to_data(i))[1] == [] + assert g6.data_to_n(g6.n_to_data(i) + [42, 43])[1] == [42, 43] + + +class TestFromGraph6Bytes: + def test_from_graph6_bytes(self): + data = b"DF{" + G = nx.from_graph6_bytes(data) + assert nodes_equal(G.nodes(), [0, 1, 2, 3, 4]) + assert edges_equal( + G.edges(), [(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + ) + + def test_read_equals_from_bytes(self): + data = b"DF{" + G = nx.from_graph6_bytes(data) + fh = BytesIO(data) + Gin = nx.read_graph6(fh) + assert nodes_equal(G.nodes(), Gin.nodes()) + assert edges_equal(G.edges(), Gin.edges()) + + +class TestReadGraph6: + def test_read_many_graph6(self): + """Test for reading many graphs from a file into a list.""" + data = b"DF{\nD`{\nDqK\nD~{\n" + fh = BytesIO(data) + glist = nx.read_graph6(fh) + assert len(glist) == 4 + for G in glist: + assert sorted(G) == list(range(5)) + + +class TestWriteGraph6: + """Unit tests for writing a graph to a file in graph6 format.""" + + def test_null_graph(self): + result = BytesIO() + nx.write_graph6(nx.null_graph(), result) + assert result.getvalue() == b">>graph6<>graph6<<@\n" + + def test_complete_graph(self): + result = BytesIO() + nx.write_graph6(nx.complete_graph(4), result) + assert result.getvalue() == b">>graph6<>graph6<>graph6<>graph6<>graph6<<@\n" + + def test_complete_graph(self): + assert g6.to_graph6_bytes(nx.complete_graph(4)) == b">>graph6<>graph6< + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + cls.simple_directed_graph = nx.DiGraph() + cls.simple_directed_graph.add_node("n10") + cls.simple_directed_graph.add_edge("n0", "n2", id="foo") + cls.simple_directed_graph.add_edge("n0", "n2") + cls.simple_directed_graph.add_edges_from( + [ + ("n1", "n2"), + ("n2", "n3"), + ("n3", "n5"), + ("n3", "n4"), + ("n4", "n6"), + ("n6", "n5"), + ("n5", "n7"), + ("n6", "n8"), + ("n8", "n7"), + ("n8", "n9"), + ] + ) + cls.simple_directed_fh = io.BytesIO(cls.simple_directed_data.encode("UTF-8")) + + cls.attribute_data = """ + + + yellow + + + + + green + + + + blue + + + red + + + + turquoise + + + 1.0 + + + 1.0 + + + 2.0 + + + + + + 1.1 + + + +""" + cls.attribute_graph = nx.DiGraph(id="G") + cls.attribute_graph.graph["node_default"] = {"color": "yellow"} + cls.attribute_graph.add_node("n0", color="green") + cls.attribute_graph.add_node("n2", color="blue") + cls.attribute_graph.add_node("n3", color="red") + cls.attribute_graph.add_node("n4") + cls.attribute_graph.add_node("n5", color="turquoise") + cls.attribute_graph.add_edge("n0", "n2", id="e0", weight=1.0) + cls.attribute_graph.add_edge("n0", "n1", id="e1", weight=1.0) + cls.attribute_graph.add_edge("n1", "n3", id="e2", weight=2.0) + cls.attribute_graph.add_edge("n3", "n2", id="e3") + cls.attribute_graph.add_edge("n2", "n4", id="e4") + cls.attribute_graph.add_edge("n3", "n5", id="e5") + cls.attribute_graph.add_edge("n5", "n4", id="e6", weight=1.1) + cls.attribute_fh = io.BytesIO(cls.attribute_data.encode("UTF-8")) + + cls.node_attribute_default_data = """ + + false + 0 + 0 + 0.0 + 0.0 + Foo + + + + + + + """ + cls.node_attribute_default_graph = nx.DiGraph(id="G") + cls.node_attribute_default_graph.graph["node_default"] = { + "boolean_attribute": False, + "int_attribute": 0, + "long_attribute": 0, + "float_attribute": 0.0, + "double_attribute": 0.0, + "string_attribute": "Foo", + } + cls.node_attribute_default_graph.add_node("n0") + cls.node_attribute_default_graph.add_node("n1") + cls.node_attribute_default_graph.add_edge("n0", "n1", id="e0") + cls.node_attribute_default_fh = io.BytesIO( + cls.node_attribute_default_data.encode("UTF-8") + ) + + cls.attribute_named_key_ids_data = """ + + + + + + + val1 + val2 + + + val_one + val2 + + + edge_value + + + +""" + cls.attribute_named_key_ids_graph = nx.DiGraph() + cls.attribute_named_key_ids_graph.add_node("0", prop1="val1", prop2="val2") + cls.attribute_named_key_ids_graph.add_node("1", prop1="val_one", prop2="val2") + cls.attribute_named_key_ids_graph.add_edge("0", "1", edge_prop="edge_value") + fh = io.BytesIO(cls.attribute_named_key_ids_data.encode("UTF-8")) + cls.attribute_named_key_ids_fh = fh + + cls.attribute_numeric_type_data = """ + + + + + + 1 + + + 2.0 + + + 1 + + + k + + + 1.0 + + + +""" + cls.attribute_numeric_type_graph = nx.DiGraph() + cls.attribute_numeric_type_graph.add_node("n0", weight=1) + cls.attribute_numeric_type_graph.add_node("n1", weight=2.0) + cls.attribute_numeric_type_graph.add_edge("n0", "n1", weight=1) + cls.attribute_numeric_type_graph.add_edge("n1", "n1", weight=1.0) + fh = io.BytesIO(cls.attribute_numeric_type_data.encode("UTF-8")) + cls.attribute_numeric_type_fh = fh + + cls.simple_undirected_data = """ + + + + + + + + + + +""" + # + cls.simple_undirected_graph = nx.Graph() + cls.simple_undirected_graph.add_node("n10") + cls.simple_undirected_graph.add_edge("n0", "n2", id="foo") + cls.simple_undirected_graph.add_edges_from([("n1", "n2"), ("n2", "n3")]) + fh = io.BytesIO(cls.simple_undirected_data.encode("UTF-8")) + cls.simple_undirected_fh = fh + + cls.undirected_multigraph_data = """ + + + + + + + + + + +""" + cls.undirected_multigraph = nx.MultiGraph() + cls.undirected_multigraph.add_node("n10") + cls.undirected_multigraph.add_edge("n0", "n2", id="e0") + cls.undirected_multigraph.add_edge("n1", "n2", id="e1") + cls.undirected_multigraph.add_edge("n2", "n1", id="e2") + fh = io.BytesIO(cls.undirected_multigraph_data.encode("UTF-8")) + cls.undirected_multigraph_fh = fh + + cls.undirected_multigraph_no_multiedge_data = """ + + + + + + + + + + +""" + cls.undirected_multigraph_no_multiedge = nx.MultiGraph() + cls.undirected_multigraph_no_multiedge.add_node("n10") + cls.undirected_multigraph_no_multiedge.add_edge("n0", "n2", id="e0") + cls.undirected_multigraph_no_multiedge.add_edge("n1", "n2", id="e1") + cls.undirected_multigraph_no_multiedge.add_edge("n2", "n3", id="e2") + fh = io.BytesIO(cls.undirected_multigraph_no_multiedge_data.encode("UTF-8")) + cls.undirected_multigraph_no_multiedge_fh = fh + + cls.multigraph_only_ids_for_multiedges_data = """ + + + + + + + + + + +""" + cls.multigraph_only_ids_for_multiedges = nx.MultiGraph() + cls.multigraph_only_ids_for_multiedges.add_node("n10") + cls.multigraph_only_ids_for_multiedges.add_edge("n0", "n2") + cls.multigraph_only_ids_for_multiedges.add_edge("n1", "n2", id="e1") + cls.multigraph_only_ids_for_multiedges.add_edge("n2", "n1", id="e2") + fh = io.BytesIO(cls.multigraph_only_ids_for_multiedges_data.encode("UTF-8")) + cls.multigraph_only_ids_for_multiedges_fh = fh + + +class TestReadGraphML(BaseGraphML): + def test_read_simple_directed_graphml(self): + G = self.simple_directed_graph + H = nx.read_graphml(self.simple_directed_fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(G.edges()) == sorted(H.edges()) + assert sorted(G.edges(data=True)) == sorted(H.edges(data=True)) + self.simple_directed_fh.seek(0) + + PG = nx.parse_graphml(self.simple_directed_data) + assert sorted(G.nodes()) == sorted(PG.nodes()) + assert sorted(G.edges()) == sorted(PG.edges()) + assert sorted(G.edges(data=True)) == sorted(PG.edges(data=True)) + + def test_read_simple_undirected_graphml(self): + G = self.simple_undirected_graph + H = nx.read_graphml(self.simple_undirected_fh) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + self.simple_undirected_fh.seek(0) + + PG = nx.parse_graphml(self.simple_undirected_data) + assert nodes_equal(G.nodes(), PG.nodes()) + assert edges_equal(G.edges(), PG.edges()) + + def test_read_undirected_multigraph_graphml(self): + G = self.undirected_multigraph + H = nx.read_graphml(self.undirected_multigraph_fh) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + self.undirected_multigraph_fh.seek(0) + + PG = nx.parse_graphml(self.undirected_multigraph_data) + assert nodes_equal(G.nodes(), PG.nodes()) + assert edges_equal(G.edges(), PG.edges()) + + def test_read_undirected_multigraph_no_multiedge_graphml(self): + G = self.undirected_multigraph_no_multiedge + H = nx.read_graphml(self.undirected_multigraph_no_multiedge_fh) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + self.undirected_multigraph_no_multiedge_fh.seek(0) + + PG = nx.parse_graphml(self.undirected_multigraph_no_multiedge_data) + assert nodes_equal(G.nodes(), PG.nodes()) + assert edges_equal(G.edges(), PG.edges()) + + def test_read_undirected_multigraph_only_ids_for_multiedges_graphml(self): + G = self.multigraph_only_ids_for_multiedges + H = nx.read_graphml(self.multigraph_only_ids_for_multiedges_fh) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + self.multigraph_only_ids_for_multiedges_fh.seek(0) + + PG = nx.parse_graphml(self.multigraph_only_ids_for_multiedges_data) + assert nodes_equal(G.nodes(), PG.nodes()) + assert edges_equal(G.edges(), PG.edges()) + + def test_read_attribute_graphml(self): + G = self.attribute_graph + H = nx.read_graphml(self.attribute_fh) + assert nodes_equal(G.nodes(True), sorted(H.nodes(data=True))) + ge = sorted(G.edges(data=True)) + he = sorted(H.edges(data=True)) + for a, b in zip(ge, he): + assert a == b + self.attribute_fh.seek(0) + + PG = nx.parse_graphml(self.attribute_data) + assert sorted(G.nodes(True)) == sorted(PG.nodes(data=True)) + ge = sorted(G.edges(data=True)) + he = sorted(PG.edges(data=True)) + for a, b in zip(ge, he): + assert a == b + + def test_node_default_attribute_graphml(self): + G = self.node_attribute_default_graph + H = nx.read_graphml(self.node_attribute_default_fh) + assert G.graph["node_default"] == H.graph["node_default"] + + def test_directed_edge_in_undirected(self): + s = """ + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_graphml, fh) + pytest.raises(nx.NetworkXError, nx.parse_graphml, s) + + def test_undirected_edge_in_directed(self): + s = """ + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_graphml, fh) + pytest.raises(nx.NetworkXError, nx.parse_graphml, s) + + def test_key_raise(self): + s = """ + + + yellow + + + + + green + + + + blue + + + 1.0 + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_graphml, fh) + pytest.raises(nx.NetworkXError, nx.parse_graphml, s) + + def test_hyperedge_raise(self): + s = """ + + + yellow + + + + + green + + + + blue + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_graphml, fh) + pytest.raises(nx.NetworkXError, nx.parse_graphml, s) + + def test_multigraph_keys(self): + # Test that reading multigraphs uses edge id attributes as keys + s = """ + + + + + + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_graphml(fh) + expected = [("n0", "n1", "e0"), ("n0", "n1", "e1")] + assert sorted(G.edges(keys=True)) == expected + fh.seek(0) + H = nx.parse_graphml(s) + assert sorted(H.edges(keys=True)) == expected + + def test_preserve_multi_edge_data(self): + """ + Test that data and keys of edges are preserved on consequent + write and reads + """ + G = nx.MultiGraph() + G.add_node(1) + G.add_node(2) + G.add_edges_from( + [ + # edges with no data, no keys: + (1, 2), + # edges with only data: + (1, 2, {"key": "data_key1"}), + (1, 2, {"id": "data_id2"}), + (1, 2, {"key": "data_key3", "id": "data_id3"}), + # edges with both data and keys: + (1, 2, 103, {"key": "data_key4"}), + (1, 2, 104, {"id": "data_id5"}), + (1, 2, 105, {"key": "data_key6", "id": "data_id7"}), + ] + ) + fh = io.BytesIO() + nx.write_graphml(G, fh) + fh.seek(0) + H = nx.read_graphml(fh, node_type=int) + assert edges_equal(G.edges(data=True, keys=True), H.edges(data=True, keys=True)) + assert G._adj == H._adj + + Gadj = { + str(node): { + str(nbr): {str(ekey): dd for ekey, dd in key_dict.items()} + for nbr, key_dict in nbr_dict.items() + } + for node, nbr_dict in G._adj.items() + } + fh.seek(0) + HH = nx.read_graphml(fh, node_type=str, edge_key_type=str) + assert Gadj == HH._adj + + fh.seek(0) + string_fh = fh.read() + HH = nx.parse_graphml(string_fh, node_type=str, edge_key_type=str) + assert Gadj == HH._adj + + def test_yfiles_extension(self): + data = """ + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + 2 + + + + + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + +""" + fh = io.BytesIO(data.encode("UTF-8")) + G = nx.read_graphml(fh, force_multigraph=True) + assert list(G.edges()) == [("n0", "n1")] + assert G.has_edge("n0", "n1", key="e0") + assert G.nodes["n0"]["label"] == "1" + assert G.nodes["n1"]["label"] == "2" + assert G.nodes["n2"]["label"] == "3" + assert G.nodes["n0"]["shape_type"] == "rectangle" + assert G.nodes["n1"]["shape_type"] == "rectangle" + assert G.nodes["n2"]["shape_type"] == "com.yworks.flowchart.terminator" + assert G.nodes["n2"]["description"] == "description\nline1\nline2" + fh.seek(0) + G = nx.read_graphml(fh) + assert list(G.edges()) == [("n0", "n1")] + assert G["n0"]["n1"]["id"] == "e0" + assert G.nodes["n0"]["label"] == "1" + assert G.nodes["n1"]["label"] == "2" + assert G.nodes["n2"]["label"] == "3" + assert G.nodes["n0"]["shape_type"] == "rectangle" + assert G.nodes["n1"]["shape_type"] == "rectangle" + assert G.nodes["n2"]["shape_type"] == "com.yworks.flowchart.terminator" + assert G.nodes["n2"]["description"] == "description\nline1\nline2" + + H = nx.parse_graphml(data, force_multigraph=True) + assert list(H.edges()) == [("n0", "n1")] + assert H.has_edge("n0", "n1", key="e0") + assert H.nodes["n0"]["label"] == "1" + assert H.nodes["n1"]["label"] == "2" + assert H.nodes["n2"]["label"] == "3" + + H = nx.parse_graphml(data) + assert list(H.edges()) == [("n0", "n1")] + assert H["n0"]["n1"]["id"] == "e0" + assert H.nodes["n0"]["label"] == "1" + assert H.nodes["n1"]["label"] == "2" + assert H.nodes["n2"]["label"] == "3" + + def test_bool(self): + s = """ + + + false + + + + true + + + + false + + + FaLsE + + + True + + + 0 + + + 1 + + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_graphml(fh) + H = nx.parse_graphml(s) + for graph in [G, H]: + assert graph.nodes["n0"]["test"] + assert not graph.nodes["n2"]["test"] + assert not graph.nodes["n3"]["test"] + assert graph.nodes["n4"]["test"] + assert not graph.nodes["n5"]["test"] + assert graph.nodes["n6"]["test"] + + def test_graphml_header_line(self): + good = """ + + + false + + + + true + + + +""" + bad = """ + + + false + + + + true + + + +""" + ugly = """ + + + false + + + + true + + + +""" + for s in (good, bad): + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_graphml(fh) + H = nx.parse_graphml(s) + for graph in [G, H]: + assert graph.nodes["n0"]["test"] + + fh = io.BytesIO(ugly.encode("UTF-8")) + pytest.raises(nx.NetworkXError, nx.read_graphml, fh) + pytest.raises(nx.NetworkXError, nx.parse_graphml, ugly) + + def test_read_attributes_with_groups(self): + data = """\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + + + + + + + + + Group 3 + + + + + + + + + + Folder 3 + + + + + + + + + + + + + + + + + + + + + Group 1 + + + + + + + + + + Folder 1 + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + Group 2 + + + + + + + + + + Folder 2 + + + + + + + + + + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + + 6 + + + + + + + + + + + + + + + + + + + + + + + 9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + # verify that nodes / attributes are correctly read when part of a group + fh = io.BytesIO(data.encode("UTF-8")) + G = nx.read_graphml(fh) + data = [x for _, x in G.nodes(data=True)] + assert len(data) == 9 + for node_data in data: + assert node_data["CustomProperty"] != "" + + def test_long_attribute_type(self): + # test that graphs with attr.type="long" (as produced by botch and + # dose3) can be parsed + s = """ + + + + + 4284 + + +""" + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_graphml(fh) + expected = [("n1", {"cudfversion": 4284})] + assert sorted(G.nodes(data=True)) == expected + fh.seek(0) + H = nx.parse_graphml(s) + assert sorted(H.nodes(data=True)) == expected + + +class TestWriteGraphML(BaseGraphML): + writer = staticmethod(nx.write_graphml_lxml) + + @classmethod + def setup_class(cls): + BaseGraphML.setup_class() + _ = pytest.importorskip("lxml.etree") + + def test_write_interface(self): + try: + import lxml.etree + + assert nx.write_graphml == nx.write_graphml_lxml + except ImportError: + assert nx.write_graphml == nx.write_graphml_xml + + def test_write_read_simple_directed_graphml(self): + G = self.simple_directed_graph + G.graph["hi"] = "there" + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(G.edges()) == sorted(H.edges()) + assert sorted(G.edges(data=True)) == sorted(H.edges(data=True)) + self.simple_directed_fh.seek(0) + + def test_GraphMLWriter_add_graphs(self): + gmlw = GraphMLWriter() + G = self.simple_directed_graph + H = G.copy() + gmlw.add_graphs([G, H]) + + def test_write_read_simple_no_prettyprint(self): + G = self.simple_directed_graph + G.graph["hi"] = "there" + G.graph["id"] = "1" + fh = io.BytesIO() + self.writer(G, fh, prettyprint=False) + fh.seek(0) + H = nx.read_graphml(fh) + assert sorted(G.nodes()) == sorted(H.nodes()) + assert sorted(G.edges()) == sorted(H.edges()) + assert sorted(G.edges(data=True)) == sorted(H.edges(data=True)) + self.simple_directed_fh.seek(0) + + def test_write_read_attribute_named_key_ids_graphml(self): + from xml.etree.ElementTree import parse + + G = self.attribute_named_key_ids_graph + fh = io.BytesIO() + self.writer(G, fh, named_key_ids=True) + fh.seek(0) + H = nx.read_graphml(fh) + fh.seek(0) + + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges(), directed=True) + assert edges_equal(G.edges(data=True), H.edges(data=True), directed=True) + self.attribute_named_key_ids_fh.seek(0) + + xml = parse(fh) + # Children are the key elements, and the graph element + children = list(xml.getroot()) + assert len(children) == 4 + + keys = [child.items() for child in children[:3]] + + assert len(keys) == 3 + assert ("id", "edge_prop") in keys[0] + assert ("attr.name", "edge_prop") in keys[0] + assert ("id", "prop2") in keys[1] + assert ("attr.name", "prop2") in keys[1] + assert ("id", "prop1") in keys[2] + assert ("attr.name", "prop1") in keys[2] + + # Confirm the read graph nodes/edge are identical when compared to + # default writing behavior. + default_behavior_fh = io.BytesIO() + nx.write_graphml(G, default_behavior_fh) + default_behavior_fh.seek(0) + H = nx.read_graphml(default_behavior_fh) + + named_key_ids_behavior_fh = io.BytesIO() + nx.write_graphml(G, named_key_ids_behavior_fh, named_key_ids=True) + named_key_ids_behavior_fh.seek(0) + J = nx.read_graphml(named_key_ids_behavior_fh) + + assert all(n1 == n2 for (n1, n2) in zip(H.nodes, J.nodes)) + assert all(e1 == e2 for (e1, e2) in zip(H.edges, J.edges)) + + def test_write_read_attribute_numeric_type_graphml(self): + from xml.etree.ElementTree import parse + + G = self.attribute_numeric_type_graph + fh = io.BytesIO() + self.writer(G, fh, infer_numeric_types=True) + fh.seek(0) + H = nx.read_graphml(fh) + fh.seek(0) + + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges(), directed=True) + assert edges_equal(G.edges(data=True), H.edges(data=True), directed=True) + self.attribute_numeric_type_fh.seek(0) + + xml = parse(fh) + # Children are the key elements, and the graph element + children = list(xml.getroot()) + assert len(children) == 3 + + keys = [child.items() for child in children[:2]] + + assert len(keys) == 2 + assert ("attr.type", "double") in keys[0] + assert ("attr.type", "double") in keys[1] + + def test_more_multigraph_keys(self, tmp_path): + """Writing keys as edge id attributes means keys become strings. + The original keys are stored as data, so read them back in + if `str(key) == edge_id` + This allows the adjacency to remain the same. + """ + G = nx.MultiGraph() + G.add_edges_from([("a", "b", 2), ("a", "b", 3)]) + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname) + assert H.is_multigraph() + assert edges_equal(G.edges(keys=True), H.edges(keys=True)) + assert G._adj == H._adj + + def test_default_attribute(self): + G = nx.Graph(name="Fred") + G.add_node(1, label=1, color="green") + nx.add_path(G, [0, 1, 2, 3]) + G.add_edge(1, 2, weight=3) + G.graph["node_default"] = {"color": "yellow"} + G.graph["edge_default"] = {"weight": 7} + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh, node_type=int) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + assert G.graph == H.graph + + def test_mixed_type_attributes(self): + G = nx.MultiGraph() + G.add_node("n0", special=False) + G.add_node("n1", special=0) + G.add_edge("n0", "n1", special=False) + G.add_edge("n0", "n1", special=0) + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh) + assert not H.nodes["n0"]["special"] + assert H.nodes["n1"]["special"] == 0 + assert not H.edges["n0", "n1", 0]["special"] + assert H.edges["n0", "n1", 1]["special"] == 0 + + def test_str_number_mixed_type_attributes(self): + G = nx.MultiGraph() + G.add_node("n0", special="hello") + G.add_node("n1", special=0) + G.add_edge("n0", "n1", special="hello") + G.add_edge("n0", "n1", special=0) + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh) + assert H.nodes["n0"]["special"] == "hello" + assert H.nodes["n1"]["special"] == 0 + assert H.edges["n0", "n1", 0]["special"] == "hello" + assert H.edges["n0", "n1", 1]["special"] == 0 + + def test_mixed_int_type_number_attributes(self): + np = pytest.importorskip("numpy") + G = nx.MultiGraph() + G.add_node("n0", special=np.int64(0)) + G.add_node("n1", special=1) + G.add_edge("n0", "n1", special=np.int64(2)) + G.add_edge("n0", "n1", special=3) + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh) + assert H.nodes["n0"]["special"] == 0 + assert H.nodes["n1"]["special"] == 1 + assert H.edges["n0", "n1", 0]["special"] == 2 + assert H.edges["n0", "n1", 1]["special"] == 3 + + def test_multigraph_to_graph(self, tmp_path): + # test converting multigraph to graph if no parallel edges found + G = nx.MultiGraph() + G.add_edges_from([("a", "b", 2), ("b", "c", 3)]) # no multiedges + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname) + assert not H.is_multigraph() + H = nx.read_graphml(fname, force_multigraph=True) + assert H.is_multigraph() + + # add a multiedge + G.add_edge("a", "b", "e-id") + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname) + assert H.is_multigraph() + H = nx.read_graphml(fname, force_multigraph=True) + assert H.is_multigraph() + + def test_write_generate_edge_id_from_attribute(self, tmp_path): + from xml.etree.ElementTree import parse + + G = nx.Graph() + G.add_edges_from([("a", "b"), ("b", "c"), ("a", "c")]) + edge_attributes = {e: str(e) for e in G.edges} + nx.set_edge_attributes(G, edge_attributes, "eid") + fname = tmp_path / "test.graphml" + # set edge_id_from_attribute e.g. "eid" for write_graphml() + self.writer(G, fname, edge_id_from_attribute="eid") + # set edge_id_from_attribute e.g. "eid" for generate_graphml() + generator = nx.generate_graphml(G, edge_id_from_attribute="eid") + + H = nx.read_graphml(fname) + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + # NetworkX adds explicit edge "id" from file as attribute + nx.set_edge_attributes(G, edge_attributes, "id") + assert edges_equal(G.edges(data=True), H.edges(data=True)) + + tree = parse(fname) + children = list(tree.getroot()) + assert len(children) == 2 + edge_ids = [ + edge.attrib["id"] + for edge in tree.getroot().findall( + ".//{http://graphml.graphdrawing.org/xmlns}edge" + ) + ] + # verify edge id value is equal to specified attribute value + assert sorted(edge_ids) == sorted(edge_attributes.values()) + + # check graphml generated from generate_graphml() + data = "".join(generator) + J = nx.parse_graphml(data) + assert sorted(G.nodes()) == sorted(J.nodes()) + assert sorted(G.edges()) == sorted(J.edges()) + # NetworkX adds explicit edge "id" from file as attribute + nx.set_edge_attributes(G, edge_attributes, "id") + assert edges_equal(G.edges(data=True), J.edges(data=True)) + + def test_multigraph_write_generate_edge_id_from_attribute(self, tmp_path): + from xml.etree.ElementTree import parse + + G = nx.MultiGraph() + G.add_edges_from([("a", "b"), ("b", "c"), ("a", "c"), ("a", "b")]) + edge_attributes = {e: str(e) for e in G.edges} + nx.set_edge_attributes(G, edge_attributes, "eid") + fname = tmp_path / "test.graphml" + # set edge_id_from_attribute e.g. "eid" for write_graphml() + self.writer(G, fname, edge_id_from_attribute="eid") + # set edge_id_from_attribute e.g. "eid" for generate_graphml() + generator = nx.generate_graphml(G, edge_id_from_attribute="eid") + + H = nx.read_graphml(fname) + assert H.is_multigraph() + H = nx.read_graphml(fname, force_multigraph=True) + assert H.is_multigraph() + + assert nodes_equal(G.nodes(), H.nodes()) + assert edges_equal(G.edges(), H.edges()) + assert sorted(data.get("eid") for u, v, data in H.edges(data=True)) == sorted( + edge_attributes.values() + ) + # NetworkX uses edge_ids as keys in multigraphs if no key + assert sorted(key for u, v, key in H.edges(keys=True)) == sorted( + edge_attributes.values() + ) + + tree = parse(fname) + children = list(tree.getroot()) + assert len(children) == 2 + edge_ids = [ + edge.attrib["id"] + for edge in tree.getroot().findall( + ".//{http://graphml.graphdrawing.org/xmlns}edge" + ) + ] + # verify edge id value is equal to specified attribute value + assert sorted(edge_ids) == sorted(edge_attributes.values()) + + # check graphml generated from generate_graphml() + graphml_data = "".join(generator) + J = nx.parse_graphml(graphml_data) + assert J.is_multigraph() + + assert nodes_equal(G.nodes(), J.nodes()) + assert edges_equal(G.edges(), J.edges()) + assert sorted(data.get("eid") for u, v, data in J.edges(data=True)) == sorted( + edge_attributes.values() + ) + # NetworkX uses edge_ids as keys in multigraphs if no key + assert sorted(key for u, v, key in J.edges(keys=True)) == sorted( + edge_attributes.values() + ) + + def test_numpy_float64(self, tmp_path): + np = pytest.importorskip("numpy") + wt = np.float64(3.4) + G = nx.Graph([(1, 2, {"weight": wt})]) + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname, node_type=int) + assert G.edges == H.edges + wtG = G[1][2]["weight"] + wtH = H[1][2]["weight"] + assert wtG == pytest.approx(wtH, abs=1e-6) + assert type(wtG) is np.float64 + assert type(wtH) is float + + def test_numpy_float32(self, tmp_path): + np = pytest.importorskip("numpy") + wt = np.float32(3.4) + G = nx.Graph([(1, 2, {"weight": wt})]) + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname, node_type=int) + assert G.edges == H.edges + wtG = G[1][2]["weight"] + wtH = H[1][2]["weight"] + assert wtG == pytest.approx(wtH, abs=1e-6) + assert type(wtG) is np.float32 + assert type(wtH) is float + + def test_numpy_float64_inference(self, tmp_path): + np = pytest.importorskip("numpy") + G = self.attribute_numeric_type_graph + G.edges[("n1", "n1")]["weight"] = np.float64(1.1) + fname = tmp_path / "test.graphml" + self.writer(G, fname, infer_numeric_types=True) + H = nx.read_graphml(fname) + assert G._adj == H._adj + + def test_unicode_attributes(self, tmp_path): + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + node_type = str + G.add_edge(name1, "Radiohead", foo=name2) + fname = tmp_path / "test.graphml" + self.writer(G, fname) + H = nx.read_graphml(fname, node_type=node_type) + assert G._adj == H._adj + + def test_unicode_escape(self): + # test for handling json escaped strings in python 2 Issue #1880 + import json + + a = {"a": '{"a": "123"}'} # an object with many chars to escape + sa = json.dumps(a) + G = nx.Graph() + G.graph["test"] = sa + fh = io.BytesIO() + self.writer(G, fh) + fh.seek(0) + H = nx.read_graphml(fh) + assert G.graph["test"] == H.graph["test"] + + +class TestXMLGraphML(TestWriteGraphML): + writer = staticmethod(nx.write_graphml_xml) + + @classmethod + def setup_class(cls): + TestWriteGraphML.setup_class() + + +def test_exception_for_unsupported_datatype_node_attr(): + """Test that a detailed exception is raised when an attribute is of a type + not supported by GraphML, e.g. a list""" + pytest.importorskip("lxml.etree") + # node attribute + G = nx.Graph() + G.add_node(0, my_list_attribute=[0, 1, 2]) + fh = io.BytesIO() + with pytest.raises(TypeError, match="GraphML does not support"): + nx.write_graphml(G, fh) + + +def test_exception_for_unsupported_datatype_edge_attr(): + """Test that a detailed exception is raised when an attribute is of a type + not supported by GraphML, e.g. a list""" + pytest.importorskip("lxml.etree") + # edge attribute + G = nx.Graph() + G.add_edge(0, 1, my_list_attribute=[0, 1, 2]) + fh = io.BytesIO() + with pytest.raises(TypeError, match="GraphML does not support"): + nx.write_graphml(G, fh) + + +def test_exception_for_unsupported_datatype_graph_attr(): + """Test that a detailed exception is raised when an attribute is of a type + not supported by GraphML, e.g. a list""" + pytest.importorskip("lxml.etree") + # graph attribute + G = nx.Graph() + G.graph["my_list_attribute"] = [0, 1, 2] + fh = io.BytesIO() + with pytest.raises(TypeError, match="GraphML does not support"): + nx.write_graphml(G, fh) + + +def test_empty_attribute(): + """Tests that a GraphML string with an empty attribute can be parsed + correctly.""" + s = """ + + + + + + aaa + bbb + + + ccc + + + + """ + fh = io.BytesIO(s.encode("UTF-8")) + G = nx.read_graphml(fh) + assert G.nodes["0"] == {"foo": "aaa", "bar": "bbb"} + assert G.nodes["1"] == {"foo": "ccc", "bar": ""} diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py new file mode 100644 index 0000000000000000000000000000000000000000..8ac5ecc34bf9b42bd49e316bdc72e0e56c76a616 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_leda.py @@ -0,0 +1,30 @@ +import io + +import networkx as nx + + +class TestLEDA: + def test_parse_leda(self): + data = """#header section \nLEDA.GRAPH \nstring\nint\n-1\n#nodes section\n5 \n|{v1}| \n|{v2}| \n|{v3}| \n|{v4}| \n|{v5}| \n\n#edges section\n7 \n1 2 0 |{4}| \n1 3 0 |{3}| \n2 3 0 |{2}| \n3 4 0 |{3}| \n3 5 0 |{7}| \n4 5 0 |{6}| \n5 1 0 |{foo}|""" + G = nx.parse_leda(data) + G = nx.parse_leda(data.split("\n")) + assert sorted(G.nodes()) == ["v1", "v2", "v3", "v4", "v5"] + assert sorted(G.edges(data=True)) == [ + ("v1", "v2", {"label": "4"}), + ("v1", "v3", {"label": "3"}), + ("v2", "v3", {"label": "2"}), + ("v3", "v4", {"label": "3"}), + ("v3", "v5", {"label": "7"}), + ("v4", "v5", {"label": "6"}), + ("v5", "v1", {"label": "foo"}), + ] + + def test_read_LEDA(self): + fh = io.BytesIO() + data = """#header section \nLEDA.GRAPH \nstring\nint\n-1\n#nodes section\n5 \n|{v1}| \n|{v2}| \n|{v3}| \n|{v4}| \n|{v5}| \n\n#edges section\n7 \n1 2 0 |{4}| \n1 3 0 |{3}| \n2 3 0 |{2}| \n3 4 0 |{3}| \n3 5 0 |{7}| \n4 5 0 |{6}| \n5 1 0 |{foo}|""" + G = nx.parse_leda(data) + fh.write(data.encode("UTF-8")) + fh.seek(0) + Gin = nx.read_leda(fh) + assert sorted(G.nodes()) == sorted(Gin.nodes()) + assert sorted(G.edges()) == sorted(Gin.edges()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py new file mode 100644 index 0000000000000000000000000000000000000000..c6e36bfbca87db83a3732d737355f051e3821a1e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_p2g.py @@ -0,0 +1,63 @@ +import io + +import networkx as nx +from networkx.readwrite.p2g import read_p2g, write_p2g +from networkx.utils import edges_equal + + +class TestP2G: + @classmethod + def setup_class(cls): + cls.G = nx.Graph(name="test") + e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")] + cls.G.add_edges_from(e) + cls.G.add_node("g") + cls.DG = nx.DiGraph(cls.G) + + def test_read_p2g(self): + s = b"""\ +name +3 4 +a +1 2 +b + +c +0 2 +""" + bytesIO = io.BytesIO(s) + DG = read_p2g(bytesIO) + assert DG.name == "name" + assert sorted(DG) == ["a", "b", "c"] + assert edges_equal( + DG.edges(), [("a", "c"), ("a", "b"), ("c", "a"), ("c", "c")], directed=True + ) + + def test_write_p2g(self): + s = b"""foo +3 2 +1 +1 +2 +2 +3 + +""" + fh = io.BytesIO() + G = nx.DiGraph() + G.name = "foo" + G.add_edges_from([(1, 2), (2, 3)]) + write_p2g(G, fh) + fh.seek(0) + r = fh.read() + assert r == s + + def test_write_read_p2g(self): + fh = io.BytesIO() + G = nx.DiGraph() + G.name = "foo" + G.add_edges_from([("a", "b"), ("b", "c")]) + write_p2g(G, fh) + fh.seek(0) + H = read_p2g(fh) + assert edges_equal(G.edges(), H.edges(), directed=True) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py new file mode 100644 index 0000000000000000000000000000000000000000..9cce4d5a8c0fa120fe1239851046c41eb7eb7014 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_pajek.py @@ -0,0 +1,128 @@ +""" +Pajek tests +""" + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + + +class TestPajek: + @classmethod + def setup_class(cls): + cls.data = """*network Tralala\n*vertices 4\n 1 "A1" 0.0938 0.0896 ellipse x_fact 1 y_fact 1\n 2 "Bb" 0.8188 0.2458 ellipse x_fact 1 y_fact 1\n 3 "C" 0.3688 0.7792 ellipse x_fact 1\n 4 "D2" 0.9583 0.8563 ellipse x_fact 1\n*arcs\n1 1 1 h2 0 w 3 c Blue s 3 a1 -130 k1 0.6 a2 -130 k2 0.6 ap 0.5 l "Bezier loop" lc BlueViolet fos 20 lr 58 lp 0.3 la 360\n2 1 1 h2 0 a1 120 k1 1.3 a2 -120 k2 0.3 ap 25 l "Bezier arc" lphi 270 la 180 lr 19 lp 0.5\n1 2 1 h2 0 a1 40 k1 2.8 a2 30 k2 0.8 ap 25 l "Bezier arc" lphi 90 la 0 lp 0.65\n4 2 -1 h2 0 w 1 k1 -2 k2 250 ap 25 l "Circular arc" c Red lc OrangeRed\n3 4 1 p Dashed h2 0 w 2 c OliveGreen ap 25 l "Straight arc" lc PineGreen\n1 3 1 p Dashed h2 0 w 5 k1 -1 k2 -20 ap 25 l "Oval arc" c Brown lc Black\n3 3 -1 h1 6 w 1 h2 12 k1 -2 k2 -15 ap 0.5 l "Circular loop" c Red lc OrangeRed lphi 270 la 180""" + cls.G = nx.MultiDiGraph() + cls.G.add_nodes_from(["A1", "Bb", "C", "D2"]) + cls.G.add_edges_from( + [ + ("A1", "A1"), + ("A1", "Bb"), + ("A1", "C"), + ("Bb", "A1"), + ("C", "C"), + ("C", "D2"), + ("D2", "Bb"), + ] + ) + + cls.G.graph["name"] = "Tralala" + + def test_parse_pajek_simple(self): + # Example without node positions or shape + data = """*Vertices 2\n1 "1"\n2 "2"\n*Edges\n1 2\n2 1""" + G = nx.parse_pajek(data) + assert sorted(G.nodes()) == ["1", "2"] + assert edges_equal(G.edges(), [("1", "2"), ("1", "2")]) + + def test_parse_pajek(self): + G = nx.parse_pajek(self.data) + assert sorted(G.nodes()) == ["A1", "Bb", "C", "D2"] + assert edges_equal( + G.edges(), + [ + ("A1", "A1"), + ("A1", "Bb"), + ("A1", "C"), + ("Bb", "A1"), + ("C", "C"), + ("C", "D2"), + ("D2", "Bb"), + ], + directed=True, + ) + + def test_parse_pajek_mat(self): + data = """*Vertices 3\n1 "one"\n2 "two"\n3 "three"\n*Matrix\n1 1 0\n0 1 0\n0 1 0\n""" + G = nx.parse_pajek(data) + assert set(G.nodes()) == {"one", "two", "three"} + assert G.nodes["two"] == {"id": "2"} + assert edges_equal( + G.edges(), + [("one", "one"), ("one", "two"), ("two", "two"), ("three", "two")], + directed=True, + ) + + def test_read_pajek(self, tmp_path): + G = nx.parse_pajek(self.data) + # Read data from file + fname = tmp_path / "test.pjk" + with open(fname, "wb") as fh: + fh.write(self.data.encode("UTF-8")) + + Gin = nx.read_pajek(fname) + assert sorted(G.nodes()) == sorted(Gin.nodes()) + assert edges_equal(G.edges(), Gin.edges(), directed=True) + assert self.G.graph == Gin.graph + for n in G: + assert G.nodes[n] == Gin.nodes[n] + + def test_write_pajek(self): + import io + + G = nx.parse_pajek(self.data) + fh = io.BytesIO() + nx.write_pajek(G, fh) + fh.seek(0) + H = nx.read_pajek(fh) + assert nodes_equal(list(G), list(H)) + assert edges_equal(G.edges(), H.edges(), directed=True) + # Graph name is left out for now, therefore it is not tested. + # assert_equal(G.graph, H.graph) + + def test_ignored_attribute(self): + import io + + G = nx.Graph() + fh = io.BytesIO() + G.add_node(1, int_attr=1) + G.add_node(2, empty_attr=" ") + G.add_edge(1, 2, int_attr=2) + G.add_edge(2, 3, empty_attr=" ") + + import warnings + + with warnings.catch_warnings(record=True) as w: + nx.write_pajek(G, fh) + assert len(w) == 4 + + def test_noname(self): + # Make sure we can parse a line such as: *network + # Issue #952 + line = "*network\n" + other_lines = self.data.split("\n")[1:] + data = line + "\n".join(other_lines) + G = nx.parse_pajek(data) + + def test_unicode(self): + import io + + G = nx.Graph() + name1 = chr(2344) + chr(123) + chr(6543) + name2 = chr(5543) + chr(1543) + chr(324) + G.add_edge(name1, "Radiohead", foo=name2) + fh = io.BytesIO() + nx.write_pajek(G, fh) + fh.seek(0) + H = nx.read_pajek(fh) + assert nodes_equal(list(G), list(H)) + assert edges_equal(list(G.edges()), list(H.edges())) + assert G.graph == H.graph diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py new file mode 100644 index 0000000000000000000000000000000000000000..52cd271d060bd00a4c70e0e21fbdb33990078951 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_sparse6.py @@ -0,0 +1,166 @@ +from io import BytesIO + +import pytest + +import networkx as nx +from networkx.utils import edges_equal, nodes_equal + + +class TestSparseGraph6: + def test_from_sparse6_bytes(self): + data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM" + G = nx.from_sparse6_bytes(data) + assert nodes_equal( + sorted(G.nodes()), + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + ) + assert edges_equal( + G.edges(), + [ + (0, 1), + (0, 2), + (0, 3), + (1, 12), + (1, 14), + (2, 13), + (2, 15), + (3, 16), + (3, 17), + (4, 7), + (4, 9), + (4, 11), + (5, 6), + (5, 8), + (5, 9), + (6, 10), + (6, 11), + (7, 8), + (7, 10), + (8, 12), + (9, 15), + (10, 14), + (11, 13), + (12, 16), + (13, 17), + (14, 17), + (15, 16), + ], + ) + + def test_from_bytes_multigraph_graph(self): + graph_data = b":An" + G = nx.from_sparse6_bytes(graph_data) + assert isinstance(G, nx.Graph) + multigraph_data = b":Ab" + M = nx.from_sparse6_bytes(multigraph_data) + assert isinstance(M, nx.MultiGraph) + + def test_read_sparse6(self): + data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM" + G = nx.from_sparse6_bytes(data) + fh = BytesIO(data) + Gin = nx.read_sparse6(fh) + assert nodes_equal(G.nodes(), Gin.nodes()) + assert edges_equal(G.edges(), Gin.edges()) + + def test_read_many_graph6(self): + # Read many graphs into list + data = b":Q___eDcdFcDeFcE`GaJ`IaHbKNbLM\n:Q___dCfDEdcEgcbEGbFIaJ`JaHN`IM" + fh = BytesIO(data) + glist = nx.read_sparse6(fh) + assert len(glist) == 2 + for G in glist: + assert nodes_equal( + G.nodes(), + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], + ) + + +class TestWriteSparse6: + """Unit tests for writing graphs in the sparse6 format. + + Most of the test cases were checked against the sparse6 encoder in Sage. + + """ + + def test_null_graph(self): + G = nx.null_graph() + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:?\n" + + def test_trivial_graph(self): + G = nx.trivial_graph() + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:@\n" + + def test_empty_graph(self): + G = nx.empty_graph(5) + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:D\n" + + def test_large_empty_graph(self): + G = nx.empty_graph(68) + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:~?@C\n" + + def test_very_large_empty_graph(self): + G = nx.empty_graph(258049) + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:~~???~?@\n" + + def test_complete_graph(self): + G = nx.complete_graph(4) + result = BytesIO() + nx.write_sparse6(G, result) + assert result.getvalue() == b">>sparse6<<:CcKI\n" + + def test_no_header(self): + G = nx.complete_graph(4) + result = BytesIO() + nx.write_sparse6(G, result, header=False) + assert result.getvalue() == b":CcKI\n" + + def test_padding(self): + codes = (b":Cdv", b":DaYn", b":EaYnN", b":FaYnL", b":GaYnLz") + for n, code in enumerate(codes, start=4): + G = nx.path_graph(n) + result = BytesIO() + nx.write_sparse6(G, result, header=False) + assert result.getvalue() == code + b"\n" + + def test_complete_bipartite(self): + G = nx.complete_bipartite_graph(6, 9) + result = BytesIO() + nx.write_sparse6(G, result) + # Compared with sage + expected = b">>sparse6<<:Nk" + b"?G`cJ" * 9 + b"\n" + assert result.getvalue() == expected + + def test_read_write_inverse(self): + for i in list(range(13)) + [31, 47, 62, 63, 64, 72]: + m = min(2 * i, i * i // 2) + g = nx.random_graphs.gnm_random_graph(i, m, seed=i) + gstr = BytesIO() + nx.write_sparse6(g, gstr, header=False) + # Strip the trailing newline. + gstr = gstr.getvalue().rstrip() + g2 = nx.from_sparse6_bytes(gstr) + assert g2.order() == g.order() + assert edges_equal(g2.edges(), g.edges()) + + def test_no_directed_graphs(self): + with pytest.raises(nx.NetworkXNotImplemented): + nx.write_sparse6(nx.DiGraph(), BytesIO()) + + def test_write_path(self, tmp_path): + # Get a valid temporary file name + fullfilename = str(tmp_path / "test.s6") + # file should be closed now, so write_sparse6 can open it + nx.write_sparse6(nx.null_graph(), fullfilename) + with open(fullfilename, mode="rb") as fh: + assert fh.read() == b">>sparse6<<:?\n" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py new file mode 100644 index 0000000000000000000000000000000000000000..b2b744828c916a37784059c869cc990a2473305a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/tests/test_text.py @@ -0,0 +1,1742 @@ +import random +from itertools import product +from textwrap import dedent + +import pytest + +import networkx as nx + + +def test_generate_network_text_forest_directed(): + # Create a directed forest with labels + graph = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph) + for node in graph.nodes: + graph.nodes[node]["label"] = "node_" + chr(ord("a") + node) + + node_target = dedent( + """ + ╙── 0 + ├─╼ 1 + │ ├─╼ 3 + │ └─╼ 4 + └─╼ 2 + ├─╼ 5 + └─╼ 6 + """ + ).strip() + + label_target = dedent( + """ + ╙── node_a + ├─╼ node_b + │ ├─╼ node_d + │ └─╼ node_e + └─╼ node_c + ├─╼ node_f + └─╼ node_g + """ + ).strip() + + # Basic node case + ret = nx.generate_network_text(graph, with_labels=False) + assert "\n".join(ret) == node_target + + # Basic label case + ret = nx.generate_network_text(graph, with_labels=True) + assert "\n".join(ret) == label_target + + +def test_write_network_text_empty_graph(): + def _graph_str(g, **kw): + printbuf = [] + nx.write_network_text(g, printbuf.append, end="", **kw) + return "\n".join(printbuf) + + assert _graph_str(nx.DiGraph()) == "╙" + assert _graph_str(nx.Graph()) == "╙" + assert _graph_str(nx.DiGraph(), ascii_only=True) == "+" + assert _graph_str(nx.Graph(), ascii_only=True) == "+" + + +def test_write_network_text_within_forest_glyph(): + g = nx.DiGraph() + g.add_nodes_from([1, 2, 3, 4]) + g.add_edge(2, 4) + lines = [] + write = lines.append + nx.write_network_text(g, path=write, end="") + nx.write_network_text(g, path=write, ascii_only=True, end="") + text = "\n".join(lines) + target = dedent( + """ + ╟── 1 + ╟── 2 + ╎ └─╼ 4 + ╙── 3 + +-- 1 + +-- 2 + : L-> 4 + +-- 3 + """ + ).strip() + assert text == target + + +def test_generate_network_text_directed_multi_tree(): + tree1 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph) + tree2 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph) + forest = nx.disjoint_union_all([tree1, tree2]) + ret = "\n".join(nx.generate_network_text(forest)) + + target = dedent( + """ + ╟── 0 + ╎ ├─╼ 1 + ╎ │ ├─╼ 3 + ╎ │ └─╼ 4 + ╎ └─╼ 2 + ╎ ├─╼ 5 + ╎ └─╼ 6 + ╙── 7 + ├─╼ 8 + │ ├─╼ 10 + │ └─╼ 11 + └─╼ 9 + ├─╼ 12 + └─╼ 13 + """ + ).strip() + assert ret == target + + tree3 = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph) + forest = nx.disjoint_union_all([tree1, tree2, tree3]) + ret = "\n".join(nx.generate_network_text(forest, sources=[0, 14, 7])) + + target = dedent( + """ + ╟── 0 + ╎ ├─╼ 1 + ╎ │ ├─╼ 3 + ╎ │ └─╼ 4 + ╎ └─╼ 2 + ╎ ├─╼ 5 + ╎ └─╼ 6 + ╟── 14 + ╎ ├─╼ 15 + ╎ │ ├─╼ 17 + ╎ │ └─╼ 18 + ╎ └─╼ 16 + ╎ ├─╼ 19 + ╎ └─╼ 20 + ╙── 7 + ├─╼ 8 + │ ├─╼ 10 + │ └─╼ 11 + └─╼ 9 + ├─╼ 12 + └─╼ 13 + """ + ).strip() + assert ret == target + + ret = "\n".join( + nx.generate_network_text(forest, sources=[0, 14, 7], ascii_only=True) + ) + + target = dedent( + """ + +-- 0 + : |-> 1 + : | |-> 3 + : | L-> 4 + : L-> 2 + : |-> 5 + : L-> 6 + +-- 14 + : |-> 15 + : | |-> 17 + : | L-> 18 + : L-> 16 + : |-> 19 + : L-> 20 + +-- 7 + |-> 8 + | |-> 10 + | L-> 11 + L-> 9 + |-> 12 + L-> 13 + """ + ).strip() + assert ret == target + + +def test_generate_network_text_undirected_multi_tree(): + tree1 = nx.balanced_tree(r=2, h=2, create_using=nx.Graph) + tree2 = nx.balanced_tree(r=2, h=2, create_using=nx.Graph) + tree2 = nx.relabel_nodes(tree2, {n: n + len(tree1) for n in tree2.nodes}) + forest = nx.union(tree1, tree2) + ret = "\n".join(nx.generate_network_text(forest, sources=[0, 7])) + + target = dedent( + """ + ╟── 0 + ╎ ├── 1 + ╎ │ ├── 3 + ╎ │ └── 4 + ╎ └── 2 + ╎ ├── 5 + ╎ └── 6 + ╙── 7 + ├── 8 + │ ├── 10 + │ └── 11 + └── 9 + ├── 12 + └── 13 + """ + ).strip() + assert ret == target + + ret = "\n".join(nx.generate_network_text(forest, sources=[0, 7], ascii_only=True)) + + target = dedent( + """ + +-- 0 + : |-- 1 + : | |-- 3 + : | L-- 4 + : L-- 2 + : |-- 5 + : L-- 6 + +-- 7 + |-- 8 + | |-- 10 + | L-- 11 + L-- 9 + |-- 12 + L-- 13 + """ + ).strip() + assert ret == target + + +def test_generate_network_text_forest_undirected(): + # Create a directed forest + graph = nx.balanced_tree(r=2, h=2, create_using=nx.Graph) + + node_target0 = dedent( + """ + ╙── 0 + ├── 1 + │ ├── 3 + │ └── 4 + └── 2 + ├── 5 + └── 6 + """ + ).strip() + + # defined starting point + ret = "\n".join(nx.generate_network_text(graph, sources=[0])) + assert ret == node_target0 + + # defined starting point + node_target2 = dedent( + """ + ╙── 2 + ├── 0 + │ └── 1 + │ ├── 3 + │ └── 4 + ├── 5 + └── 6 + """ + ).strip() + ret = "\n".join(nx.generate_network_text(graph, sources=[2])) + assert ret == node_target2 + + +def test_generate_network_text_overspecified_sources(): + """ + When sources are directly specified, we won't be able to determine when we + are in the last component, so there will always be a trailing, leftmost + pipe. + """ + graph = nx.disjoint_union_all( + [ + nx.balanced_tree(r=2, h=1, create_using=nx.DiGraph), + nx.balanced_tree(r=1, h=2, create_using=nx.DiGraph), + nx.balanced_tree(r=2, h=1, create_using=nx.DiGraph), + ] + ) + + # defined starting point + target1 = dedent( + """ + ╟── 0 + ╎ ├─╼ 1 + ╎ └─╼ 2 + ╟── 3 + ╎ └─╼ 4 + ╎ └─╼ 5 + ╟── 6 + ╎ ├─╼ 7 + ╎ └─╼ 8 + """ + ).strip() + + target2 = dedent( + """ + ╟── 0 + ╎ ├─╼ 1 + ╎ └─╼ 2 + ╟── 3 + ╎ └─╼ 4 + ╎ └─╼ 5 + ╙── 6 + ├─╼ 7 + └─╼ 8 + """ + ).strip() + + got1 = "\n".join(nx.generate_network_text(graph, sources=graph.nodes)) + got2 = "\n".join(nx.generate_network_text(graph)) + assert got1 == target1 + assert got2 == target2 + + +def test_write_network_text_iterative_add_directed_edges(): + """ + Walk through the cases going from a disconnected to fully connected graph + """ + graph = nx.DiGraph() + graph.add_nodes_from([1, 2, 3, 4]) + lines = [] + write = lines.append + write("--- initial state ---") + nx.write_network_text(graph, path=write, end="") + for i, j in product(graph.nodes, graph.nodes): + write(f"--- add_edge({i}, {j}) ---") + graph.add_edge(i, j) + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + # defined starting point + target = dedent( + """ + --- initial state --- + ╟── 1 + ╟── 2 + ╟── 3 + ╙── 4 + --- add_edge(1, 1) --- + ╟── 1 ╾ 1 + ╎ └─╼ ... + ╟── 2 + ╟── 3 + ╙── 4 + --- add_edge(1, 2) --- + ╟── 1 ╾ 1 + ╎ ├─╼ 2 + ╎ └─╼ ... + ╟── 3 + ╙── 4 + --- add_edge(1, 3) --- + ╟── 1 ╾ 1 + ╎ ├─╼ 2 + ╎ ├─╼ 3 + ╎ └─╼ ... + ╙── 4 + --- add_edge(1, 4) --- + ╙── 1 ╾ 1 + ├─╼ 2 + ├─╼ 3 + ├─╼ 4 + └─╼ ... + --- add_edge(2, 1) --- + ╙── 2 ╾ 1 + └─╼ 1 ╾ 1 + ├─╼ 3 + ├─╼ 4 + └─╼ ... + --- add_edge(2, 2) --- + ╙── 1 ╾ 1, 2 + ├─╼ 2 ╾ 2 + │ └─╼ ... + ├─╼ 3 + ├─╼ 4 + └─╼ ... + --- add_edge(2, 3) --- + ╙── 1 ╾ 1, 2 + ├─╼ 2 ╾ 2 + │ ├─╼ 3 ╾ 1 + │ └─╼ ... + ├─╼ 4 + └─╼ ... + --- add_edge(2, 4) --- + ╙── 1 ╾ 1, 2 + ├─╼ 2 ╾ 2 + │ ├─╼ 3 ╾ 1 + │ ├─╼ 4 ╾ 1 + │ └─╼ ... + └─╼ ... + --- add_edge(3, 1) --- + ╙── 2 ╾ 1, 2 + ├─╼ 1 ╾ 1, 3 + │ ├─╼ 3 ╾ 2 + │ │ └─╼ ... + │ ├─╼ 4 ╾ 2 + │ └─╼ ... + └─╼ ... + --- add_edge(3, 2) --- + ╙── 3 ╾ 1, 2 + ├─╼ 1 ╾ 1, 2 + │ ├─╼ 2 ╾ 2, 3 + │ │ ├─╼ 4 ╾ 1 + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(3, 3) --- + ╙── 1 ╾ 1, 2, 3 + ├─╼ 2 ╾ 2, 3 + │ ├─╼ 3 ╾ 1, 3 + │ │ └─╼ ... + │ ├─╼ 4 ╾ 1 + │ └─╼ ... + └─╼ ... + --- add_edge(3, 4) --- + ╙── 1 ╾ 1, 2, 3 + ├─╼ 2 ╾ 2, 3 + │ ├─╼ 3 ╾ 1, 3 + │ │ ├─╼ 4 ╾ 1, 2 + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(4, 1) --- + ╙── 2 ╾ 1, 2, 3 + ├─╼ 1 ╾ 1, 3, 4 + │ ├─╼ 3 ╾ 2, 3 + │ │ ├─╼ 4 ╾ 1, 2 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(4, 2) --- + ╙── 3 ╾ 1, 2, 3 + ├─╼ 1 ╾ 1, 2, 4 + │ ├─╼ 2 ╾ 2, 3, 4 + │ │ ├─╼ 4 ╾ 1, 3 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(4, 3) --- + ╙── 4 ╾ 1, 2, 3 + ├─╼ 1 ╾ 1, 2, 3 + │ ├─╼ 2 ╾ 2, 3, 4 + │ │ ├─╼ 3 ╾ 1, 3, 4 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(4, 4) --- + ╙── 1 ╾ 1, 2, 3, 4 + ├─╼ 2 ╾ 2, 3, 4 + │ ├─╼ 3 ╾ 1, 3, 4 + │ │ ├─╼ 4 ╾ 1, 2, 4 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + """ + ).strip() + assert target == text + + +def test_write_network_text_iterative_add_undirected_edges(): + """ + Walk through the cases going from a disconnected to fully connected graph + """ + graph = nx.Graph() + graph.add_nodes_from([1, 2, 3, 4]) + lines = [] + write = lines.append + write("--- initial state ---") + nx.write_network_text(graph, path=write, end="") + for i, j in product(graph.nodes, graph.nodes): + if i == j: + continue + write(f"--- add_edge({i}, {j}) ---") + graph.add_edge(i, j) + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + --- initial state --- + ╟── 1 + ╟── 2 + ╟── 3 + ╙── 4 + --- add_edge(1, 2) --- + ╟── 3 + ╟── 4 + ╙── 1 + └── 2 + --- add_edge(1, 3) --- + ╟── 4 + ╙── 2 + └── 1 + └── 3 + --- add_edge(1, 4) --- + ╙── 2 + └── 1 + ├── 3 + └── 4 + --- add_edge(2, 1) --- + ╙── 2 + └── 1 + ├── 3 + └── 4 + --- add_edge(2, 3) --- + ╙── 4 + └── 1 + ├── 2 + │ └── 3 ─ 1 + └── ... + --- add_edge(2, 4) --- + ╙── 3 + ├── 1 + │ ├── 2 ─ 3 + │ │ └── 4 ─ 1 + │ └── ... + └── ... + --- add_edge(3, 1) --- + ╙── 3 + ├── 1 + │ ├── 2 ─ 3 + │ │ └── 4 ─ 1 + │ └── ... + └── ... + --- add_edge(3, 2) --- + ╙── 3 + ├── 1 + │ ├── 2 ─ 3 + │ │ └── 4 ─ 1 + │ └── ... + └── ... + --- add_edge(3, 4) --- + ╙── 1 + ├── 2 + │ ├── 3 ─ 1 + │ │ └── 4 ─ 1, 2 + │ └── ... + └── ... + --- add_edge(4, 1) --- + ╙── 1 + ├── 2 + │ ├── 3 ─ 1 + │ │ └── 4 ─ 1, 2 + │ └── ... + └── ... + --- add_edge(4, 2) --- + ╙── 1 + ├── 2 + │ ├── 3 ─ 1 + │ │ └── 4 ─ 1, 2 + │ └── ... + └── ... + --- add_edge(4, 3) --- + ╙── 1 + ├── 2 + │ ├── 3 ─ 1 + │ │ └── 4 ─ 1, 2 + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_iterative_add_random_directed_edges(): + """ + Walk through the cases going from a disconnected to fully connected graph + """ + + rng = random.Random(724466096) + graph = nx.DiGraph() + graph.add_nodes_from([1, 2, 3, 4, 5]) + possible_edges = list(product(graph.nodes, graph.nodes)) + rng.shuffle(possible_edges) + graph.add_edges_from(possible_edges[0:8]) + lines = [] + write = lines.append + write("--- initial state ---") + nx.write_network_text(graph, path=write, end="") + for i, j in possible_edges[8:12]: + write(f"--- add_edge({i}, {j}) ---") + graph.add_edge(i, j) + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + --- initial state --- + ╙── 3 ╾ 5 + └─╼ 2 ╾ 2 + ├─╼ 4 ╾ 4 + │ ├─╼ 5 + │ │ ├─╼ 1 ╾ 1 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(4, 1) --- + ╙── 3 ╾ 5 + └─╼ 2 ╾ 2 + ├─╼ 4 ╾ 4 + │ ├─╼ 5 + │ │ ├─╼ 1 ╾ 1, 4 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(2, 1) --- + ╙── 3 ╾ 5 + └─╼ 2 ╾ 2 + ├─╼ 4 ╾ 4 + │ ├─╼ 5 + │ │ ├─╼ 1 ╾ 1, 4, 2 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(5, 2) --- + ╙── 3 ╾ 5 + └─╼ 2 ╾ 2, 5 + ├─╼ 4 ╾ 4 + │ ├─╼ 5 + │ │ ├─╼ 1 ╾ 1, 4, 2 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- add_edge(1, 5) --- + ╙── 3 ╾ 5 + └─╼ 2 ╾ 2, 5 + ├─╼ 4 ╾ 4 + │ ├─╼ 5 ╾ 1 + │ │ ├─╼ 1 ╾ 1, 4, 2 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + + """ + ).strip() + assert target == text + + +def test_write_network_text_nearly_forest(): + g = nx.DiGraph() + g.add_edge(1, 2) + g.add_edge(1, 5) + g.add_edge(2, 3) + g.add_edge(3, 4) + g.add_edge(5, 6) + g.add_edge(6, 7) + g.add_edge(6, 8) + orig = g.copy() + g.add_edge(1, 8) # forward edge + g.add_edge(4, 2) # back edge + g.add_edge(6, 3) # cross edge + lines = [] + write = lines.append + write("--- directed case ---") + nx.write_network_text(orig, path=write, end="") + write("--- add (1, 8), (4, 2), (6, 3) ---") + nx.write_network_text(g, path=write, end="") + write("--- undirected case ---") + nx.write_network_text(orig.to_undirected(), path=write, sources=[1], end="") + write("--- add (1, 8), (4, 2), (6, 3) ---") + nx.write_network_text(g.to_undirected(), path=write, sources=[1], end="") + text = "\n".join(lines) + target = dedent( + """ + --- directed case --- + ╙── 1 + ├─╼ 2 + │ └─╼ 3 + │ └─╼ 4 + └─╼ 5 + └─╼ 6 + ├─╼ 7 + └─╼ 8 + --- add (1, 8), (4, 2), (6, 3) --- + ╙── 1 + ├─╼ 2 ╾ 4 + │ └─╼ 3 ╾ 6 + │ └─╼ 4 + │ └─╼ ... + ├─╼ 5 + │ └─╼ 6 + │ ├─╼ 7 + │ ├─╼ 8 ╾ 1 + │ └─╼ ... + └─╼ ... + --- undirected case --- + ╙── 1 + ├── 2 + │ └── 3 + │ └── 4 + └── 5 + └── 6 + ├── 7 + └── 8 + --- add (1, 8), (4, 2), (6, 3) --- + ╙── 1 + ├── 2 + │ ├── 3 + │ │ ├── 4 ─ 2 + │ │ └── 6 + │ │ ├── 5 ─ 1 + │ │ ├── 7 + │ │ └── 8 ─ 1 + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_complete_graph_ascii_only(): + graph = nx.generators.complete_graph(5, create_using=nx.DiGraph) + lines = [] + write = lines.append + write("--- directed case ---") + nx.write_network_text(graph, path=write, ascii_only=True, end="") + write("--- undirected case ---") + nx.write_network_text(graph.to_undirected(), path=write, ascii_only=True, end="") + text = "\n".join(lines) + target = dedent( + """ + --- directed case --- + +-- 0 <- 1, 2, 3, 4 + |-> 1 <- 2, 3, 4 + | |-> 2 <- 0, 3, 4 + | | |-> 3 <- 0, 1, 4 + | | | |-> 4 <- 0, 1, 2 + | | | | L-> ... + | | | L-> ... + | | L-> ... + | L-> ... + L-> ... + --- undirected case --- + +-- 0 + |-- 1 + | |-- 2 - 0 + | | |-- 3 - 0, 1 + | | | L-- 4 - 0, 1, 2 + | | L-- ... + | L-- ... + L-- ... + """ + ).strip() + assert target == text + + +def test_write_network_text_with_labels(): + graph = nx.generators.complete_graph(5, create_using=nx.DiGraph) + for n in graph.nodes: + graph.nodes[n]["label"] = f"Node(n={n})" + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, with_labels=True, ascii_only=False, end="") + text = "\n".join(lines) + # Non trees with labels can get somewhat out of hand with network text + # because we need to immediately show every non-tree edge to the right + target = dedent( + """ + ╙── Node(n=0) ╾ Node(n=1), Node(n=2), Node(n=3), Node(n=4) + ├─╼ Node(n=1) ╾ Node(n=2), Node(n=3), Node(n=4) + │ ├─╼ Node(n=2) ╾ Node(n=0), Node(n=3), Node(n=4) + │ │ ├─╼ Node(n=3) ╾ Node(n=0), Node(n=1), Node(n=4) + │ │ │ ├─╼ Node(n=4) ╾ Node(n=0), Node(n=1), Node(n=2) + │ │ │ │ └─╼ ... + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + """ + ).strip() + assert target == text + + +def test_write_network_text_complete_graphs(): + lines = [] + write = lines.append + for k in [0, 1, 2, 3, 4, 5]: + g = nx.generators.complete_graph(k) + write(f"--- undirected k={k} ---") + nx.write_network_text(g, path=write, end="") + + for k in [0, 1, 2, 3, 4, 5]: + g = nx.generators.complete_graph(k, nx.DiGraph) + write(f"--- directed k={k} ---") + nx.write_network_text(g, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + --- undirected k=0 --- + ╙ + --- undirected k=1 --- + ╙── 0 + --- undirected k=2 --- + ╙── 0 + └── 1 + --- undirected k=3 --- + ╙── 0 + ├── 1 + │ └── 2 ─ 0 + └── ... + --- undirected k=4 --- + ╙── 0 + ├── 1 + │ ├── 2 ─ 0 + │ │ └── 3 ─ 0, 1 + │ └── ... + └── ... + --- undirected k=5 --- + ╙── 0 + ├── 1 + │ ├── 2 ─ 0 + │ │ ├── 3 ─ 0, 1 + │ │ │ └── 4 ─ 0, 1, 2 + │ │ └── ... + │ └── ... + └── ... + --- directed k=0 --- + ╙ + --- directed k=1 --- + ╙── 0 + --- directed k=2 --- + ╙── 0 ╾ 1 + └─╼ 1 + └─╼ ... + --- directed k=3 --- + ╙── 0 ╾ 1, 2 + ├─╼ 1 ╾ 2 + │ ├─╼ 2 ╾ 0 + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- directed k=4 --- + ╙── 0 ╾ 1, 2, 3 + ├─╼ 1 ╾ 2, 3 + │ ├─╼ 2 ╾ 0, 3 + │ │ ├─╼ 3 ╾ 0, 1 + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- directed k=5 --- + ╙── 0 ╾ 1, 2, 3, 4 + ├─╼ 1 ╾ 2, 3, 4 + │ ├─╼ 2 ╾ 0, 3, 4 + │ │ ├─╼ 3 ╾ 0, 1, 4 + │ │ │ ├─╼ 4 ╾ 0, 1, 2 + │ │ │ │ └─╼ ... + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + """ + ).strip() + assert target == text + + +def test_write_network_text_multiple_sources(): + g = nx.DiGraph() + g.add_edge(1, 2) + g.add_edge(1, 3) + g.add_edge(2, 4) + g.add_edge(3, 5) + g.add_edge(3, 6) + g.add_edge(5, 4) + g.add_edge(4, 1) + g.add_edge(1, 5) + lines = [] + write = lines.append + # Use each node as the starting point to demonstrate how the representation + # changes. + nodes = sorted(g.nodes()) + for n in nodes: + write(f"--- source node: {n} ---") + nx.write_network_text(g, path=write, sources=[n], end="") + text = "\n".join(lines) + target = dedent( + """ + --- source node: 1 --- + ╙── 1 ╾ 4 + ├─╼ 2 + │ └─╼ 4 ╾ 5 + │ └─╼ ... + ├─╼ 3 + │ ├─╼ 5 ╾ 1 + │ │ └─╼ ... + │ └─╼ 6 + └─╼ ... + --- source node: 2 --- + ╙── 2 ╾ 1 + └─╼ 4 ╾ 5 + └─╼ 1 + ├─╼ 3 + │ ├─╼ 5 ╾ 1 + │ │ └─╼ ... + │ └─╼ 6 + └─╼ ... + --- source node: 3 --- + ╙── 3 ╾ 1 + ├─╼ 5 ╾ 1 + │ └─╼ 4 ╾ 2 + │ └─╼ 1 + │ ├─╼ 2 + │ │ └─╼ ... + │ └─╼ ... + └─╼ 6 + --- source node: 4 --- + ╙── 4 ╾ 2, 5 + └─╼ 1 + ├─╼ 2 + │ └─╼ ... + ├─╼ 3 + │ ├─╼ 5 ╾ 1 + │ │ └─╼ ... + │ └─╼ 6 + └─╼ ... + --- source node: 5 --- + ╙── 5 ╾ 3, 1 + └─╼ 4 ╾ 2 + └─╼ 1 + ├─╼ 2 + │ └─╼ ... + ├─╼ 3 + │ ├─╼ 6 + │ └─╼ ... + └─╼ ... + --- source node: 6 --- + ╙── 6 ╾ 3 + """ + ).strip() + assert target == text + + +def test_write_network_text_star_graph(): + graph = nx.star_graph(5, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 1 + └── 0 + ├── 2 + ├── 3 + ├── 4 + └── 5 + """ + ).strip() + assert target == text + + +def test_write_network_text_path_graph(): + graph = nx.path_graph(3, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 0 + └── 1 + └── 2 + """ + ).strip() + assert target == text + + +def test_write_network_text_lollipop_graph(): + graph = nx.lollipop_graph(4, 2, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 5 + └── 4 + └── 3 + ├── 0 + │ ├── 1 ─ 3 + │ │ └── 2 ─ 0, 3 + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_wheel_graph(): + graph = nx.wheel_graph(7, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 1 + ├── 0 + │ ├── 2 ─ 1 + │ │ └── 3 ─ 0 + │ │ └── 4 ─ 0 + │ │ └── 5 ─ 0 + │ │ └── 6 ─ 0, 1 + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_circular_ladder_graph(): + graph = nx.circular_ladder_graph(4, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 0 + ├── 1 + │ ├── 2 + │ │ ├── 3 ─ 0 + │ │ │ └── 7 + │ │ │ ├── 6 ─ 2 + │ │ │ │ └── 5 ─ 1 + │ │ │ │ └── 4 ─ 0, 7 + │ │ │ └── ... + │ │ └── ... + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_dorogovtsev_goltsev_mendes_graph(): + graph = nx.dorogovtsev_goltsev_mendes_graph(4, create_using=nx.Graph) + lines = [] + write = lines.append + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + ╙── 15 + ├── 0 + │ ├── 1 ─ 15 + │ │ ├── 2 ─ 0 + │ │ │ ├── 4 ─ 0 + │ │ │ │ ├── 9 ─ 0 + │ │ │ │ │ ├── 22 ─ 0 + │ │ │ │ │ └── 38 ─ 4 + │ │ │ │ ├── 13 ─ 2 + │ │ │ │ │ ├── 34 ─ 2 + │ │ │ │ │ └── 39 ─ 4 + │ │ │ │ ├── 18 ─ 0 + │ │ │ │ ├── 30 ─ 2 + │ │ │ │ └── ... + │ │ │ ├── 5 ─ 1 + │ │ │ │ ├── 12 ─ 1 + │ │ │ │ │ ├── 29 ─ 1 + │ │ │ │ │ └── 40 ─ 5 + │ │ │ │ ├── 14 ─ 2 + │ │ │ │ │ ├── 35 ─ 2 + │ │ │ │ │ └── 41 ─ 5 + │ │ │ │ ├── 25 ─ 1 + │ │ │ │ ├── 31 ─ 2 + │ │ │ │ └── ... + │ │ │ ├── 7 ─ 0 + │ │ │ │ ├── 20 ─ 0 + │ │ │ │ └── 32 ─ 2 + │ │ │ ├── 10 ─ 1 + │ │ │ │ ├── 27 ─ 1 + │ │ │ │ └── 33 ─ 2 + │ │ │ ├── 16 ─ 0 + │ │ │ ├── 23 ─ 1 + │ │ │ └── ... + │ │ ├── 3 ─ 0 + │ │ │ ├── 8 ─ 0 + │ │ │ │ ├── 21 ─ 0 + │ │ │ │ └── 36 ─ 3 + │ │ │ ├── 11 ─ 1 + │ │ │ │ ├── 28 ─ 1 + │ │ │ │ └── 37 ─ 3 + │ │ │ ├── 17 ─ 0 + │ │ │ ├── 24 ─ 1 + │ │ │ └── ... + │ │ ├── 6 ─ 0 + │ │ │ ├── 19 ─ 0 + │ │ │ └── 26 ─ 1 + │ │ └── ... + │ └── ... + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_tree_max_depth(): + orig = nx.balanced_tree(r=1, h=3, create_using=nx.DiGraph) + lines = [] + write = lines.append + write("--- directed case, max_depth=0 ---") + nx.write_network_text(orig, path=write, end="", max_depth=0) + write("--- directed case, max_depth=1 ---") + nx.write_network_text(orig, path=write, end="", max_depth=1) + write("--- directed case, max_depth=2 ---") + nx.write_network_text(orig, path=write, end="", max_depth=2) + write("--- directed case, max_depth=3 ---") + nx.write_network_text(orig, path=write, end="", max_depth=3) + write("--- directed case, max_depth=4 ---") + nx.write_network_text(orig, path=write, end="", max_depth=4) + write("--- undirected case, max_depth=0 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0) + write("--- undirected case, max_depth=1 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1) + write("--- undirected case, max_depth=2 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2) + write("--- undirected case, max_depth=3 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3) + write("--- undirected case, max_depth=4 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=4) + text = "\n".join(lines) + target = dedent( + """ + --- directed case, max_depth=0 --- + ╙ ... + --- directed case, max_depth=1 --- + ╙── 0 + └─╼ ... + --- directed case, max_depth=2 --- + ╙── 0 + └─╼ 1 + └─╼ ... + --- directed case, max_depth=3 --- + ╙── 0 + └─╼ 1 + └─╼ 2 + └─╼ ... + --- directed case, max_depth=4 --- + ╙── 0 + └─╼ 1 + └─╼ 2 + └─╼ 3 + --- undirected case, max_depth=0 --- + ╙ ... + --- undirected case, max_depth=1 --- + ╙── 0 ─ 1 + └── ... + --- undirected case, max_depth=2 --- + ╙── 0 + └── 1 ─ 2 + └── ... + --- undirected case, max_depth=3 --- + ╙── 0 + └── 1 + └── 2 ─ 3 + └── ... + --- undirected case, max_depth=4 --- + ╙── 0 + └── 1 + └── 2 + └── 3 + """ + ).strip() + assert target == text + + +def test_write_network_text_graph_max_depth(): + orig = nx.erdos_renyi_graph(10, 0.15, directed=True, seed=40392) + lines = [] + write = lines.append + write("--- directed case, max_depth=None ---") + nx.write_network_text(orig, path=write, end="", max_depth=None) + write("--- directed case, max_depth=0 ---") + nx.write_network_text(orig, path=write, end="", max_depth=0) + write("--- directed case, max_depth=1 ---") + nx.write_network_text(orig, path=write, end="", max_depth=1) + write("--- directed case, max_depth=2 ---") + nx.write_network_text(orig, path=write, end="", max_depth=2) + write("--- directed case, max_depth=3 ---") + nx.write_network_text(orig, path=write, end="", max_depth=3) + write("--- undirected case, max_depth=None ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=None) + write("--- undirected case, max_depth=0 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0) + write("--- undirected case, max_depth=1 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1) + write("--- undirected case, max_depth=2 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2) + write("--- undirected case, max_depth=3 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3) + text = "\n".join(lines) + target = dedent( + """ + --- directed case, max_depth=None --- + ╟── 4 + ╎ ├─╼ 0 ╾ 3 + ╎ ├─╼ 5 ╾ 7 + ╎ │ └─╼ 3 + ╎ │ ├─╼ 1 ╾ 9 + ╎ │ │ └─╼ 9 ╾ 6 + ╎ │ │ ├─╼ 6 + ╎ │ │ │ └─╼ ... + ╎ │ │ ├─╼ 7 ╾ 4 + ╎ │ │ │ ├─╼ 2 + ╎ │ │ │ └─╼ ... + ╎ │ │ └─╼ ... + ╎ │ └─╼ ... + ╎ └─╼ ... + ╙── 8 + --- directed case, max_depth=0 --- + ╙ ... + --- directed case, max_depth=1 --- + ╟── 4 + ╎ └─╼ ... + ╙── 8 + --- directed case, max_depth=2 --- + ╟── 4 + ╎ ├─╼ 0 ╾ 3 + ╎ ├─╼ 5 ╾ 7 + ╎ │ └─╼ ... + ╎ └─╼ 7 ╾ 9 + ╎ └─╼ ... + ╙── 8 + --- directed case, max_depth=3 --- + ╟── 4 + ╎ ├─╼ 0 ╾ 3 + ╎ ├─╼ 5 ╾ 7 + ╎ │ └─╼ 3 + ╎ │ └─╼ ... + ╎ └─╼ 7 ╾ 9 + ╎ ├─╼ 2 + ╎ └─╼ ... + ╙── 8 + --- undirected case, max_depth=None --- + ╟── 8 + ╙── 2 + └── 7 + ├── 4 + │ ├── 0 + │ │ └── 3 + │ │ ├── 1 + │ │ │ └── 9 ─ 7 + │ │ │ └── 6 + │ │ └── 5 ─ 4, 7 + │ └── ... + └── ... + --- undirected case, max_depth=0 --- + ╙ ... + --- undirected case, max_depth=1 --- + ╟── 8 + ╙── 2 ─ 7 + └── ... + --- undirected case, max_depth=2 --- + ╟── 8 + ╙── 2 + └── 7 ─ 4, 5, 9 + └── ... + --- undirected case, max_depth=3 --- + ╟── 8 + ╙── 2 + └── 7 + ├── 4 ─ 0, 5 + │ └── ... + ├── 5 ─ 4, 3 + │ └── ... + └── 9 ─ 1, 6 + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_clique_max_depth(): + orig = nx.complete_graph(5, nx.DiGraph) + lines = [] + write = lines.append + write("--- directed case, max_depth=None ---") + nx.write_network_text(orig, path=write, end="", max_depth=None) + write("--- directed case, max_depth=0 ---") + nx.write_network_text(orig, path=write, end="", max_depth=0) + write("--- directed case, max_depth=1 ---") + nx.write_network_text(orig, path=write, end="", max_depth=1) + write("--- directed case, max_depth=2 ---") + nx.write_network_text(orig, path=write, end="", max_depth=2) + write("--- directed case, max_depth=3 ---") + nx.write_network_text(orig, path=write, end="", max_depth=3) + write("--- undirected case, max_depth=None ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=None) + write("--- undirected case, max_depth=0 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=0) + write("--- undirected case, max_depth=1 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=1) + write("--- undirected case, max_depth=2 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=2) + write("--- undirected case, max_depth=3 ---") + nx.write_network_text(orig.to_undirected(), path=write, end="", max_depth=3) + text = "\n".join(lines) + target = dedent( + """ + --- directed case, max_depth=None --- + ╙── 0 ╾ 1, 2, 3, 4 + ├─╼ 1 ╾ 2, 3, 4 + │ ├─╼ 2 ╾ 0, 3, 4 + │ │ ├─╼ 3 ╾ 0, 1, 4 + │ │ │ ├─╼ 4 ╾ 0, 1, 2 + │ │ │ │ └─╼ ... + │ │ │ └─╼ ... + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- directed case, max_depth=0 --- + ╙ ... + --- directed case, max_depth=1 --- + ╙── 0 ╾ 1, 2, 3, 4 + └─╼ ... + --- directed case, max_depth=2 --- + ╙── 0 ╾ 1, 2, 3, 4 + ├─╼ 1 ╾ 2, 3, 4 + │ └─╼ ... + ├─╼ 2 ╾ 1, 3, 4 + │ └─╼ ... + ├─╼ 3 ╾ 1, 2, 4 + │ └─╼ ... + └─╼ 4 ╾ 1, 2, 3 + └─╼ ... + --- directed case, max_depth=3 --- + ╙── 0 ╾ 1, 2, 3, 4 + ├─╼ 1 ╾ 2, 3, 4 + │ ├─╼ 2 ╾ 0, 3, 4 + │ │ └─╼ ... + │ ├─╼ 3 ╾ 0, 2, 4 + │ │ └─╼ ... + │ ├─╼ 4 ╾ 0, 2, 3 + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + --- undirected case, max_depth=None --- + ╙── 0 + ├── 1 + │ ├── 2 ─ 0 + │ │ ├── 3 ─ 0, 1 + │ │ │ └── 4 ─ 0, 1, 2 + │ │ └── ... + │ └── ... + └── ... + --- undirected case, max_depth=0 --- + ╙ ... + --- undirected case, max_depth=1 --- + ╙── 0 ─ 1, 2, 3, 4 + └── ... + --- undirected case, max_depth=2 --- + ╙── 0 + ├── 1 ─ 2, 3, 4 + │ └── ... + ├── 2 ─ 1, 3, 4 + │ └── ... + ├── 3 ─ 1, 2, 4 + │ └── ... + └── 4 ─ 1, 2, 3 + --- undirected case, max_depth=3 --- + ╙── 0 + ├── 1 + │ ├── 2 ─ 0, 3, 4 + │ │ └── ... + │ ├── 3 ─ 0, 2, 4 + │ │ └── ... + │ └── 4 ─ 0, 2, 3 + └── ... + """ + ).strip() + assert target == text + + +def test_write_network_text_custom_label(): + # Create a directed forest with labels + graph = nx.erdos_renyi_graph(5, 0.4, directed=True, seed=359222358) + for node in graph.nodes: + graph.nodes[node]["label"] = f"Node({node})" + graph.nodes[node]["chr"] = chr(node + ord("a") - 1) + if node % 2 == 0: + graph.nodes[node]["part"] = chr(node + ord("a")) + + lines = [] + write = lines.append + write("--- when with_labels=True, uses the 'label' attr ---") + nx.write_network_text(graph, path=write, with_labels=True, end="", max_depth=None) + write("--- when with_labels=False, uses str(node) value ---") + nx.write_network_text(graph, path=write, with_labels=False, end="", max_depth=None) + write("--- when with_labels is a string, use that attr ---") + nx.write_network_text(graph, path=write, with_labels="chr", end="", max_depth=None) + write("--- fallback to str(node) when the attr does not exist ---") + nx.write_network_text(graph, path=write, with_labels="part", end="", max_depth=None) + + text = "\n".join(lines) + target = dedent( + """ + --- when with_labels=True, uses the 'label' attr --- + ╙── Node(1) + └─╼ Node(3) ╾ Node(2) + ├─╼ Node(0) + │ ├─╼ Node(2) ╾ Node(3), Node(4) + │ │ └─╼ ... + │ └─╼ Node(4) + │ └─╼ ... + └─╼ ... + --- when with_labels=False, uses str(node) value --- + ╙── 1 + └─╼ 3 ╾ 2 + ├─╼ 0 + │ ├─╼ 2 ╾ 3, 4 + │ │ └─╼ ... + │ └─╼ 4 + │ └─╼ ... + └─╼ ... + --- when with_labels is a string, use that attr --- + ╙── a + └─╼ c ╾ b + ├─╼ ` + │ ├─╼ b ╾ c, d + │ │ └─╼ ... + │ └─╼ d + │ └─╼ ... + └─╼ ... + --- fallback to str(node) when the attr does not exist --- + ╙── 1 + └─╼ 3 ╾ c + ├─╼ a + │ ├─╼ c ╾ 3, e + │ │ └─╼ ... + │ └─╼ e + │ └─╼ ... + └─╼ ... + """ + ).strip() + assert target == text + + +def test_write_network_text_vertical_chains(): + graph1 = nx.lollipop_graph(4, 2, create_using=nx.Graph) + graph1.add_edge(0, -1) + graph1.add_edge(-1, -2) + graph1.add_edge(-2, -3) + + graph2 = graph1.to_directed() + graph2.remove_edges_from([(u, v) for u, v in graph2.edges if v > u]) + + lines = [] + write = lines.append + write("--- Undirected UTF ---") + nx.write_network_text(graph1, path=write, end="", vertical_chains=True) + write("--- Undirected ASCI ---") + nx.write_network_text( + graph1, path=write, end="", vertical_chains=True, ascii_only=True + ) + write("--- Directed UTF ---") + nx.write_network_text(graph2, path=write, end="", vertical_chains=True) + write("--- Directed ASCI ---") + nx.write_network_text( + graph2, path=write, end="", vertical_chains=True, ascii_only=True + ) + + text = "\n".join(lines) + target = dedent( + """ + --- Undirected UTF --- + ╙── 5 + │ + 4 + │ + 3 + ├── 0 + │ ├── 1 ─ 3 + │ │ │ + │ │ 2 ─ 0, 3 + │ ├── -1 + │ │ │ + │ │ -2 + │ │ │ + │ │ -3 + │ └── ... + └── ... + --- Undirected ASCI --- + +-- 5 + | + 4 + | + 3 + |-- 0 + | |-- 1 - 3 + | | | + | | 2 - 0, 3 + | |-- -1 + | | | + | | -2 + | | | + | | -3 + | L-- ... + L-- ... + --- Directed UTF --- + ╙── 5 + ╽ + 4 + ╽ + 3 + ├─╼ 0 ╾ 1, 2 + │ ╽ + │ -1 + │ ╽ + │ -2 + │ ╽ + │ -3 + ├─╼ 1 ╾ 2 + │ └─╼ ... + └─╼ 2 + └─╼ ... + --- Directed ASCI --- + +-- 5 + ! + 4 + ! + 3 + |-> 0 <- 1, 2 + | ! + | -1 + | ! + | -2 + | ! + | -3 + |-> 1 <- 2 + | L-> ... + L-> 2 + L-> ... + """ + ).strip() + assert target == text + + +def test_collapse_directed(): + graph = nx.balanced_tree(r=2, h=3, create_using=nx.DiGraph) + lines = [] + write = lines.append + write("--- Original ---") + nx.write_network_text(graph, path=write, end="") + graph.nodes[1]["collapse"] = True + write("--- Collapse Node 1 ---") + nx.write_network_text(graph, path=write, end="") + write("--- Add alternate path (5, 3) to collapsed zone") + graph.add_edge(5, 3) + nx.write_network_text(graph, path=write, end="") + write("--- Collapse Node 0 ---") + graph.nodes[0]["collapse"] = True + nx.write_network_text(graph, path=write, end="") + text = "\n".join(lines) + target = dedent( + """ + --- Original --- + ╙── 0 + ├─╼ 1 + │ ├─╼ 3 + │ │ ├─╼ 7 + │ │ └─╼ 8 + │ └─╼ 4 + │ ├─╼ 9 + │ └─╼ 10 + └─╼ 2 + ├─╼ 5 + │ ├─╼ 11 + │ └─╼ 12 + └─╼ 6 + ├─╼ 13 + └─╼ 14 + --- Collapse Node 1 --- + ╙── 0 + ├─╼ 1 + │ └─╼ ... + └─╼ 2 + ├─╼ 5 + │ ├─╼ 11 + │ └─╼ 12 + └─╼ 6 + ├─╼ 13 + └─╼ 14 + --- Add alternate path (5, 3) to collapsed zone + ╙── 0 + ├─╼ 1 + │ └─╼ ... + └─╼ 2 + ├─╼ 5 + │ ├─╼ 11 + │ ├─╼ 12 + │ └─╼ 3 ╾ 1 + │ ├─╼ 7 + │ └─╼ 8 + └─╼ 6 + ├─╼ 13 + └─╼ 14 + --- Collapse Node 0 --- + ╙── 0 + └─╼ ... + """ + ).strip() + assert target == text + + +def test_collapse_undirected(): + graph = nx.balanced_tree(r=2, h=3, create_using=nx.Graph) + lines = [] + write = lines.append + write("--- Original ---") + nx.write_network_text(graph, path=write, end="", sources=[0]) + graph.nodes[1]["collapse"] = True + write("--- Collapse Node 1 ---") + nx.write_network_text(graph, path=write, end="", sources=[0]) + write("--- Add alternate path (5, 3) to collapsed zone") + graph.add_edge(5, 3) + nx.write_network_text(graph, path=write, end="", sources=[0]) + write("--- Collapse Node 0 ---") + graph.nodes[0]["collapse"] = True + nx.write_network_text(graph, path=write, end="", sources=[0]) + text = "\n".join(lines) + target = dedent( + """ + --- Original --- + ╙── 0 + ├── 1 + │ ├── 3 + │ │ ├── 7 + │ │ └── 8 + │ └── 4 + │ ├── 9 + │ └── 10 + └── 2 + ├── 5 + │ ├── 11 + │ └── 12 + └── 6 + ├── 13 + └── 14 + --- Collapse Node 1 --- + ╙── 0 + ├── 1 ─ 3, 4 + │ └── ... + └── 2 + ├── 5 + │ ├── 11 + │ └── 12 + └── 6 + ├── 13 + └── 14 + --- Add alternate path (5, 3) to collapsed zone + ╙── 0 + ├── 1 ─ 3, 4 + │ └── ... + └── 2 + ├── 5 + │ ├── 11 + │ ├── 12 + │ └── 3 ─ 1 + │ ├── 7 + │ └── 8 + └── 6 + ├── 13 + └── 14 + --- Collapse Node 0 --- + ╙── 0 ─ 1, 2 + └── ... + """ + ).strip() + assert target == text + + +def generate_test_graphs(): + """ + Generate a gauntlet of different test graphs with different properties + """ + import random + + rng = random.Random(976689776) + num_randomized = 3 + + for directed in [0, 1]: + cls = nx.DiGraph if directed else nx.Graph + + for num_nodes in range(17): + # Disconnected graph + graph = cls() + graph.add_nodes_from(range(num_nodes)) + yield graph + + # Randomize graphs + if num_nodes > 0: + for p in [0.1, 0.3, 0.5, 0.7, 0.9]: + for seed in range(num_randomized): + graph = nx.erdos_renyi_graph( + num_nodes, p, directed=directed, seed=rng + ) + yield graph + + yield nx.complete_graph(num_nodes, cls) + + yield nx.path_graph(3, create_using=cls) + yield nx.balanced_tree(r=1, h=3, create_using=cls) + if not directed: + yield nx.circular_ladder_graph(4, create_using=cls) + yield nx.star_graph(5, create_using=cls) + yield nx.lollipop_graph(4, 2, create_using=cls) + yield nx.wheel_graph(7, create_using=cls) + yield nx.dorogovtsev_goltsev_mendes_graph(4, create_using=cls) + + +@pytest.mark.parametrize( + ("vertical_chains", "ascii_only"), + tuple( + [ + (vertical_chains, ascii_only) + for vertical_chains in [0, 1] + for ascii_only in [0, 1] + ] + ), +) +def test_network_text_round_trip(vertical_chains, ascii_only): + """ + Write the graph to network text format, then parse it back in, assert it is + the same as the original graph. Passing this test is strong validation of + both the format generator and parser. + """ + from networkx.readwrite.text import _parse_network_text + + for graph in generate_test_graphs(): + graph = nx.relabel_nodes(graph, {n: str(n) for n in graph.nodes}) + lines = list( + nx.generate_network_text( + graph, vertical_chains=vertical_chains, ascii_only=ascii_only + ) + ) + new = _parse_network_text(lines) + try: + assert new.nodes == graph.nodes + assert new.edges == graph.edges + except Exception: + nx.write_network_text(graph) + raise diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/text.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/text.py new file mode 100644 index 0000000000000000000000000000000000000000..6fce220764d0e3aab0dff0200f3d7b601d03d007 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/readwrite/text.py @@ -0,0 +1,851 @@ +""" +Text-based visual representations of graphs +""" + +import sys +from collections import defaultdict + +import networkx as nx +from networkx.utils import open_file + +__all__ = ["generate_network_text", "write_network_text"] + + +class BaseGlyphs: + @classmethod + def as_dict(cls): + return { + a: getattr(cls, a) + for a in dir(cls) + if not a.startswith("_") and a != "as_dict" + } + + +class AsciiBaseGlyphs(BaseGlyphs): + empty: str = "+" + newtree_last: str = "+-- " + newtree_mid: str = "+-- " + endof_forest: str = " " + within_forest: str = ": " + within_tree: str = "| " + + +class AsciiDirectedGlyphs(AsciiBaseGlyphs): + last: str = "L-> " + mid: str = "|-> " + backedge: str = "<-" + vertical_edge: str = "!" + + +class AsciiUndirectedGlyphs(AsciiBaseGlyphs): + last: str = "L-- " + mid: str = "|-- " + backedge: str = "-" + vertical_edge: str = "|" + + +class UtfBaseGlyphs(BaseGlyphs): + # Notes on available box and arrow characters + # https://en.wikipedia.org/wiki/Box-drawing_character + # https://stackoverflow.com/questions/2701192/triangle-arrow + empty: str = "╙" + newtree_last: str = "╙── " + newtree_mid: str = "╟── " + endof_forest: str = " " + within_forest: str = "╎ " + within_tree: str = "│ " + + +class UtfDirectedGlyphs(UtfBaseGlyphs): + last: str = "└─╼ " + mid: str = "├─╼ " + backedge: str = "╾" + vertical_edge: str = "╽" + + +class UtfUndirectedGlyphs(UtfBaseGlyphs): + last: str = "└── " + mid: str = "├── " + backedge: str = "─" + vertical_edge: str = "│" + + +def generate_network_text( + graph, + with_labels=True, + sources=None, + max_depth=None, + ascii_only=False, + vertical_chains=False, +): + """Generate lines in the "network text" format + + This works via a depth-first traversal of the graph and writing a line for + each unique node encountered. Non-tree edges are written to the right of + each node, and connection to a non-tree edge is indicated with an ellipsis. + This representation works best when the input graph is a forest, but any + graph can be represented. + + This notation is original to networkx, although it is simple enough that it + may be known in existing literature. See #5602 for details. The procedure + is summarized as follows: + + 1. Given a set of source nodes (which can be specified, or automatically + discovered via finding the (strongly) connected components and choosing one + node with minimum degree from each), we traverse the graph in depth first + order. + + 2. Each reachable node will be printed exactly once on it's own line. + + 3. Edges are indicated in one of four ways: + + a. a parent "L-style" connection on the upper left. This corresponds to + a traversal in the directed DFS tree. + + b. a backref "<-style" connection shown directly on the right. For + directed graphs, these are drawn for any incoming edges to a node that + is not a parent edge. For undirected graphs, these are drawn for only + the non-parent edges that have already been represented (The edges that + have not been represented will be handled in the recursive case). + + c. a child "L-style" connection on the lower right. Drawing of the + children are handled recursively. + + d. if ``vertical_chains`` is true, and a parent node only has one child + a "vertical-style" edge is drawn between them. + + 4. The children of each node (wrt the directed DFS tree) are drawn + underneath and to the right of it. In the case that a child node has already + been drawn the connection is replaced with an ellipsis ("...") to indicate + that there is one or more connections represented elsewhere. + + 5. If a maximum depth is specified, an edge to nodes past this maximum + depth will be represented by an ellipsis. + + 6. If a node has a truthy "collapse" value, then we do not traverse past + that node. + + Parameters + ---------- + graph : nx.DiGraph | nx.Graph + Graph to represent + + with_labels : bool | str + If True will use the "label" attribute of a node to display if it + exists otherwise it will use the node value itself. If given as a + string, then that attribute name will be used instead of "label". + Defaults to True. + + sources : List + Specifies which nodes to start traversal from. Note: nodes that are not + reachable from one of these sources may not be shown. If unspecified, + the minimal set of nodes needed to reach all others will be used. + + max_depth : int | None + The maximum depth to traverse before stopping. Defaults to None. + + ascii_only : Boolean + If True only ASCII characters are used to construct the visualization + + vertical_chains : Boolean + If True, chains of nodes will be drawn vertically when possible. + + Yields + ------ + str : a line of generated text + + Examples + -------- + >>> graph = nx.path_graph(10) + >>> graph.add_node("A") + >>> graph.add_node("B") + >>> graph.add_node("C") + >>> graph.add_node("D") + >>> graph.add_edge(9, "A") + >>> graph.add_edge(9, "B") + >>> graph.add_edge(9, "C") + >>> graph.add_edge("C", "D") + >>> graph.add_edge("C", "E") + >>> graph.add_edge("C", "F") + >>> nx.write_network_text(graph) + ╙── 0 + └── 1 + └── 2 + └── 3 + └── 4 + └── 5 + └── 6 + └── 7 + └── 8 + └── 9 + ├── A + ├── B + └── C + ├── D + ├── E + └── F + >>> nx.write_network_text(graph, vertical_chains=True) + ╙── 0 + │ + 1 + │ + 2 + │ + 3 + │ + 4 + │ + 5 + │ + 6 + │ + 7 + │ + 8 + │ + 9 + ├── A + ├── B + └── C + ├── D + ├── E + └── F + """ + from typing import Any, NamedTuple + + class StackFrame(NamedTuple): + parent: Any + node: Any + indents: list + this_islast: bool + this_vertical: bool + + collapse_attr = "collapse" + + is_directed = graph.is_directed() + + if is_directed: + glyphs = AsciiDirectedGlyphs if ascii_only else UtfDirectedGlyphs + succ = graph.succ + pred = graph.pred + else: + glyphs = AsciiUndirectedGlyphs if ascii_only else UtfUndirectedGlyphs + succ = graph.adj + pred = graph.adj + + if isinstance(with_labels, str): + label_attr = with_labels + elif with_labels: + label_attr = "label" + else: + label_attr = None + + if max_depth == 0: + yield glyphs.empty + " ..." + elif len(graph.nodes) == 0: + yield glyphs.empty + else: + # If the nodes to traverse are unspecified, find the minimal set of + # nodes that will reach the entire graph + if sources is None: + sources = _find_sources(graph) + + # Populate the stack with each: + # 1. parent node in the DFS tree (or None for root nodes), + # 2. the current node in the DFS tree + # 2. a list of indentations indicating depth + # 3. a flag indicating if the node is the final one to be written. + # Reverse the stack so sources are popped in the correct order. + last_idx = len(sources) - 1 + stack = [ + StackFrame(None, node, [], (idx == last_idx), False) + for idx, node in enumerate(sources) + ][::-1] + + num_skipped_children = defaultdict(lambda: 0) + seen_nodes = set() + while stack: + parent, node, indents, this_islast, this_vertical = stack.pop() + + if node is not Ellipsis: + skip = node in seen_nodes + if skip: + # Mark that we skipped a parent's child + num_skipped_children[parent] += 1 + + if this_islast: + # If we reached the last child of a parent, and we skipped + # any of that parents children, then we should emit an + # ellipsis at the end after this. + if num_skipped_children[parent] and parent is not None: + # Append the ellipsis to be emitted last + next_islast = True + try_frame = StackFrame( + node, Ellipsis, indents, next_islast, False + ) + stack.append(try_frame) + + # Redo this frame, but not as a last object + next_islast = False + try_frame = StackFrame( + parent, node, indents, next_islast, this_vertical + ) + stack.append(try_frame) + continue + + if skip: + continue + seen_nodes.add(node) + + if not indents: + # Top level items (i.e. trees in the forest) get different + # glyphs to indicate they are not actually connected + if this_islast: + this_vertical = False + this_prefix = indents + [glyphs.newtree_last] + next_prefix = indents + [glyphs.endof_forest] + else: + this_prefix = indents + [glyphs.newtree_mid] + next_prefix = indents + [glyphs.within_forest] + + else: + # Non-top-level items + if this_vertical: + this_prefix = indents + next_prefix = indents + else: + if this_islast: + this_prefix = indents + [glyphs.last] + next_prefix = indents + [glyphs.endof_forest] + else: + this_prefix = indents + [glyphs.mid] + next_prefix = indents + [glyphs.within_tree] + + if node is Ellipsis: + label = " ..." + suffix = "" + children = [] + else: + if label_attr is not None: + label = str(graph.nodes[node].get(label_attr, node)) + else: + label = str(node) + + # Determine if we want to show the children of this node. + if collapse_attr is not None: + collapse = graph.nodes[node].get(collapse_attr, False) + else: + collapse = False + + # Determine: + # (1) children to traverse into after showing this node. + # (2) parents to immediately show to the right of this node. + if is_directed: + # In the directed case we must show every successor node + # note: it may be skipped later, but we don't have that + # information here. + children = list(succ[node]) + # In the directed case we must show every predecessor + # except for parent we directly traversed from. + handled_parents = {parent} + else: + # Showing only the unseen children results in a more + # concise representation for the undirected case. + children = [ + child for child in succ[node] if child not in seen_nodes + ] + + # In the undirected case, parents are also children, so we + # only need to immediately show the ones we can no longer + # traverse + handled_parents = {*children, parent} + + if max_depth is not None and len(indents) == max_depth - 1: + # Use ellipsis to indicate we have reached maximum depth + if children: + children = [Ellipsis] + handled_parents = {parent} + + if collapse: + # Collapsing a node is the same as reaching maximum depth + if children: + children = [Ellipsis] + handled_parents = {parent} + + # The other parents are other predecessors of this node that + # are not handled elsewhere. + other_parents = [p for p in pred[node] if p not in handled_parents] + if other_parents: + if label_attr is not None: + other_parents_labels = ", ".join( + [ + str(graph.nodes[p].get(label_attr, p)) + for p in other_parents + ] + ) + else: + other_parents_labels = ", ".join( + [str(p) for p in other_parents] + ) + suffix = " ".join(["", glyphs.backedge, other_parents_labels]) + else: + suffix = "" + + # Emit the line for this node, this will be called for each node + # exactly once. + if this_vertical: + yield "".join(this_prefix + [glyphs.vertical_edge]) + + yield "".join(this_prefix + [label, suffix]) + + if vertical_chains: + if is_directed: + num_children = len(set(children)) + else: + num_children = len(set(children) - {parent}) + # The next node can be drawn vertically if it is the only + # remaining child of this node. + next_is_vertical = num_children == 1 + else: + next_is_vertical = False + + # Push children on the stack in reverse order so they are popped in + # the original order. + for idx, child in enumerate(children[::-1]): + next_islast = idx == 0 + try_frame = StackFrame( + node, child, next_prefix, next_islast, next_is_vertical + ) + stack.append(try_frame) + + +@open_file(1, "w") +def write_network_text( + graph, + path=None, + with_labels=True, + sources=None, + max_depth=None, + ascii_only=False, + end="\n", + vertical_chains=False, +): + """Creates a nice text representation of a graph + + This works via a depth-first traversal of the graph and writing a line for + each unique node encountered. Non-tree edges are written to the right of + each node, and connection to a non-tree edge is indicated with an ellipsis. + This representation works best when the input graph is a forest, but any + graph can be represented. + + Parameters + ---------- + graph : nx.DiGraph | nx.Graph + Graph to represent + + path : string or file or callable or None + Filename or file handle for data output. + if a function, then it will be called for each generated line. + if None, this will default to "sys.stdout.write" + + with_labels : bool | str + If True will use the "label" attribute of a node to display if it + exists otherwise it will use the node value itself. If given as a + string, then that attribute name will be used instead of "label". + Defaults to True. + + sources : List + Specifies which nodes to start traversal from. Note: nodes that are not + reachable from one of these sources may not be shown. If unspecified, + the minimal set of nodes needed to reach all others will be used. + + max_depth : int | None + The maximum depth to traverse before stopping. Defaults to None. + + ascii_only : Boolean + If True only ASCII characters are used to construct the visualization + + end : string + The line ending character + + vertical_chains : Boolean + If True, chains of nodes will be drawn vertically when possible. + + Examples + -------- + >>> graph = nx.balanced_tree(r=2, h=2, create_using=nx.DiGraph) + >>> nx.write_network_text(graph) + ╙── 0 + ├─╼ 1 + │ ├─╼ 3 + │ └─╼ 4 + └─╼ 2 + ├─╼ 5 + └─╼ 6 + + >>> # A near tree with one non-tree edge + >>> graph.add_edge(5, 1) + >>> nx.write_network_text(graph) + ╙── 0 + ├─╼ 1 ╾ 5 + │ ├─╼ 3 + │ └─╼ 4 + └─╼ 2 + ├─╼ 5 + │ └─╼ ... + └─╼ 6 + + >>> graph = nx.cycle_graph(5) + >>> nx.write_network_text(graph) + ╙── 0 + ├── 1 + │ └── 2 + │ └── 3 + │ └── 4 ─ 0 + └── ... + + >>> graph = nx.cycle_graph(5, nx.DiGraph) + >>> nx.write_network_text(graph, vertical_chains=True) + ╙── 0 ╾ 4 + ╽ + 1 + ╽ + 2 + ╽ + 3 + ╽ + 4 + └─╼ ... + + >>> nx.write_network_text(graph, vertical_chains=True, ascii_only=True) + +-- 0 <- 4 + ! + 1 + ! + 2 + ! + 3 + ! + 4 + L-> ... + + >>> graph = nx.generators.barbell_graph(4, 2) + >>> nx.write_network_text(graph, vertical_chains=False) + ╙── 4 + ├── 5 + │ └── 6 + │ ├── 7 + │ │ ├── 8 ─ 6 + │ │ │ └── 9 ─ 6, 7 + │ │ └── ... + │ └── ... + └── 3 + ├── 0 + │ ├── 1 ─ 3 + │ │ └── 2 ─ 0, 3 + │ └── ... + └── ... + >>> nx.write_network_text(graph, vertical_chains=True) + ╙── 4 + ├── 5 + │ │ + │ 6 + │ ├── 7 + │ │ ├── 8 ─ 6 + │ │ │ │ + │ │ │ 9 ─ 6, 7 + │ │ └── ... + │ └── ... + └── 3 + ├── 0 + │ ├── 1 ─ 3 + │ │ │ + │ │ 2 ─ 0, 3 + │ └── ... + └── ... + + >>> graph = nx.complete_graph(5, create_using=nx.Graph) + >>> nx.write_network_text(graph) + ╙── 0 + ├── 1 + │ ├── 2 ─ 0 + │ │ ├── 3 ─ 0, 1 + │ │ │ └── 4 ─ 0, 1, 2 + │ │ └── ... + │ └── ... + └── ... + + >>> graph = nx.complete_graph(3, create_using=nx.DiGraph) + >>> nx.write_network_text(graph) + ╙── 0 ╾ 1, 2 + ├─╼ 1 ╾ 2 + │ ├─╼ 2 ╾ 0 + │ │ └─╼ ... + │ └─╼ ... + └─╼ ... + """ + if path is None: + # The path is unspecified, write to stdout + _write = sys.stdout.write + elif hasattr(path, "write"): + # The path is already an open file + _write = path.write + elif callable(path): + # The path is a custom callable + _write = path + else: + raise TypeError(type(path)) + + for line in generate_network_text( + graph, + with_labels=with_labels, + sources=sources, + max_depth=max_depth, + ascii_only=ascii_only, + vertical_chains=vertical_chains, + ): + _write(line + end) + + +def _find_sources(graph): + """ + Determine a minimal set of nodes such that the entire graph is reachable + """ + # For each connected part of the graph, choose at least + # one node as a starting point, preferably without a parent + if graph.is_directed(): + # Choose one node from each SCC with minimum in_degree + sccs = list(nx.strongly_connected_components(graph)) + # condensing the SCCs forms a dag, the nodes in this graph with + # 0 in-degree correspond to the SCCs from which the minimum set + # of nodes from which all other nodes can be reached. + scc_graph = nx.condensation(graph, sccs) + supernode_to_nodes = {sn: [] for sn in scc_graph.nodes()} + # Note: the order of mapping differs between pypy and cpython + # so we have to loop over graph nodes for consistency + mapping = scc_graph.graph["mapping"] + for n in graph.nodes: + sn = mapping[n] + supernode_to_nodes[sn].append(n) + sources = [] + for sn in scc_graph.nodes(): + if scc_graph.in_degree[sn] == 0: + scc = supernode_to_nodes[sn] + node = min(scc, key=lambda n: graph.in_degree[n]) + sources.append(node) + else: + # For undirected graph, the entire graph will be reachable as + # long as we consider one node from every connected component + sources = [ + min(cc, key=lambda n: graph.degree[n]) + for cc in nx.connected_components(graph) + ] + sources = sorted(sources, key=lambda n: graph.degree[n]) + return sources + + +def _parse_network_text(lines): + """Reconstructs a graph from a network text representation. + + This is mainly used for testing. Network text is for display, not + serialization, as such this cannot parse all network text representations + because node labels can be ambiguous with the glyphs and indentation used + to represent edge structure. Additionally, there is no way to determine if + disconnected graphs were originally directed or undirected. + + Parameters + ---------- + lines : list or iterator of strings + Input data in network text format + + Returns + ------- + G: NetworkX graph + The graph corresponding to the lines in network text format. + """ + from itertools import chain + from typing import Any, NamedTuple + + class ParseStackFrame(NamedTuple): + node: Any + indent: int + has_vertical_child: int | None + + initial_line_iter = iter(lines) + + is_ascii = None + is_directed = None + + ############## + # Initial Pass + ############## + + # Do an initial pass over the lines to determine what type of graph it is. + # Remember what these lines were, so we can reiterate over them in the + # parsing pass. + initial_lines = [] + try: + first_line = next(initial_line_iter) + except StopIteration: + ... + else: + initial_lines.append(first_line) + # The first character indicates if it is an ASCII or UTF graph + first_char = first_line[0] + if first_char in { + UtfBaseGlyphs.empty, + UtfBaseGlyphs.newtree_mid[0], + UtfBaseGlyphs.newtree_last[0], + }: + is_ascii = False + elif first_char in { + AsciiBaseGlyphs.empty, + AsciiBaseGlyphs.newtree_mid[0], + AsciiBaseGlyphs.newtree_last[0], + }: + is_ascii = True + else: + raise AssertionError(f"Unexpected first character: {first_char}") + + if is_ascii: + directed_glyphs = AsciiDirectedGlyphs.as_dict() + undirected_glyphs = AsciiUndirectedGlyphs.as_dict() + else: + directed_glyphs = UtfDirectedGlyphs.as_dict() + undirected_glyphs = UtfUndirectedGlyphs.as_dict() + + # For both directed / undirected glyphs, determine which glyphs never + # appear as substrings in the other undirected / directed glyphs. Glyphs + # with this property unambiguously indicates if a graph is directed / + # undirected. + directed_items = set(directed_glyphs.values()) + undirected_items = set(undirected_glyphs.values()) + unambiguous_directed_items = [] + for item in directed_items: + other_items = undirected_items + other_supersets = [other for other in other_items if item in other] + if not other_supersets: + unambiguous_directed_items.append(item) + unambiguous_undirected_items = [] + for item in undirected_items: + other_items = directed_items + other_supersets = [other for other in other_items if item in other] + if not other_supersets: + unambiguous_undirected_items.append(item) + + for line in initial_line_iter: + initial_lines.append(line) + if any(item in line for item in unambiguous_undirected_items): + is_directed = False + break + elif any(item in line for item in unambiguous_directed_items): + is_directed = True + break + + if is_directed is None: + # Not enough information to determine, choose undirected by default + is_directed = False + + glyphs = directed_glyphs if is_directed else undirected_glyphs + + # the backedge symbol by itself can be ambiguous, but with spaces around it + # becomes unambiguous. + backedge_symbol = " " + glyphs["backedge"] + " " + + # Reconstruct an iterator over all of the lines. + parsing_line_iter = chain(initial_lines, initial_line_iter) + + ############## + # Parsing Pass + ############## + + edges = [] + nodes = [] + is_empty = None + + noparent = object() # sentinel value + + # keep a stack of previous nodes that could be parents of subsequent nodes + stack = [ParseStackFrame(noparent, -1, None)] + + for line in parsing_line_iter: + if line == glyphs["empty"]: + # If the line is the empty glyph, we are done. + # There shouldn't be anything else after this. + is_empty = True + continue + + if backedge_symbol in line: + # This line has one or more backedges, separate those out + node_part, backedge_part = line.split(backedge_symbol) + backedge_nodes = [u.strip() for u in backedge_part.split(", ")] + # Now the node can be parsed + node_part = node_part.rstrip() + prefix, node = node_part.rsplit(" ", 1) + node = node.strip() + # Add the backedges to the edge list + edges.extend([(u, node) for u in backedge_nodes]) + else: + # No backedge, the tail of this line is the node + prefix, node = line.rsplit(" ", 1) + node = node.strip() + + prev = stack.pop() + + if node in glyphs["vertical_edge"]: + # Previous node is still the previous node, but we know it will + # have exactly one child, which will need to have its nesting level + # adjusted. + modified_prev = ParseStackFrame( + prev.node, + prev.indent, + True, + ) + stack.append(modified_prev) + continue + + # The length of the string before the node characters give us a hint + # about our nesting level. The only case where this doesn't work is + # when there are vertical chains, which is handled explicitly. + indent = len(prefix) + curr = ParseStackFrame(node, indent, None) + + if prev.has_vertical_child: + # In this case we know prev must be the parent of our current line, + # so we don't have to search the stack. (which is good because the + # indentation check wouldn't work in this case). + ... + else: + # If the previous node nesting-level is greater than the current + # nodes nesting-level than the previous node was the end of a path, + # and is not our parent. We can safely pop nodes off the stack + # until we find one with a comparable nesting-level, which is our + # parent. + while curr.indent <= prev.indent: + prev = stack.pop() + + if node == "...": + # The current previous node is no longer a valid parent, + # keep it popped from the stack. + stack.append(prev) + else: + # The previous and current nodes may still be parents, so add them + # back onto the stack. + stack.append(prev) + stack.append(curr) + + # Add the node and the edge to its parent to the node / edge lists. + nodes.append(curr.node) + if prev.node is not noparent: + edges.append((prev.node, curr.node)) + + if is_empty: + # Sanity check + assert len(nodes) == 0 + + # Reconstruct the graph + cls = nx.DiGraph if is_directed else nx.Graph + new = cls() + new.add_nodes_from(nodes) + new.add_edges_from(edges) + return new diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46be498d8f4e5e13509c4875c079c0e0becdec7a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_all_random_functions.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_all_random_functions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdf2fa874474a3864b7e86fa978eca4f53f82c4d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_all_random_functions.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e9be9d076577cdfabd2113a7ba4ecfbb368626c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_numpy.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_numpy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eae82a3fb9de4053d7e67388c5e961d14f8cef4d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_numpy.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_pandas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_pandas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b5d89314fd5931a3f28e76ec39d58a0486d5265 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_pandas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_scipy.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_scipy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4f9f9aa93d59aa99aa148ceabdf40c36afbb063 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_convert_scipy.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_exceptions.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c148c0e0465e18ce701d08bc76bd3980318184ac Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_exceptions.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_import.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_import.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9850a60dc1709c363831465bfc6ef8d1c5f2d155 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_import.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_lazy_imports.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_lazy_imports.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f72649ba959ec01e0c191171a9d15470e248015a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_lazy_imports.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_relabel.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_relabel.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cfbacff5e833161cbb820085a6af25876587d5c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_relabel.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_removed_functions_exception_messages.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_removed_functions_exception_messages.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3160fa654040035efb66928b0454ea3042d47bb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/__pycache__/test_removed_functions_exception_messages.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_all_random_functions.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_all_random_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..fb3a73d0666b01bd1a90a4759670f912590aaf6b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_all_random_functions.py @@ -0,0 +1,248 @@ +import random + +import pytest + +import networkx as nx +from networkx.algorithms import approximation as approx +from networkx.algorithms import threshold + +np = pytest.importorskip("numpy") + +progress = 0 + +# store the random numbers after setting a global seed +np.random.seed(42) +np_rv = np.random.rand() +random.seed(42) +py_rv = random.random() + + +def t(f, *args, **kwds): + """call one function and check if global RNG changed""" + global progress + progress += 1 + print(progress, ",", end="") + + f(*args, **kwds) + + after_np_rv = np.random.rand() + # if np_rv != after_np_rv: + # print(np_rv, after_np_rv, "don't match np!") + assert np_rv == after_np_rv + np.random.seed(42) + + after_py_rv = random.random() + # if py_rv != after_py_rv: + # print(py_rv, after_py_rv, "don't match py!") + assert py_rv == after_py_rv + random.seed(42) + + +def run_all_random_functions(seed): + n = 20 + m = 10 + k = l = 2 + s = v = 10 + p = q = p1 = p2 = p_in = p_out = 0.4 + alpha = radius = theta = 0.75 + sizes = (20, 20, 10) + colors = [1, 2, 3] + G = nx.barbell_graph(12, 20) + H = nx.cycle_graph(3) + H.add_weighted_edges_from((u, v, 0.2) for u, v in H.edges) + deg_sequence = [3, 2, 1, 3, 2, 1, 3, 2, 1, 2, 1, 2, 1] + in_degree_sequence = w = sequence = aseq = bseq = deg_sequence + + # print("starting...") + t(nx.maximal_independent_set, G, seed=seed) + t(nx.rich_club_coefficient, G, seed=seed, normalized=False) + t(nx.random_reference, G, seed=seed) + t(nx.lattice_reference, G, seed=seed) + t(nx.sigma, G, 1, 2, seed=seed) + t(nx.omega, G, 1, 2, seed=seed) + # print("out of smallworld.py") + t(nx.double_edge_swap, G, seed=seed) + # print("starting connected_double_edge_swap") + t(nx.connected_double_edge_swap, nx.complete_graph(9), seed=seed) + # print("ending connected_double_edge_swap") + t(nx.random_layout, G, seed=seed) + t(nx.fruchterman_reingold_layout, G, seed=seed) + t(nx.algebraic_connectivity, G, seed=seed) + t(nx.fiedler_vector, G, seed=seed) + t(nx.spectral_ordering, G, seed=seed) + # print('starting average_clustering') + t(approx.average_clustering, G, seed=seed) + t(approx.simulated_annealing_tsp, H, "greedy", source=1, seed=seed) + t(approx.threshold_accepting_tsp, H, "greedy", source=1, seed=seed) + t( + approx.traveling_salesman_problem, + H, + method=lambda G, weight: approx.simulated_annealing_tsp( + G, "greedy", weight, seed=seed + ), + ) + t( + approx.traveling_salesman_problem, + H, + method=lambda G, weight: approx.threshold_accepting_tsp( + G, "greedy", weight, seed=seed + ), + ) + t(nx.betweenness_centrality, G, seed=seed) + t(nx.edge_betweenness_centrality, G, seed=seed) + t(nx.approximate_current_flow_betweenness_centrality, G, seed=seed) + # print("kernighan") + t(nx.algorithms.community.kernighan_lin_bisection, G, seed=seed) + # nx.algorithms.community.asyn_lpa_communities(G, seed=seed) + t(nx.algorithms.tree.greedy_branching, G, seed=seed) + # print('done with graph argument functions') + + t(nx.spectral_graph_forge, G, alpha, seed=seed) + t(nx.algorithms.community.asyn_fluidc, G, k, max_iter=1, seed=seed) + t( + nx.algorithms.connectivity.edge_augmentation.greedy_k_edge_augmentation, + G, + k, + seed=seed, + ) + t(nx.algorithms.coloring.strategy_random_sequential, G, colors, seed=seed) + + t(nx.configuration_model, deg_sequence, seed=seed) + t( + nx.directed_configuration_model, + in_degree_sequence, + in_degree_sequence, + seed=seed, + ) + t(nx.expected_degree_graph, w, seed=seed) + t(nx.random_degree_sequence_graph, sequence, seed=seed) + joint_degrees = { + 1: {4: 1}, + 2: {2: 2, 3: 2, 4: 2}, + 3: {2: 2, 4: 1}, + 4: {1: 1, 2: 2, 3: 1}, + } + t(nx.joint_degree_graph, joint_degrees, seed=seed) + joint_degree_sequence = [ + (1, 0), + (1, 0), + (1, 0), + (2, 0), + (1, 0), + (2, 1), + (0, 1), + (0, 1), + ] + t(nx.random_clustered_graph, joint_degree_sequence, seed=seed) + constructor = [(3, 3, 0.5), (10, 10, 0.7)] + t(nx.random_shell_graph, constructor, seed=seed) + mapping = {1: 0.4, 2: 0.3, 3: 0.3} + t(nx.utils.random_weighted_sample, mapping, k, seed=seed) + t(nx.utils.weighted_choice, mapping, seed=seed) + t(nx.algorithms.bipartite.configuration_model, aseq, bseq, seed=seed) + t(nx.algorithms.bipartite.preferential_attachment_graph, aseq, p, seed=seed) + + def kernel_integral(u, w, z): + return z - w + + t(nx.random_kernel_graph, n, kernel_integral, seed=seed) + + sizes = [75, 75, 300] + probs = [[0.25, 0.05, 0.02], [0.05, 0.35, 0.07], [0.02, 0.07, 0.40]] + t(nx.stochastic_block_model, sizes, probs, seed=seed) + t(nx.random_partition_graph, sizes, p_in, p_out, seed=seed) + + # print("starting generator functions") + t(threshold.random_threshold_sequence, n, p, seed=seed) + t(nx.tournament.random_tournament, n, seed=seed) + t(nx.relaxed_caveman_graph, l, k, p, seed=seed) + t(nx.planted_partition_graph, l, k, p_in, p_out, seed=seed) + t(nx.gaussian_random_partition_graph, n, s, v, p_in, p_out, seed=seed) + t(nx.gn_graph, n, seed=seed) + t(nx.gnr_graph, n, p, seed=seed) + t(nx.gnc_graph, n, seed=seed) + t(nx.scale_free_graph, n, seed=seed) + t(nx.directed.random_uniform_k_out_graph, n, k, seed=seed) + t(nx.random_k_out_graph, n, k, alpha, seed=seed) + N = 1000 + t(nx.partial_duplication_graph, N, n, p, q, seed=seed) + t(nx.duplication_divergence_graph, n, p, seed=seed) + t(nx.random_geometric_graph, n, radius, seed=seed) + t(nx.soft_random_geometric_graph, n, radius, seed=seed) + t(nx.geographical_threshold_graph, n, theta, seed=seed) + t(nx.waxman_graph, n, seed=seed) + t(nx.navigable_small_world_graph, n, seed=seed) + t(nx.thresholded_random_geometric_graph, n, radius, theta, seed=seed) + t(nx.uniform_random_intersection_graph, n, m, p, seed=seed) + t(nx.k_random_intersection_graph, n, m, k, seed=seed) + + t(nx.general_random_intersection_graph, n, 2, [0.1, 0.5], seed=seed) + t(nx.fast_gnp_random_graph, n, p, seed=seed) + t(nx.gnp_random_graph, n, p, seed=seed) + t(nx.dense_gnm_random_graph, n, m, seed=seed) + t(nx.gnm_random_graph, n, m, seed=seed) + t(nx.newman_watts_strogatz_graph, n, k, p, seed=seed) + t(nx.watts_strogatz_graph, n, k, p, seed=seed) + t(nx.connected_watts_strogatz_graph, n, k, p, seed=seed) + t(nx.random_regular_graph, 3, n, seed=seed) + t(nx.barabasi_albert_graph, n, m, seed=seed) + t(nx.extended_barabasi_albert_graph, n, m, p, q, seed=seed) + t(nx.powerlaw_cluster_graph, n, m, p, seed=seed) + t(nx.random_lobster_graph, n, p1, p2, seed=seed) + t(nx.random_powerlaw_tree, 5, seed=seed, tries=5000) + t(nx.random_powerlaw_tree_sequence, 5, seed=seed, tries=5000) + t(nx.random_labeled_tree, n, seed=seed) + t(nx.utils.powerlaw_sequence, n, seed=seed) + t(nx.utils.zipf_rv, 2.3, seed=seed) + cdist = [0.2, 0.4, 0.5, 0.7, 0.9, 1.0] + t(nx.utils.discrete_sequence, n, cdistribution=cdist, seed=seed) + t(nx.algorithms.bipartite.random_graph, n, m, p, seed=seed) + t(nx.algorithms.bipartite.gnmk_random_graph, n, m, k, seed=seed) + LFR = nx.generators.LFR_benchmark_graph + t( + LFR, + 25, + 3, + 1.5, + 0.1, + average_degree=3, + min_community=10, + seed=seed, + max_community=20, + ) + t(nx.random_internet_as_graph, n, seed=seed) + # print("done") + + +# choose to test an integer seed, or whether a single RNG can be everywhere +# np_rng = np.random.RandomState(14) +# seed = np_rng +# seed = 14 + + +@pytest.mark.slow +# print("NetworkX Version:", nx.__version__) +def test_rng_interface(): + global progress + + # try different kinds of seeds + for seed in [14, np.random.RandomState(14)]: + np.random.seed(42) + random.seed(42) + run_all_random_functions(seed) + progress = 0 + + # check that both global RNGs are unaffected + after_np_rv = np.random.rand() + # if np_rv != after_np_rv: + # print(np_rv, after_np_rv, "don't match np!") + assert np_rv == after_np_rv + after_py_rv = random.random() + # if py_rv != after_py_rv: + # print(py_rv, after_py_rv, "don't match py!") + assert py_rv == after_py_rv + + +# print("\nDone testing seed:", seed) + +# test_rng_interface() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert.py new file mode 100644 index 0000000000000000000000000000000000000000..44bed9438945a39bb5eb85477301f58cfcd70cf0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert.py @@ -0,0 +1,321 @@ +import pytest + +import networkx as nx +from networkx.convert import ( + from_dict_of_dicts, + from_dict_of_lists, + to_dict_of_dicts, + to_dict_of_lists, + to_networkx_graph, +) +from networkx.generators.classic import barbell_graph, cycle_graph +from networkx.utils import edges_equal, graphs_equal, nodes_equal + + +class TestConvert: + def edgelists_equal(self, e1, e2): + return sorted(sorted(e) for e in e1) == sorted(sorted(e) for e in e2) + + def test_simple_graphs(self): + for dest, source in [ + (to_dict_of_dicts, from_dict_of_dicts), + (to_dict_of_lists, from_dict_of_lists), + ]: + G = barbell_graph(10, 3) + G.graph = {} + dod = dest(G) + + # Dict of [dicts, lists] + GG = source(dod) + assert graphs_equal(G, GG) + GW = to_networkx_graph(dod) + assert graphs_equal(G, GW) + GI = nx.Graph(dod) + assert graphs_equal(G, GI) + + # With nodelist keyword + P4 = nx.path_graph(4) + P3 = nx.path_graph(3) + P4.graph = {} + P3.graph = {} + dod = dest(P4, nodelist=[0, 1, 2]) + Gdod = nx.Graph(dod) + assert graphs_equal(Gdod, P3) + + def test_exceptions(self): + # NX graph + class G: + adj = None + + pytest.raises(nx.NetworkXError, to_networkx_graph, G) + + # pygraphviz agraph + class G: + is_strict = None + + pytest.raises(nx.NetworkXError, to_networkx_graph, G) + + # Dict of [dicts, lists] + G = {"a": 0} + pytest.raises(TypeError, to_networkx_graph, G) + + # list or generator of edges + class G: + next = None + + pytest.raises(nx.NetworkXError, to_networkx_graph, G) + + # no match + pytest.raises(nx.NetworkXError, to_networkx_graph, "a") + + def test_digraphs(self): + for dest, source in [ + (to_dict_of_dicts, from_dict_of_dicts), + (to_dict_of_lists, from_dict_of_lists), + ]: + G = cycle_graph(10) + + # Dict of [dicts, lists] + dod = dest(G) + GG = source(dod) + assert nodes_equal(sorted(G.nodes()), sorted(GG.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GG.edges())) + GW = to_networkx_graph(dod) + assert nodes_equal(sorted(G.nodes()), sorted(GW.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GW.edges())) + GI = nx.Graph(dod) + assert nodes_equal(sorted(G.nodes()), sorted(GI.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GI.edges())) + + G = cycle_graph(10, create_using=nx.DiGraph) + dod = dest(G) + GG = source(dod, create_using=nx.DiGraph) + assert sorted(G.nodes()) == sorted(GG.nodes()) + assert sorted(G.edges()) == sorted(GG.edges()) + GW = to_networkx_graph(dod, create_using=nx.DiGraph) + assert sorted(G.nodes()) == sorted(GW.nodes()) + assert sorted(G.edges()) == sorted(GW.edges()) + GI = nx.DiGraph(dod) + assert sorted(G.nodes()) == sorted(GI.nodes()) + assert sorted(G.edges()) == sorted(GI.edges()) + + def test_graph(self): + g = nx.cycle_graph(10) + G = nx.Graph() + G.add_nodes_from(g) + G.add_weighted_edges_from((u, v, u) for u, v in g.edges()) + + # Dict of dicts + dod = to_dict_of_dicts(G) + GG = from_dict_of_dicts(dod, create_using=nx.Graph) + assert nodes_equal(sorted(G.nodes()), sorted(GG.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GG.edges())) + GW = to_networkx_graph(dod, create_using=nx.Graph) + assert nodes_equal(sorted(G.nodes()), sorted(GW.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GW.edges())) + GI = nx.Graph(dod) + assert sorted(G.nodes()) == sorted(GI.nodes()) + assert sorted(G.edges()) == sorted(GI.edges()) + + # Dict of lists + dol = to_dict_of_lists(G) + GG = from_dict_of_lists(dol, create_using=nx.Graph) + # dict of lists throws away edge data so set it to none + enone = [(u, v, {}) for (u, v, d) in G.edges(data=True)] + assert nodes_equal(sorted(G.nodes()), sorted(GG.nodes())) + assert edges_equal(enone, sorted(GG.edges(data=True))) + GW = to_networkx_graph(dol, create_using=nx.Graph) + assert nodes_equal(sorted(G.nodes()), sorted(GW.nodes())) + assert edges_equal(enone, sorted(GW.edges(data=True))) + GI = nx.Graph(dol) + assert nodes_equal(sorted(G.nodes()), sorted(GI.nodes())) + assert edges_equal(enone, sorted(GI.edges(data=True))) + + def test_with_multiedges_self_loops(self): + G = cycle_graph(10) + XG = nx.Graph() + XG.add_nodes_from(G) + XG.add_weighted_edges_from((u, v, u) for u, v in G.edges()) + XGM = nx.MultiGraph() + XGM.add_nodes_from(G) + XGM.add_weighted_edges_from((u, v, u) for u, v in G.edges()) + XGM.add_edge(0, 1, weight=2) # multiedge + XGS = nx.Graph() + XGS.add_nodes_from(G) + XGS.add_weighted_edges_from((u, v, u) for u, v in G.edges()) + XGS.add_edge(0, 0, weight=100) # self loop + + # Dict of dicts + # with self loops, OK + dod = to_dict_of_dicts(XGS) + GG = from_dict_of_dicts(dod, create_using=nx.Graph) + assert nodes_equal(XGS.nodes(), GG.nodes()) + assert edges_equal(XGS.edges(), GG.edges()) + GW = to_networkx_graph(dod, create_using=nx.Graph) + assert nodes_equal(XGS.nodes(), GW.nodes()) + assert edges_equal(XGS.edges(), GW.edges()) + GI = nx.Graph(dod) + assert nodes_equal(XGS.nodes(), GI.nodes()) + assert edges_equal(XGS.edges(), GI.edges()) + + # Dict of lists + # with self loops, OK + dol = to_dict_of_lists(XGS) + GG = from_dict_of_lists(dol, create_using=nx.Graph) + # dict of lists throws away edge data so set it to none + enone = [(u, v, {}) for (u, v, d) in XGS.edges(data=True)] + assert nodes_equal(sorted(XGS.nodes()), sorted(GG.nodes())) + assert edges_equal(enone, sorted(GG.edges(data=True))) + GW = to_networkx_graph(dol, create_using=nx.Graph) + assert nodes_equal(sorted(XGS.nodes()), sorted(GW.nodes())) + assert edges_equal(enone, sorted(GW.edges(data=True))) + GI = nx.Graph(dol) + assert nodes_equal(sorted(XGS.nodes()), sorted(GI.nodes())) + assert edges_equal(enone, sorted(GI.edges(data=True))) + + # Dict of dicts + # with multiedges, OK + dod = to_dict_of_dicts(XGM) + GG = from_dict_of_dicts(dod, create_using=nx.MultiGraph, multigraph_input=True) + assert nodes_equal(sorted(XGM.nodes()), sorted(GG.nodes())) + assert edges_equal(sorted(XGM.edges()), sorted(GG.edges())) + GW = to_networkx_graph(dod, create_using=nx.MultiGraph, multigraph_input=True) + assert nodes_equal(sorted(XGM.nodes()), sorted(GW.nodes())) + assert edges_equal(sorted(XGM.edges()), sorted(GW.edges())) + GI = nx.MultiGraph(dod) + assert nodes_equal(sorted(XGM.nodes()), sorted(GI.nodes())) + assert sorted(XGM.edges()) == sorted(GI.edges()) + GE = from_dict_of_dicts(dod, create_using=nx.MultiGraph, multigraph_input=False) + assert nodes_equal(sorted(XGM.nodes()), sorted(GE.nodes())) + assert sorted(XGM.edges()) != sorted(GE.edges()) + GI = nx.MultiGraph(XGM) + assert nodes_equal(sorted(XGM.nodes()), sorted(GI.nodes())) + assert edges_equal(sorted(XGM.edges()), sorted(GI.edges())) + GM = nx.MultiGraph(G) + assert nodes_equal(sorted(GM.nodes()), sorted(G.nodes())) + assert edges_equal(sorted(GM.edges()), sorted(G.edges())) + + # Dict of lists + # with multiedges, OK, but better write as DiGraph else you'll + # get double edges + dol = to_dict_of_lists(G) + GG = from_dict_of_lists(dol, create_using=nx.MultiGraph) + assert nodes_equal(sorted(G.nodes()), sorted(GG.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GG.edges())) + GW = to_networkx_graph(dol, create_using=nx.MultiGraph) + assert nodes_equal(sorted(G.nodes()), sorted(GW.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GW.edges())) + GI = nx.MultiGraph(dol) + assert nodes_equal(sorted(G.nodes()), sorted(GI.nodes())) + assert edges_equal(sorted(G.edges()), sorted(GI.edges())) + + def test_edgelists(self): + P = nx.path_graph(4) + e = [(0, 1), (1, 2), (2, 3)] + G = nx.Graph(e) + assert nodes_equal(sorted(G.nodes()), sorted(P.nodes())) + assert edges_equal(sorted(G.edges()), sorted(P.edges())) + assert edges_equal(sorted(G.edges(data=True)), sorted(P.edges(data=True))) + + e = [(0, 1, {}), (1, 2, {}), (2, 3, {})] + G = nx.Graph(e) + assert nodes_equal(sorted(G.nodes()), sorted(P.nodes())) + assert edges_equal(sorted(G.edges()), sorted(P.edges())) + assert edges_equal(sorted(G.edges(data=True)), sorted(P.edges(data=True))) + + e = ((n, n + 1) for n in range(3)) + G = nx.Graph(e) + assert nodes_equal(sorted(G.nodes()), sorted(P.nodes())) + assert edges_equal(sorted(G.edges()), sorted(P.edges())) + assert edges_equal(sorted(G.edges(data=True)), sorted(P.edges(data=True))) + + def test_directed_to_undirected(self): + edges1 = [(0, 1), (1, 2), (2, 0)] + edges2 = [(0, 1), (1, 2), (0, 2)] + assert self.edgelists_equal(nx.Graph(nx.DiGraph(edges1)).edges(), edges1) + assert self.edgelists_equal(nx.Graph(nx.DiGraph(edges2)).edges(), edges1) + assert self.edgelists_equal(nx.MultiGraph(nx.DiGraph(edges1)).edges(), edges1) + assert self.edgelists_equal(nx.MultiGraph(nx.DiGraph(edges2)).edges(), edges1) + + assert self.edgelists_equal( + nx.MultiGraph(nx.MultiDiGraph(edges1)).edges(), edges1 + ) + assert self.edgelists_equal( + nx.MultiGraph(nx.MultiDiGraph(edges2)).edges(), edges1 + ) + + assert self.edgelists_equal(nx.Graph(nx.MultiDiGraph(edges1)).edges(), edges1) + assert self.edgelists_equal(nx.Graph(nx.MultiDiGraph(edges2)).edges(), edges1) + + def test_attribute_dict_integrity(self): + # we must not replace dict-like graph data structures with dicts + G = nx.Graph() + G.add_nodes_from("abc") + H = to_networkx_graph(G, create_using=nx.Graph) + assert list(H.nodes) == list(G.nodes) + H = nx.DiGraph(G) + assert list(H.nodes) == list(G.nodes) + + def test_to_edgelist(self): + G = nx.Graph([(1, 1)]) + elist = nx.to_edgelist(G, nodelist=list(G)) + assert edges_equal(G.edges(data=True), elist) + + def test_custom_node_attr_dict_safekeeping(self): + class custom_dict(dict): + pass + + class Custom(nx.Graph): + node_attr_dict_factory = custom_dict + + g = nx.Graph() + g.add_node(1, weight=1) + + h = Custom(g) + assert isinstance(g._node[1], dict) + assert isinstance(h._node[1], custom_dict) + + # this raise exception + # h._node.update((n, dd.copy()) for n, dd in g.nodes.items()) + # assert isinstance(h._node[1], custom_dict) + + +@pytest.mark.parametrize( + "edgelist", + ( + # Graph with no edge data + [(0, 1), (1, 2)], + # Graph with edge data + [(0, 1, {"weight": 1.0}), (1, 2, {"weight": 2.0})], + ), +) +def test_to_dict_of_dicts_with_edgedata_param(edgelist): + G = nx.Graph() + G.add_edges_from(edgelist) + # Innermost dict value == edge_data when edge_data != None. + # In the case when G has edge data, it is overwritten + expected = {0: {1: 10}, 1: {0: 10, 2: 10}, 2: {1: 10}} + assert nx.to_dict_of_dicts(G, edge_data=10) == expected + + +def test_to_dict_of_dicts_with_edgedata_and_nodelist(): + G = nx.path_graph(5) + nodelist = [2, 3, 4] + expected = {2: {3: 10}, 3: {2: 10, 4: 10}, 4: {3: 10}} + assert nx.to_dict_of_dicts(G, nodelist=nodelist, edge_data=10) == expected + + +def test_to_dict_of_dicts_with_edgedata_multigraph(): + """Multi edge data overwritten when edge_data != None""" + G = nx.MultiGraph() + G.add_edge(0, 1, key="a") + G.add_edge(0, 1, key="b") + # Multi edge data lost when edge_data is not None + expected = {0: {1: 10}, 1: {0: 10}} + assert nx.to_dict_of_dicts(G, edge_data=10) == expected + + +def test_to_networkx_graph_non_edgelist(): + invalid_edgelist = [1, 2, 3] + with pytest.raises(nx.NetworkXError, match="Input is not a valid edge list"): + nx.to_networkx_graph(invalid_edgelist) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_numpy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_numpy.py new file mode 100644 index 0000000000000000000000000000000000000000..0a554fd4b2d5e00e69a0537fbd04e877a95b6593 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_numpy.py @@ -0,0 +1,531 @@ +import itertools + +import pytest + +import networkx as nx +from networkx.utils import graphs_equal + +np = pytest.importorskip("numpy") +npt = pytest.importorskip("numpy.testing") + + +class TestConvertNumpyArray: + def setup_method(self): + self.G1 = nx.barbell_graph(10, 3) + self.G2 = nx.cycle_graph(10, create_using=nx.DiGraph) + self.G3 = self.create_weighted(nx.Graph()) + self.G4 = self.create_weighted(nx.DiGraph()) + + def create_weighted(self, G): + g = nx.cycle_graph(4) + G.add_nodes_from(g) + G.add_weighted_edges_from((u, v, 10 + u) for u, v in g.edges()) + return G + + def assert_equal(self, G1, G2): + assert sorted(G1.nodes()) == sorted(G2.nodes()) + assert sorted(G1.edges()) == sorted(G2.edges()) + + def identity_conversion(self, G, A, create_using): + assert A.sum() > 0 + GG = nx.from_numpy_array(A, create_using=create_using) + self.assert_equal(G, GG) + GW = nx.to_networkx_graph(A, create_using=create_using) + self.assert_equal(G, GW) + GI = nx.empty_graph(0, create_using).__class__(A) + self.assert_equal(G, GI) + + def test_shape(self): + "Conversion from non-square array." + A = np.array([[1, 2, 3], [4, 5, 6]]) + pytest.raises(nx.NetworkXError, nx.from_numpy_array, A) + + def test_identity_graph_array(self): + "Conversion from graph to array to graph." + A = nx.to_numpy_array(self.G1) + self.identity_conversion(self.G1, A, nx.Graph()) + + def test_identity_digraph_array(self): + """Conversion from digraph to array to digraph.""" + A = nx.to_numpy_array(self.G2) + self.identity_conversion(self.G2, A, nx.DiGraph()) + + def test_identity_weighted_graph_array(self): + """Conversion from weighted graph to array to weighted graph.""" + A = nx.to_numpy_array(self.G3) + self.identity_conversion(self.G3, A, nx.Graph()) + + def test_identity_weighted_digraph_array(self): + """Conversion from weighted digraph to array to weighted digraph.""" + A = nx.to_numpy_array(self.G4) + self.identity_conversion(self.G4, A, nx.DiGraph()) + + def test_nodelist(self): + """Conversion from graph to array to graph with nodelist.""" + P4 = nx.path_graph(4) + P3 = nx.path_graph(3) + nodelist = list(P3) + A = nx.to_numpy_array(P4, nodelist=nodelist) + GA = nx.Graph(A) + self.assert_equal(GA, P3) + + # Make nodelist ambiguous by containing duplicates. + nodelist += [nodelist[0]] + pytest.raises(nx.NetworkXError, nx.to_numpy_array, P3, nodelist=nodelist) + + # Make nodelist invalid by including nonexistent nodes + nodelist = [-1, 0, 1] + with pytest.raises( + nx.NetworkXError, + match=f"Nodes {nodelist - P3.nodes} in nodelist is not in G", + ): + nx.to_numpy_array(P3, nodelist=nodelist) + + def test_weight_keyword(self): + WP4 = nx.Graph() + WP4.add_edges_from((n, n + 1, {"weight": 0.5, "other": 0.3}) for n in range(3)) + P4 = nx.path_graph(4) + A = nx.to_numpy_array(P4) + np.testing.assert_equal(A, nx.to_numpy_array(WP4, weight=None)) + np.testing.assert_equal(0.5 * A, nx.to_numpy_array(WP4)) + np.testing.assert_equal(0.3 * A, nx.to_numpy_array(WP4, weight="other")) + + def test_from_numpy_array_type(self): + A = np.array([[1]]) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], int) + + A = np.array([[1]]).astype(float) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], float) + + A = np.array([[1]]).astype(str) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], str) + + A = np.array([[1]]).astype(bool) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], bool) + + A = np.array([[1]]).astype(complex) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], complex) + + A = np.array([[1]]).astype(object) + pytest.raises(TypeError, nx.from_numpy_array, A) + + A = np.array([[[1, 1, 1], [1, 1, 1]], [[1, 1, 1], [1, 1, 1]]]) + with pytest.raises( + nx.NetworkXError, match=f"Input array must be 2D, not {A.ndim}" + ): + g = nx.from_numpy_array(A) + + def test_from_numpy_array_dtype(self): + dt = [("weight", float), ("cost", int)] + A = np.array([[(1.0, 2)]], dtype=dt) + G = nx.from_numpy_array(A) + assert isinstance(G[0][0]["weight"], float) + assert isinstance(G[0][0]["cost"], int) + assert G[0][0]["cost"] == 2 + assert G[0][0]["weight"] == 1.0 + + def test_from_numpy_array_parallel_edges(self): + """Tests that the :func:`networkx.from_numpy_array` function + interprets integer weights as the number of parallel edges when + creating a multigraph. + + """ + A = np.array([[1, 1], [1, 2]]) + # First, with a simple graph, each integer entry in the adjacency + # matrix is interpreted as the weight of a single edge in the graph. + expected = nx.DiGraph() + edges = [(0, 0), (0, 1), (1, 0)] + expected.add_weighted_edges_from([(u, v, 1) for (u, v) in edges]) + expected.add_edge(1, 1, weight=2) + actual = nx.from_numpy_array(A, parallel_edges=True, create_using=nx.DiGraph) + assert graphs_equal(actual, expected) + actual = nx.from_numpy_array(A, parallel_edges=False, create_using=nx.DiGraph) + assert graphs_equal(actual, expected) + # Now each integer entry in the adjacency matrix is interpreted as the + # number of parallel edges in the graph if the appropriate keyword + # argument is specified. + edges = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 1)] + expected = nx.MultiDiGraph() + expected.add_weighted_edges_from([(u, v, 1) for (u, v) in edges]) + actual = nx.from_numpy_array( + A, parallel_edges=True, create_using=nx.MultiDiGraph + ) + assert graphs_equal(actual, expected) + expected = nx.MultiDiGraph() + expected.add_edges_from(set(edges), weight=1) + # The sole self-loop (edge 0) on vertex 1 should have weight 2. + expected[1][1][0]["weight"] = 2 + actual = nx.from_numpy_array( + A, parallel_edges=False, create_using=nx.MultiDiGraph + ) + assert graphs_equal(actual, expected) + + @pytest.mark.parametrize( + "dt", + ( + None, # default + int, # integer dtype + np.dtype( + [("weight", "f8"), ("color", "i1")] + ), # Structured dtype with named fields + ), + ) + def test_from_numpy_array_no_edge_attr(self, dt): + A = np.array([[0, 1], [1, 0]], dtype=dt) + G = nx.from_numpy_array(A, edge_attr=None) + assert "weight" not in G.edges[0, 1] + assert len(G.edges[0, 1]) == 0 + + def test_from_numpy_array_multiedge_no_edge_attr(self): + A = np.array([[0, 2], [2, 0]]) + G = nx.from_numpy_array(A, create_using=nx.MultiDiGraph, edge_attr=None) + assert all("weight" not in e for _, e in G[0][1].items()) + assert len(G[0][1][0]) == 0 + + def test_from_numpy_array_custom_edge_attr(self): + A = np.array([[0, 2], [3, 0]]) + G = nx.from_numpy_array(A, edge_attr="cost") + assert "weight" not in G.edges[0, 1] + assert G.edges[0, 1]["cost"] == 3 + + def test_symmetric(self): + """Tests that a symmetric array has edges added only once to an + undirected multigraph when using :func:`networkx.from_numpy_array`. + + """ + A = np.array([[0, 1], [1, 0]]) + G = nx.from_numpy_array(A, create_using=nx.MultiGraph) + expected = nx.MultiGraph() + expected.add_edge(0, 1, weight=1) + assert graphs_equal(G, expected) + + def test_dtype_int_graph(self): + """Test that setting dtype int actually gives an integer array. + + For more information, see GitHub pull request #1363. + + """ + G = nx.complete_graph(3) + A = nx.to_numpy_array(G, dtype=int) + assert A.dtype == int + + def test_dtype_int_multigraph(self): + """Test that setting dtype int actually gives an integer array. + + For more information, see GitHub pull request #1363. + + """ + G = nx.MultiGraph(nx.complete_graph(3)) + A = nx.to_numpy_array(G, dtype=int) + assert A.dtype == int + + +@pytest.fixture +def multigraph_test_graph(): + G = nx.MultiGraph() + G.add_edge(1, 2, weight=7) + G.add_edge(1, 2, weight=70) + return G + + +@pytest.mark.parametrize(("operator", "expected"), ((sum, 77), (min, 7), (max, 70))) +def test_numpy_multigraph(multigraph_test_graph, operator, expected): + A = nx.to_numpy_array(multigraph_test_graph, multigraph_weight=operator) + assert A[1, 0] == expected + + +def test_to_numpy_array_multigraph_nodelist(multigraph_test_graph): + G = multigraph_test_graph + G.add_edge(0, 1, weight=3) + A = nx.to_numpy_array(G, nodelist=[1, 2]) + assert A.shape == (2, 2) + assert A[1, 0] == 77 + + +@pytest.mark.parametrize( + "G, expected", + [ + (nx.Graph(), np.array([[0, 1 + 2j], [1 + 2j, 0]], dtype=complex)), + (nx.DiGraph(), np.array([[0, 1 + 2j], [0, 0]], dtype=complex)), + ], +) +def test_to_numpy_array_complex_weights(G, expected): + G.add_edge(0, 1, weight=1 + 2j) + A = nx.to_numpy_array(G, dtype=complex) + npt.assert_array_equal(A, expected) + + +def test_to_numpy_array_arbitrary_weights(): + G = nx.DiGraph() + w = 922337203685477580102 # Out of range for int64 + G.add_edge(0, 1, weight=922337203685477580102) # val not representable by int64 + A = nx.to_numpy_array(G, dtype=object) + expected = np.array([[0, w], [0, 0]], dtype=object) + npt.assert_array_equal(A, expected) + + # Undirected + A = nx.to_numpy_array(G.to_undirected(), dtype=object) + expected = np.array([[0, w], [w, 0]], dtype=object) + npt.assert_array_equal(A, expected) + + +@pytest.mark.parametrize( + "func, expected", + ((min, -1), (max, 10), (sum, 11), (np.mean, 11 / 3), (np.median, 2)), +) +def test_to_numpy_array_multiweight_reduction(func, expected): + """Test various functions for reducing multiedge weights.""" + G = nx.MultiDiGraph() + weights = [-1, 2, 10.0] + for w in weights: + G.add_edge(0, 1, weight=w) + A = nx.to_numpy_array(G, multigraph_weight=func, dtype=float) + assert np.allclose(A, [[0, expected], [0, 0]]) + + # Undirected case + A = nx.to_numpy_array(G.to_undirected(), multigraph_weight=func, dtype=float) + assert np.allclose(A, [[0, expected], [expected, 0]]) + + +@pytest.mark.parametrize( + ("G, expected"), + [ + (nx.Graph(), [[(0, 0), (10, 5)], [(10, 5), (0, 0)]]), + (nx.DiGraph(), [[(0, 0), (10, 5)], [(0, 0), (0, 0)]]), + ], +) +def test_to_numpy_array_structured_dtype_attrs_from_fields(G, expected): + """When `dtype` is structured (i.e. has names) and `weight` is None, use + the named fields of the dtype to look up edge attributes.""" + G.add_edge(0, 1, weight=10, cost=5.0) + dtype = np.dtype([("weight", int), ("cost", int)]) + A = nx.to_numpy_array(G, dtype=dtype, weight=None) + expected = np.asarray(expected, dtype=dtype) + npt.assert_array_equal(A, expected) + + +def test_to_numpy_array_structured_dtype_single_attr_default(): + G = nx.path_graph(3) + dtype = np.dtype([("weight", float)]) # A single named field + A = nx.to_numpy_array(G, dtype=dtype, weight=None) + expected = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], dtype=float) + npt.assert_array_equal(A["weight"], expected) + + +@pytest.mark.parametrize( + ("field_name", "expected_attr_val"), + [ + ("weight", 1), + ("cost", 3), + ], +) +def test_to_numpy_array_structured_dtype_single_attr(field_name, expected_attr_val): + G = nx.Graph() + G.add_edge(0, 1, cost=3) + dtype = np.dtype([(field_name, float)]) + A = nx.to_numpy_array(G, dtype=dtype, weight=None) + expected = np.array([[0, expected_attr_val], [expected_attr_val, 0]], dtype=float) + npt.assert_array_equal(A[field_name], expected) + + +@pytest.mark.parametrize("graph_type", (nx.Graph, nx.DiGraph)) +@pytest.mark.parametrize( + "edge", + [ + (0, 1), # No edge attributes + (0, 1, {"weight": 10}), # One edge attr + (0, 1, {"weight": 5, "flow": -4}), # Multiple but not all edge attrs + (0, 1, {"weight": 2.0, "cost": 10, "flow": -45}), # All attrs + ], +) +def test_to_numpy_array_structured_dtype_multiple_fields(graph_type, edge): + G = graph_type([edge]) + dtype = np.dtype([("weight", float), ("cost", float), ("flow", float)]) + A = nx.to_numpy_array(G, dtype=dtype, weight=None) + for attr in dtype.names: + expected = nx.to_numpy_array(G, dtype=float, weight=attr) + npt.assert_array_equal(A[attr], expected) + + +@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) +def test_to_numpy_array_structured_dtype_scalar_nonedge(G): + G.add_edge(0, 1, weight=10) + dtype = np.dtype([("weight", float), ("cost", float)]) + A = nx.to_numpy_array(G, dtype=dtype, weight=None, nonedge=np.nan) + for attr in dtype.names: + expected = nx.to_numpy_array(G, dtype=float, weight=attr, nonedge=np.nan) + npt.assert_array_equal(A[attr], expected) + + +@pytest.mark.parametrize("G", (nx.Graph(), nx.DiGraph())) +def test_to_numpy_array_structured_dtype_nonedge_ary(G): + """Similar to the scalar case, except has a different non-edge value for + each named field.""" + G.add_edge(0, 1, weight=10) + dtype = np.dtype([("weight", float), ("cost", float)]) + nonedges = np.array([(0, np.inf)], dtype=dtype) + A = nx.to_numpy_array(G, dtype=dtype, weight=None, nonedge=nonedges) + for attr in dtype.names: + nonedge = nonedges[attr] + expected = nx.to_numpy_array(G, dtype=float, weight=attr, nonedge=nonedge) + npt.assert_array_equal(A[attr], expected) + + +def test_to_numpy_array_structured_dtype_with_weight_raises(): + """Using both a structured dtype (with named fields) and specifying a `weight` + parameter is ambiguous.""" + G = nx.path_graph(3) + dtype = np.dtype([("weight", int), ("cost", int)]) + exception_msg = "Specifying `weight` not supported for structured dtypes" + with pytest.raises(ValueError, match=exception_msg): + nx.to_numpy_array(G, dtype=dtype) # Default is weight="weight" + with pytest.raises(ValueError, match=exception_msg): + nx.to_numpy_array(G, dtype=dtype, weight="cost") + + +@pytest.mark.parametrize("graph_type", (nx.MultiGraph, nx.MultiDiGraph)) +def test_to_numpy_array_structured_multigraph_raises(graph_type): + G = nx.path_graph(3, create_using=graph_type) + dtype = np.dtype([("weight", int), ("cost", int)]) + with pytest.raises(nx.NetworkXError, match="Structured arrays are not supported"): + nx.to_numpy_array(G, dtype=dtype, weight=None) + + +def test_from_numpy_array_nodelist_bad_size(): + """An exception is raised when `len(nodelist) != A.shape[0]`.""" + n = 5 # Number of nodes + A = np.diag(np.ones(n - 1), k=1) # Adj. matrix for P_n + expected = nx.path_graph(n) + + assert graphs_equal(nx.from_numpy_array(A, edge_attr=None), expected) + nodes = list(range(n)) + assert graphs_equal( + nx.from_numpy_array(A, edge_attr=None, nodelist=nodes), expected + ) + + # Too many node labels + nodes = list(range(n + 1)) + with pytest.raises(ValueError, match="nodelist must have the same length as A"): + nx.from_numpy_array(A, nodelist=nodes) + + # Too few node labels + nodes = list(range(n - 1)) + with pytest.raises(ValueError, match="nodelist must have the same length as A"): + nx.from_numpy_array(A, nodelist=nodes) + + +@pytest.mark.parametrize( + "nodes", + ( + [4, 3, 2, 1, 0], + [9, 7, 1, 2, 8], + ["a", "b", "c", "d", "e"], + [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)], + ["A", 2, 7, "spam", (1, 3)], + ), +) +def test_from_numpy_array_nodelist(nodes): + A = np.diag(np.ones(4), k=1) + # Without edge attributes + expected = nx.relabel_nodes( + nx.path_graph(5), mapping=dict(enumerate(nodes)), copy=True + ) + G = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes) + assert graphs_equal(G, expected) + + # With edge attributes + nx.set_edge_attributes(expected, 1.0, name="weight") + G = nx.from_numpy_array(A, nodelist=nodes) + assert graphs_equal(G, expected) + + +@pytest.mark.parametrize( + "nodes", + ( + [4, 3, 2, 1, 0], + [9, 7, 1, 2, 8], + ["a", "b", "c", "d", "e"], + [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)], + ["A", 2, 7, "spam", (1, 3)], + ), +) +def test_from_numpy_array_nodelist_directed(nodes): + A = np.diag(np.ones(4), k=1) + # Without edge attributes + H = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4)]) + expected = nx.relabel_nodes(H, mapping=dict(enumerate(nodes)), copy=True) + G = nx.from_numpy_array(A, create_using=nx.DiGraph, edge_attr=None, nodelist=nodes) + assert graphs_equal(G, expected) + + # With edge attributes + nx.set_edge_attributes(expected, 1.0, name="weight") + G = nx.from_numpy_array(A, create_using=nx.DiGraph, nodelist=nodes) + assert graphs_equal(G, expected) + + +@pytest.mark.parametrize( + "nodes", + ( + [4, 3, 2, 1, 0], + [9, 7, 1, 2, 8], + ["a", "b", "c", "d", "e"], + [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)], + ["A", 2, 7, "spam", (1, 3)], + ), +) +def test_from_numpy_array_nodelist_multigraph(nodes): + A = np.array( + [ + [0, 1, 0, 0, 0], + [1, 0, 2, 0, 0], + [0, 2, 0, 3, 0], + [0, 0, 3, 0, 4], + [0, 0, 0, 4, 0], + ] + ) + + H = nx.MultiGraph() + for i, edge in enumerate(((0, 1), (1, 2), (2, 3), (3, 4))): + H.add_edges_from(itertools.repeat(edge, i + 1)) + expected = nx.relabel_nodes(H, mapping=dict(enumerate(nodes)), copy=True) + + G = nx.from_numpy_array( + A, + parallel_edges=True, + create_using=nx.MultiGraph, + edge_attr=None, + nodelist=nodes, + ) + assert graphs_equal(G, expected) + + +@pytest.mark.parametrize( + "nodes", + ( + [4, 3, 2, 1, 0], + [9, 7, 1, 2, 8], + ["a", "b", "c", "d", "e"], + [(0, 0), (1, 1), (2, 3), (0, 2), (3, 1)], + ["A", 2, 7, "spam", (1, 3)], + ), +) +@pytest.mark.parametrize("graph", (nx.complete_graph, nx.cycle_graph, nx.wheel_graph)) +def test_from_numpy_array_nodelist_rountrip(graph, nodes): + G = graph(5) + A = nx.to_numpy_array(G) + expected = nx.relabel_nodes(G, mapping=dict(enumerate(nodes)), copy=True) + H = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes) + assert graphs_equal(H, expected) + + # With an isolated node + G = graph(4) + G.add_node("foo") + A = nx.to_numpy_array(G) + expected = nx.relabel_nodes(G, mapping=dict(zip(G.nodes, nodes)), copy=True) + H = nx.from_numpy_array(A, edge_attr=None, nodelist=nodes) + assert graphs_equal(H, expected) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_pandas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_pandas.py new file mode 100644 index 0000000000000000000000000000000000000000..eaa8d695f868dbb551d3a3819cedd590a25492f8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_pandas.py @@ -0,0 +1,349 @@ +import pytest + +import networkx as nx +from networkx.utils import edges_equal, graphs_equal, nodes_equal + +np = pytest.importorskip("numpy") +pd = pytest.importorskip("pandas") + + +class TestConvertPandas: + def setup_method(self): + self.rng = np.random.RandomState(seed=5) + ints = self.rng.randint(1, 11, size=(3, 2)) + a = ["A", "B", "C"] + b = ["D", "A", "E"] + df = pd.DataFrame(ints, columns=["weight", "cost"]) + df[0] = a # Column label 0 (int) + df["b"] = b # Column label 'b' (str) + self.df = df + + mdf = pd.DataFrame([[4, 16, "A", "D"]], columns=["weight", "cost", 0, "b"]) + self.mdf = pd.concat([df, mdf]) + + def test_exceptions(self): + G = pd.DataFrame(["a"]) # adj + pytest.raises(nx.NetworkXError, nx.to_networkx_graph, G) + G = pd.DataFrame(["a", 0.0]) # elist + pytest.raises(nx.NetworkXError, nx.to_networkx_graph, G) + df = pd.DataFrame([[1, 1], [1, 0]], dtype=int, index=[1, 2], columns=["a", "b"]) + pytest.raises(nx.NetworkXError, nx.from_pandas_adjacency, df) + + def test_from_edgelist_all_attr(self): + Gtrue = nx.Graph( + [ + ("E", "C", {"cost": 9, "weight": 10}), + ("B", "A", {"cost": 1, "weight": 7}), + ("A", "D", {"cost": 7, "weight": 4}), + ] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", True) + assert graphs_equal(G, Gtrue) + # MultiGraph + MGtrue = nx.MultiGraph(Gtrue) + MGtrue.add_edge("A", "D", cost=16, weight=4) + MG = nx.from_pandas_edgelist(self.mdf, 0, "b", True, nx.MultiGraph()) + assert graphs_equal(MG, MGtrue) + + def test_from_edgelist_multi_attr(self): + Gtrue = nx.Graph( + [ + ("E", "C", {"cost": 9, "weight": 10}), + ("B", "A", {"cost": 1, "weight": 7}), + ("A", "D", {"cost": 7, "weight": 4}), + ] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", ["weight", "cost"]) + assert graphs_equal(G, Gtrue) + + def test_from_edgelist_multi_attr_incl_target(self): + Gtrue = nx.Graph( + [ + ("E", "C", {0: "C", "b": "E", "weight": 10}), + ("B", "A", {0: "B", "b": "A", "weight": 7}), + ("A", "D", {0: "A", "b": "D", "weight": 4}), + ] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", [0, "b", "weight"]) + assert graphs_equal(G, Gtrue) + + def test_from_edgelist_multidigraph_and_edge_attr(self): + # example from issue #2374 + edges = [ + ("X1", "X4", {"Co": "zA", "Mi": 0, "St": "X1"}), + ("X1", "X4", {"Co": "zB", "Mi": 54, "St": "X2"}), + ("X1", "X4", {"Co": "zB", "Mi": 49, "St": "X3"}), + ("X1", "X4", {"Co": "zB", "Mi": 44, "St": "X4"}), + ("Y1", "Y3", {"Co": "zC", "Mi": 0, "St": "Y1"}), + ("Y1", "Y3", {"Co": "zC", "Mi": 34, "St": "Y2"}), + ("Y1", "Y3", {"Co": "zC", "Mi": 29, "St": "X2"}), + ("Y1", "Y3", {"Co": "zC", "Mi": 24, "St": "Y3"}), + ("Z1", "Z3", {"Co": "zD", "Mi": 0, "St": "Z1"}), + ("Z1", "Z3", {"Co": "zD", "Mi": 14, "St": "X3"}), + ] + Gtrue = nx.MultiDiGraph(edges) + data = { + "O": ["X1", "X1", "X1", "X1", "Y1", "Y1", "Y1", "Y1", "Z1", "Z1"], + "D": ["X4", "X4", "X4", "X4", "Y3", "Y3", "Y3", "Y3", "Z3", "Z3"], + "St": ["X1", "X2", "X3", "X4", "Y1", "Y2", "X2", "Y3", "Z1", "X3"], + "Co": ["zA", "zB", "zB", "zB", "zC", "zC", "zC", "zC", "zD", "zD"], + "Mi": [0, 54, 49, 44, 0, 34, 29, 24, 0, 14], + } + df = pd.DataFrame.from_dict(data) + G1 = nx.from_pandas_edgelist( + df, source="O", target="D", edge_attr=True, create_using=nx.MultiDiGraph + ) + G2 = nx.from_pandas_edgelist( + df, + source="O", + target="D", + edge_attr=["St", "Co", "Mi"], + create_using=nx.MultiDiGraph, + ) + assert graphs_equal(G1, Gtrue) + assert graphs_equal(G2, Gtrue) + + def test_from_edgelist_one_attr(self): + Gtrue = nx.Graph( + [ + ("E", "C", {"weight": 10}), + ("B", "A", {"weight": 7}), + ("A", "D", {"weight": 4}), + ] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", "weight") + assert graphs_equal(G, Gtrue) + + def test_from_edgelist_int_attr_name(self): + # note: this also tests that edge_attr can be `source` + Gtrue = nx.Graph( + [("E", "C", {0: "C"}), ("B", "A", {0: "B"}), ("A", "D", {0: "A"})] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", 0) + assert graphs_equal(G, Gtrue) + + def test_from_edgelist_invalid_attr(self): + pytest.raises( + nx.NetworkXError, nx.from_pandas_edgelist, self.df, 0, "b", "misspell" + ) + pytest.raises(nx.NetworkXError, nx.from_pandas_edgelist, self.df, 0, "b", 1) + # see Issue #3562 + edgeframe = pd.DataFrame([[0, 1], [1, 2], [2, 0]], columns=["s", "t"]) + pytest.raises( + nx.NetworkXError, nx.from_pandas_edgelist, edgeframe, "s", "t", True + ) + pytest.raises( + nx.NetworkXError, nx.from_pandas_edgelist, edgeframe, "s", "t", "weight" + ) + pytest.raises( + nx.NetworkXError, + nx.from_pandas_edgelist, + edgeframe, + "s", + "t", + ["weight", "size"], + ) + + def test_from_edgelist_no_attr(self): + Gtrue = nx.Graph([("E", "C", {}), ("B", "A", {}), ("A", "D", {})]) + G = nx.from_pandas_edgelist(self.df, 0, "b") + assert graphs_equal(G, Gtrue) + + def test_from_edgelist(self): + # Pandas DataFrame + G = nx.cycle_graph(10) + G.add_weighted_edges_from((u, v, u) for u, v in list(G.edges)) + + edgelist = nx.to_edgelist(G) + source = [s for s, t, d in edgelist] + target = [t for s, t, d in edgelist] + weight = [d["weight"] for s, t, d in edgelist] + edges = pd.DataFrame({"source": source, "target": target, "weight": weight}) + + GG = nx.from_pandas_edgelist(edges, edge_attr="weight") + assert nodes_equal(G.nodes(), GG.nodes()) + assert edges_equal(G.edges(), GG.edges()) + GW = nx.to_networkx_graph(edges, create_using=nx.Graph) + assert nodes_equal(G.nodes(), GW.nodes()) + assert edges_equal(G.edges(), GW.edges()) + + def test_to_edgelist_default_source_or_target_col_exists(self): + G = nx.path_graph(10) + G.add_weighted_edges_from((u, v, u) for u, v in list(G.edges)) + nx.set_edge_attributes(G, 0, name="source") + pytest.raises(nx.NetworkXError, nx.to_pandas_edgelist, G) + + # drop source column to test an exception raised for the target column + for u, v, d in G.edges(data=True): + d.pop("source", None) + + nx.set_edge_attributes(G, 0, name="target") + pytest.raises(nx.NetworkXError, nx.to_pandas_edgelist, G) + + def test_to_edgelist_custom_source_or_target_col_exists(self): + G = nx.path_graph(10) + G.add_weighted_edges_from((u, v, u) for u, v in list(G.edges)) + nx.set_edge_attributes(G, 0, name="source_col_name") + pytest.raises( + nx.NetworkXError, nx.to_pandas_edgelist, G, source="source_col_name" + ) + + # drop source column to test an exception raised for the target column + for u, v, d in G.edges(data=True): + d.pop("source_col_name", None) + + nx.set_edge_attributes(G, 0, name="target_col_name") + pytest.raises( + nx.NetworkXError, nx.to_pandas_edgelist, G, target="target_col_name" + ) + + def test_to_edgelist_edge_key_col_exists(self): + G = nx.path_graph(10, create_using=nx.MultiGraph) + G.add_weighted_edges_from((u, v, u) for u, v in list(G.edges())) + nx.set_edge_attributes(G, 0, name="edge_key_name") + pytest.raises( + nx.NetworkXError, nx.to_pandas_edgelist, G, edge_key="edge_key_name" + ) + + def test_from_adjacency(self): + nodelist = [1, 2] + dftrue = pd.DataFrame( + [[1, 1], [1, 0]], dtype=int, index=nodelist, columns=nodelist + ) + G = nx.Graph([(1, 1), (1, 2)]) + df = nx.to_pandas_adjacency(G, dtype=int) + pd.testing.assert_frame_equal(df, dftrue) + + @pytest.mark.parametrize("graph", [nx.Graph, nx.MultiGraph]) + def test_roundtrip(self, graph): + # edgelist + Gtrue = graph([(1, 1), (1, 2)]) + df = nx.to_pandas_edgelist(Gtrue) + G = nx.from_pandas_edgelist(df, create_using=graph) + assert graphs_equal(Gtrue, G) + # adjacency + adj = {1: {1: {"weight": 1}, 2: {"weight": 1}}, 2: {1: {"weight": 1}}} + Gtrue = graph(adj) + df = nx.to_pandas_adjacency(Gtrue, dtype=int) + G = nx.from_pandas_adjacency(df, create_using=graph) + assert graphs_equal(Gtrue, G) + + def test_from_adjacency_named(self): + # example from issue #3105 + data = { + "A": {"A": 0, "B": 0, "C": 0}, + "B": {"A": 1, "B": 0, "C": 0}, + "C": {"A": 0, "B": 1, "C": 0}, + } + dftrue = pd.DataFrame(data, dtype=np.intp) + df = dftrue[["A", "C", "B"]] + G = nx.from_pandas_adjacency(df, create_using=nx.DiGraph()) + df = nx.to_pandas_adjacency(G, dtype=np.intp) + pd.testing.assert_frame_equal(df, dftrue) + + @pytest.mark.parametrize("edge_attr", [["attr2", "attr3"], True]) + def test_edgekey_with_multigraph(self, edge_attr): + df = pd.DataFrame( + { + "source": {"A": "N1", "B": "N2", "C": "N1", "D": "N1"}, + "target": {"A": "N2", "B": "N3", "C": "N1", "D": "N2"}, + "attr1": {"A": "F1", "B": "F2", "C": "F3", "D": "F4"}, + "attr2": {"A": 1, "B": 0, "C": 0, "D": 0}, + "attr3": {"A": 0, "B": 1, "C": 0, "D": 1}, + } + ) + Gtrue = nx.MultiGraph( + [ + ("N1", "N2", "F1", {"attr2": 1, "attr3": 0}), + ("N2", "N3", "F2", {"attr2": 0, "attr3": 1}), + ("N1", "N1", "F3", {"attr2": 0, "attr3": 0}), + ("N1", "N2", "F4", {"attr2": 0, "attr3": 1}), + ] + ) + # example from issue #4065 + G = nx.from_pandas_edgelist( + df, + source="source", + target="target", + edge_attr=edge_attr, + edge_key="attr1", + create_using=nx.MultiGraph(), + ) + assert graphs_equal(G, Gtrue) + + df_roundtrip = nx.to_pandas_edgelist(G, edge_key="attr1") + df_roundtrip = df_roundtrip.sort_values("attr1") + df_roundtrip.index = ["A", "B", "C", "D"] + pd.testing.assert_frame_equal( + df, df_roundtrip[["source", "target", "attr1", "attr2", "attr3"]] + ) + + def test_edgekey_with_normal_graph_no_action(self): + Gtrue = nx.Graph( + [ + ("E", "C", {"cost": 9, "weight": 10}), + ("B", "A", {"cost": 1, "weight": 7}), + ("A", "D", {"cost": 7, "weight": 4}), + ] + ) + G = nx.from_pandas_edgelist(self.df, 0, "b", True, edge_key="weight") + assert graphs_equal(G, Gtrue) + + def test_nonexisting_edgekey_raises(self): + with pytest.raises(nx.exception.NetworkXError): + nx.from_pandas_edgelist( + self.df, + source="source", + target="target", + edge_key="Not_real", + edge_attr=True, + create_using=nx.MultiGraph(), + ) + + def test_multigraph_with_edgekey_no_edgeattrs(self): + Gtrue = nx.MultiGraph() + Gtrue.add_edge(0, 1, key=0) + Gtrue.add_edge(0, 1, key=3) + df = nx.to_pandas_edgelist(Gtrue, edge_key="key") + expected = pd.DataFrame({"source": [0, 0], "target": [1, 1], "key": [0, 3]}) + pd.testing.assert_frame_equal(expected, df) + G = nx.from_pandas_edgelist(df, edge_key="key", create_using=nx.MultiGraph) + assert graphs_equal(Gtrue, G) + + +def test_to_pandas_adjacency_with_nodelist(): + G = nx.complete_graph(5) + nodelist = [1, 4] + expected = pd.DataFrame( + [[0, 1], [1, 0]], dtype=int, index=nodelist, columns=nodelist + ) + pd.testing.assert_frame_equal( + expected, nx.to_pandas_adjacency(G, nodelist, dtype=int) + ) + + +def test_to_pandas_edgelist_with_nodelist(): + G = nx.Graph() + G.add_edges_from([(0, 1), (1, 2), (1, 3)], weight=2.0) + G.add_edge(0, 5, weight=100) + df = nx.to_pandas_edgelist(G, nodelist=[1, 2]) + assert 0 not in df["source"].to_numpy() + assert 100 not in df["weight"].to_numpy() + + +def test_from_pandas_adjacency_with_index_collisions(): + """See gh-7407""" + df = pd.DataFrame( + [ + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [0, 0, 0, 0], + ], + index=[1010001, 2, 1, 1010002], + columns=[1010001, 2, 1, 1010002], + ) + G = nx.from_pandas_adjacency(df, create_using=nx.DiGraph) + expected = nx.DiGraph([(1010001, 2), (2, 1), (1, 1010002)]) + assert nodes_equal(G.nodes, expected.nodes) + assert edges_equal(G.edges, expected.edges, directed=True) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_scipy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_scipy.py new file mode 100644 index 0000000000000000000000000000000000000000..2575027046c61612c1be3c36a5b7877c28e71d92 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_convert_scipy.py @@ -0,0 +1,281 @@ +import pytest + +import networkx as nx +from networkx.utils import graphs_equal + +np = pytest.importorskip("numpy") +sp = pytest.importorskip("scipy") + + +class TestConvertScipy: + def setup_method(self): + self.G1 = nx.barbell_graph(10, 3) + self.G2 = nx.cycle_graph(10, create_using=nx.DiGraph) + + self.G3 = self.create_weighted(nx.Graph()) + self.G4 = self.create_weighted(nx.DiGraph()) + + def test_exceptions(self): + class G: + format = None + + pytest.raises(nx.NetworkXError, nx.to_networkx_graph, G) + + def create_weighted(self, G): + g = nx.cycle_graph(4) + e = list(g.edges()) + source = [u for u, v in e] + dest = [v for u, v in e] + weight = [s + 10 for s in source] + ex = zip(source, dest, weight) + G.add_weighted_edges_from(ex) + return G + + def identity_conversion(self, G, A, create_using): + GG = nx.from_scipy_sparse_array(A, create_using=create_using) + assert nx.is_isomorphic(G, GG) + + GW = nx.to_networkx_graph(A, create_using=create_using) + assert nx.is_isomorphic(G, GW) + + GI = nx.empty_graph(0, create_using).__class__(A) + assert nx.is_isomorphic(G, GI) + + ACSR = A.tocsr() + GI = nx.empty_graph(0, create_using).__class__(ACSR) + assert nx.is_isomorphic(G, GI) + + ACOO = A.tocoo() + GI = nx.empty_graph(0, create_using).__class__(ACOO) + assert nx.is_isomorphic(G, GI) + + ACSC = A.tocsc() + GI = nx.empty_graph(0, create_using).__class__(ACSC) + assert nx.is_isomorphic(G, GI) + + AD = A.todense() + GI = nx.empty_graph(0, create_using).__class__(AD) + assert nx.is_isomorphic(G, GI) + + AA = A.toarray() + GI = nx.empty_graph(0, create_using).__class__(AA) + assert nx.is_isomorphic(G, GI) + + def test_shape(self): + "Conversion from non-square sparse array." + A = sp.sparse.lil_array([[1, 2, 3], [4, 5, 6]]) + pytest.raises(nx.NetworkXError, nx.from_scipy_sparse_array, A) + + def test_identity_graph_matrix(self): + "Conversion from graph to sparse matrix to graph." + A = nx.to_scipy_sparse_array(self.G1) + self.identity_conversion(self.G1, A, nx.Graph()) + + def test_identity_digraph_matrix(self): + "Conversion from digraph to sparse matrix to digraph." + A = nx.to_scipy_sparse_array(self.G2) + self.identity_conversion(self.G2, A, nx.DiGraph()) + + def test_identity_weighted_graph_matrix(self): + """Conversion from weighted graph to sparse matrix to weighted graph.""" + A = nx.to_scipy_sparse_array(self.G3) + self.identity_conversion(self.G3, A, nx.Graph()) + + def test_identity_weighted_digraph_matrix(self): + """Conversion from weighted digraph to sparse matrix to weighted digraph.""" + A = nx.to_scipy_sparse_array(self.G4) + self.identity_conversion(self.G4, A, nx.DiGraph()) + + def test_nodelist(self): + """Conversion from graph to sparse matrix to graph with nodelist.""" + P4 = nx.path_graph(4) + P3 = nx.path_graph(3) + nodelist = list(P3.nodes()) + A = nx.to_scipy_sparse_array(P4, nodelist=nodelist) + GA = nx.Graph(A) + assert nx.is_isomorphic(GA, P3) + + pytest.raises(nx.NetworkXError, nx.to_scipy_sparse_array, P3, nodelist=[]) + # Test nodelist duplicates. + long_nl = nodelist + [0] + pytest.raises(nx.NetworkXError, nx.to_scipy_sparse_array, P3, nodelist=long_nl) + + # Test nodelist contains non-nodes + non_nl = [-1, 0, 1, 2] + pytest.raises(nx.NetworkXError, nx.to_scipy_sparse_array, P3, nodelist=non_nl) + + def test_weight_keyword(self): + WP4 = nx.Graph() + WP4.add_edges_from((n, n + 1, {"weight": 0.5, "other": 0.3}) for n in range(3)) + P4 = nx.path_graph(4) + A = nx.to_scipy_sparse_array(P4) + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + np.testing.assert_equal( + 0.5 * A.todense(), nx.to_scipy_sparse_array(WP4).todense() + ) + np.testing.assert_equal( + 0.3 * A.todense(), nx.to_scipy_sparse_array(WP4, weight="other").todense() + ) + + def test_format_keyword(self): + WP4 = nx.Graph() + WP4.add_edges_from((n, n + 1, {"weight": 0.5, "other": 0.3}) for n in range(3)) + P4 = nx.path_graph(4) + A = nx.to_scipy_sparse_array(P4, format="csr") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="csc") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="coo") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="bsr") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="lil") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="dia") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + A = nx.to_scipy_sparse_array(P4, format="dok") + np.testing.assert_equal( + A.todense(), nx.to_scipy_sparse_array(WP4, weight=None).todense() + ) + + def test_format_keyword_raise(self): + with pytest.raises(nx.NetworkXError): + WP4 = nx.Graph() + WP4.add_edges_from( + (n, n + 1, {"weight": 0.5, "other": 0.3}) for n in range(3) + ) + P4 = nx.path_graph(4) + nx.to_scipy_sparse_array(P4, format="any_other") + + def test_null_raise(self): + with pytest.raises(nx.NetworkXError): + nx.to_scipy_sparse_array(nx.Graph()) + + def test_empty(self): + G = nx.Graph() + G.add_node(1) + M = nx.to_scipy_sparse_array(G) + np.testing.assert_equal(M.toarray(), np.array([[0]])) + + def test_ordering(self): + G = nx.DiGraph() + G.add_edge(1, 2) + G.add_edge(2, 3) + G.add_edge(3, 1) + M = nx.to_scipy_sparse_array(G, nodelist=[3, 2, 1]) + np.testing.assert_equal( + M.toarray(), np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) + ) + + def test_selfloop_graph(self): + G = nx.Graph([(1, 1)]) + M = nx.to_scipy_sparse_array(G) + np.testing.assert_equal(M.toarray(), np.array([[1]])) + + G.add_edges_from([(2, 3), (3, 4)]) + M = nx.to_scipy_sparse_array(G, nodelist=[2, 3, 4]) + np.testing.assert_equal( + M.toarray(), np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) + ) + + def test_selfloop_digraph(self): + G = nx.DiGraph([(1, 1)]) + M = nx.to_scipy_sparse_array(G) + np.testing.assert_equal(M.toarray(), np.array([[1]])) + + G.add_edges_from([(2, 3), (3, 4)]) + M = nx.to_scipy_sparse_array(G, nodelist=[2, 3, 4]) + np.testing.assert_equal( + M.toarray(), np.array([[0, 1, 0], [0, 0, 1], [0, 0, 0]]) + ) + + def test_from_scipy_sparse_array_parallel_edges(self): + """Tests that the :func:`networkx.from_scipy_sparse_array` function + interprets integer weights as the number of parallel edges when + creating a multigraph. + + """ + A = sp.sparse.csr_array([[1, 1], [1, 2]]) + # First, with a simple graph, each integer entry in the adjacency + # matrix is interpreted as the weight of a single edge in the graph. + expected = nx.DiGraph() + edges = [(0, 0), (0, 1), (1, 0)] + expected.add_weighted_edges_from([(u, v, 1) for (u, v) in edges]) + expected.add_edge(1, 1, weight=2) + actual = nx.from_scipy_sparse_array( + A, parallel_edges=True, create_using=nx.DiGraph + ) + assert graphs_equal(actual, expected) + actual = nx.from_scipy_sparse_array( + A, parallel_edges=False, create_using=nx.DiGraph + ) + assert graphs_equal(actual, expected) + # Now each integer entry in the adjacency matrix is interpreted as the + # number of parallel edges in the graph if the appropriate keyword + # argument is specified. + edges = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 1)] + expected = nx.MultiDiGraph() + expected.add_weighted_edges_from([(u, v, 1) for (u, v) in edges]) + actual = nx.from_scipy_sparse_array( + A, parallel_edges=True, create_using=nx.MultiDiGraph + ) + assert graphs_equal(actual, expected) + expected = nx.MultiDiGraph() + expected.add_edges_from(set(edges), weight=1) + # The sole self-loop (edge 0) on vertex 1 should have weight 2. + expected[1][1][0]["weight"] = 2 + actual = nx.from_scipy_sparse_array( + A, parallel_edges=False, create_using=nx.MultiDiGraph + ) + assert graphs_equal(actual, expected) + + def test_symmetric(self): + """Tests that a symmetric matrix has edges added only once to an + undirected multigraph when using + :func:`networkx.from_scipy_sparse_array`. + + """ + A = sp.sparse.csr_array([[0, 1], [1, 0]]) + G = nx.from_scipy_sparse_array(A, create_using=nx.MultiGraph) + expected = nx.MultiGraph() + expected.add_edge(0, 1, weight=1) + assert graphs_equal(G, expected) + + +@pytest.mark.parametrize("sparse_format", ("csr", "csc", "dok")) +def test_from_scipy_sparse_array_formats(sparse_format): + """Test all formats supported by _generate_weighted_edges.""" + # trinode complete graph with non-uniform edge weights + expected = nx.Graph() + expected.add_edges_from( + [ + (0, 1, {"weight": 3}), + (0, 2, {"weight": 2}), + (1, 0, {"weight": 3}), + (1, 2, {"weight": 1}), + (2, 0, {"weight": 2}), + (2, 1, {"weight": 1}), + ] + ) + A = sp.sparse.coo_array([[0, 3, 2], [3, 0, 1], [2, 1, 0]]).asformat(sparse_format) + assert graphs_equal(expected, nx.from_scipy_sparse_array(A)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_exceptions.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..cf59983cb8d12a119f5744ebc8b11e7cb9075366 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_exceptions.py @@ -0,0 +1,40 @@ +import pytest + +import networkx as nx + +# smoke tests for exceptions + + +def test_raises_networkxexception(): + with pytest.raises(nx.NetworkXException): + raise nx.NetworkXException + + +def test_raises_networkxerr(): + with pytest.raises(nx.NetworkXError): + raise nx.NetworkXError + + +def test_raises_networkx_pointless_concept(): + with pytest.raises(nx.NetworkXPointlessConcept): + raise nx.NetworkXPointlessConcept + + +def test_raises_networkxalgorithmerr(): + with pytest.raises(nx.NetworkXAlgorithmError): + raise nx.NetworkXAlgorithmError + + +def test_raises_networkx_unfeasible(): + with pytest.raises(nx.NetworkXUnfeasible): + raise nx.NetworkXUnfeasible + + +def test_raises_networkx_no_path(): + with pytest.raises(nx.NetworkXNoPath): + raise nx.NetworkXNoPath + + +def test_raises_networkx_unbounded(): + with pytest.raises(nx.NetworkXUnbounded): + raise nx.NetworkXUnbounded diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_import.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_import.py new file mode 100644 index 0000000000000000000000000000000000000000..32aafdf2a4dafc85cee088138590b84f4c627b5e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_import.py @@ -0,0 +1,11 @@ +import pytest + + +def test_namespace_alias(): + with pytest.raises(ImportError): + from networkx import nx + + +def test_namespace_nesting(): + with pytest.raises(ImportError): + from networkx import networkx diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_lazy_imports.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_lazy_imports.py new file mode 100644 index 0000000000000000000000000000000000000000..ec09ac2fcda5c9ce8b1f9c6f1d0fba58eb54742e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_lazy_imports.py @@ -0,0 +1,96 @@ +import sys +import types + +import pytest + +import networkx.lazy_imports as lazy + + +def test_lazy_import_basics(): + math = lazy._lazy_import("math") + anything_not_real = lazy._lazy_import("anything_not_real") + + # Now test that accessing attributes does what it should + assert math.sin(math.pi) == pytest.approx(0, 1e-6) + # poor-mans pytest.raises for testing errors on attribute access + try: + anything_not_real.pi + assert False # Should not get here + except ModuleNotFoundError: + pass + assert isinstance(anything_not_real, lazy.DelayedImportErrorModule) + # see if it changes for second access + try: + anything_not_real.pi + assert False # Should not get here + except ModuleNotFoundError: + pass + + +def test_lazy_import_impact_on_sys_modules(): + math = lazy._lazy_import("math") + anything_not_real = lazy._lazy_import("anything_not_real") + + assert isinstance(math, types.ModuleType) + assert "math" in sys.modules + assert type(anything_not_real) is lazy.DelayedImportErrorModule + assert "anything_not_real" not in sys.modules + + # only do this if numpy is installed + np_test = pytest.importorskip("numpy") + np = lazy._lazy_import("numpy") + assert isinstance(np, types.ModuleType) + assert "numpy" in sys.modules + + np.pi # trigger load of numpy + + assert isinstance(np, types.ModuleType) + assert "numpy" in sys.modules + + +def test_lazy_import_nonbuiltins(): + sp = lazy._lazy_import("scipy") + np = lazy._lazy_import("numpy") + if isinstance(sp, lazy.DelayedImportErrorModule): + try: + sp.special.erf + assert False + except ModuleNotFoundError: + pass + elif isinstance(np, lazy.DelayedImportErrorModule): + try: + np.sin(np.pi) + assert False + except ModuleNotFoundError: + pass + else: + assert sp.special.erf(np.pi) == pytest.approx(1, 1e-4) + + +def test_lazy_attach(): + name = "mymod" + submods = ["mysubmodule", "anothersubmodule"] + myall = {"not_real_submod": ["some_var_or_func"]} + + locls = { + "attach": lazy.attach, + "name": name, + "submods": submods, + "myall": myall, + } + s = "__getattr__, __lazy_dir__, __all__ = attach(name, submods, myall)" + + exec(s, {}, locls) + expected = { + "attach": lazy.attach, + "name": name, + "submods": submods, + "myall": myall, + "__getattr__": None, + "__lazy_dir__": None, + "__all__": None, + } + assert locls.keys() == expected.keys() + for k, v in expected.items(): + if v is not None: + assert locls[k] == v diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_relabel.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_relabel.py new file mode 100644 index 0000000000000000000000000000000000000000..7a70ec11e6a4b2ed3e7371e1adaee90ece46f2c7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_relabel.py @@ -0,0 +1,349 @@ +import pytest + +import networkx as nx +from networkx.generators.classic import empty_graph +from networkx.utils import edges_equal, nodes_equal + + +class TestRelabel: + def test_convert_node_labels_to_integers(self): + # test that empty graph converts fine for all options + G = empty_graph() + H = nx.convert_node_labels_to_integers(G, 100) + assert list(H.nodes()) == [] + assert list(H.edges()) == [] + + for opt in ["default", "sorted", "increasing degree", "decreasing degree"]: + G = empty_graph() + H = nx.convert_node_labels_to_integers(G, 100, ordering=opt) + assert list(H.nodes()) == [] + assert list(H.edges()) == [] + + G = empty_graph() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + H = nx.convert_node_labels_to_integers(G) + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + + H = nx.convert_node_labels_to_integers(G, 1000) + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + assert nodes_equal(H.nodes(), [1000, 1001, 1002, 1003]) + + H = nx.convert_node_labels_to_integers(G, ordering="increasing degree") + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + assert H.degree(0) == 1 + assert H.degree(1) == 2 + assert H.degree(2) == 2 + assert H.degree(3) == 3 + + H = nx.convert_node_labels_to_integers(G, ordering="decreasing degree") + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + assert H.degree(0) == 3 + assert H.degree(1) == 2 + assert H.degree(2) == 2 + assert H.degree(3) == 1 + + H = nx.convert_node_labels_to_integers( + G, ordering="increasing degree", label_attribute="label" + ) + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + assert H.degree(0) == 1 + assert H.degree(1) == 2 + assert H.degree(2) == 2 + assert H.degree(3) == 3 + + # check mapping + assert H.nodes[3]["label"] == "C" + assert H.nodes[0]["label"] == "D" + assert H.nodes[1]["label"] == "A" or H.nodes[2]["label"] == "A" + assert H.nodes[1]["label"] == "B" or H.nodes[2]["label"] == "B" + + def test_convert_to_integers2(self): + G = empty_graph() + G.add_edges_from([("C", "D"), ("A", "B"), ("A", "C"), ("B", "C")]) + H = nx.convert_node_labels_to_integers(G, ordering="sorted") + degH = (d for n, d in H.degree()) + degG = (d for n, d in G.degree()) + assert sorted(degH) == sorted(degG) + + H = nx.convert_node_labels_to_integers( + G, ordering="sorted", label_attribute="label" + ) + assert H.nodes[0]["label"] == "A" + assert H.nodes[1]["label"] == "B" + assert H.nodes[2]["label"] == "C" + assert H.nodes[3]["label"] == "D" + + def test_convert_to_integers_raise(self): + with pytest.raises(nx.NetworkXError): + G = nx.Graph() + H = nx.convert_node_labels_to_integers(G, ordering="increasing age") + + def test_relabel_nodes_copy(self): + G = nx.empty_graph() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + mapping = {"A": "aardvark", "B": "bear", "C": "cat", "D": "dog"} + H = nx.relabel_nodes(G, mapping) + assert nodes_equal(H.nodes(), ["aardvark", "bear", "cat", "dog"]) + + def test_relabel_nodes_function(self): + G = nx.empty_graph() + G.add_edges_from([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + # function mapping no longer encouraged but works + + def mapping(n): + return ord(n) + + H = nx.relabel_nodes(G, mapping) + assert nodes_equal(H.nodes(), [65, 66, 67, 68]) + + def test_relabel_nodes_callable_type(self): + G = nx.path_graph(4) + H = nx.relabel_nodes(G, str) + assert nodes_equal(H.nodes, ["0", "1", "2", "3"]) + + @pytest.mark.parametrize("non_mc", ("0123", ["0", "1", "2", "3"])) + def test_relabel_nodes_non_mapping_or_callable(self, non_mc): + """If `mapping` is neither a Callable or a Mapping, an exception + should be raised.""" + G = nx.path_graph(4) + with pytest.raises(AttributeError): + nx.relabel_nodes(G, non_mc) + + def test_relabel_nodes_graph(self): + G = nx.Graph([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + mapping = {"A": "aardvark", "B": "bear", "C": "cat", "D": "dog"} + H = nx.relabel_nodes(G, mapping) + assert nodes_equal(H.nodes(), ["aardvark", "bear", "cat", "dog"]) + + def test_relabel_nodes_orderedgraph(self): + G = nx.Graph() + G.add_nodes_from([1, 2, 3]) + G.add_edges_from([(1, 3), (2, 3)]) + mapping = {1: "a", 2: "b", 3: "c"} + H = nx.relabel_nodes(G, mapping) + assert list(H.nodes) == ["a", "b", "c"] + + def test_relabel_nodes_digraph(self): + G = nx.DiGraph([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + mapping = {"A": "aardvark", "B": "bear", "C": "cat", "D": "dog"} + H = nx.relabel_nodes(G, mapping, copy=False) + assert nodes_equal(H.nodes(), ["aardvark", "bear", "cat", "dog"]) + + def test_relabel_nodes_multigraph(self): + G = nx.MultiGraph([("a", "b"), ("a", "b")]) + mapping = {"a": "aardvark", "b": "bear"} + G = nx.relabel_nodes(G, mapping, copy=False) + assert nodes_equal(G.nodes(), ["aardvark", "bear"]) + assert edges_equal(G.edges(), [("aardvark", "bear"), ("aardvark", "bear")]) + + def test_relabel_nodes_multidigraph(self): + G = nx.MultiDiGraph([("a", "b"), ("a", "b")]) + mapping = {"a": "aardvark", "b": "bear"} + G = nx.relabel_nodes(G, mapping, copy=False) + assert nodes_equal(G.nodes(), ["aardvark", "bear"]) + assert edges_equal( + G.edges(), [("aardvark", "bear"), ("aardvark", "bear")], directed=True + ) + + def test_relabel_isolated_nodes_to_same(self): + G = nx.Graph() + G.add_nodes_from(range(4)) + mapping = {1: 1} + H = nx.relabel_nodes(G, mapping, copy=False) + assert nodes_equal(H.nodes(), list(range(4))) + + def test_relabel_nodes_missing(self): + G = nx.Graph([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")]) + mapping = {0: "aardvark"} + # copy=True + H = nx.relabel_nodes(G, mapping, copy=True) + assert nodes_equal(H.nodes, G.nodes) + # copy=False + GG = G.copy() + nx.relabel_nodes(G, mapping, copy=False) + assert nodes_equal(G.nodes, GG.nodes) + + def test_relabel_copy_name(self): + G = nx.Graph() + H = nx.relabel_nodes(G, {}, copy=True) + assert H.graph == G.graph + H = nx.relabel_nodes(G, {}, copy=False) + assert H.graph == G.graph + G.name = "first" + H = nx.relabel_nodes(G, {}, copy=True) + assert H.graph == G.graph + H = nx.relabel_nodes(G, {}, copy=False) + assert H.graph == G.graph + + def test_relabel_toposort(self): + K5 = nx.complete_graph(4) + G = nx.complete_graph(4) + G = nx.relabel_nodes(G, {i: i + 1 for i in range(4)}, copy=False) + assert nx.is_isomorphic(K5, G) + G = nx.complete_graph(4) + G = nx.relabel_nodes(G, {i: i - 1 for i in range(4)}, copy=False) + assert nx.is_isomorphic(K5, G) + + def test_relabel_selfloop(self): + G = nx.DiGraph([(1, 1), (1, 2), (2, 3)]) + G = nx.relabel_nodes(G, {1: "One", 2: "Two", 3: "Three"}, copy=False) + assert nodes_equal(G.nodes(), ["One", "Three", "Two"]) + G = nx.MultiDiGraph([(1, 1), (1, 2), (2, 3)]) + G = nx.relabel_nodes(G, {1: "One", 2: "Two", 3: "Three"}, copy=False) + assert nodes_equal(G.nodes(), ["One", "Three", "Two"]) + G = nx.MultiDiGraph([(1, 1)]) + G = nx.relabel_nodes(G, {1: 0}, copy=False) + assert nodes_equal(G.nodes(), [0]) + + def test_relabel_multidigraph_inout_merge_nodes(self): + for MG in (nx.MultiGraph, nx.MultiDiGraph): + for cc in (True, False): + G = MG([(0, 4), (1, 4), (4, 2), (4, 3)]) + G[0][4][0]["value"] = "a" + G[1][4][0]["value"] = "b" + G[4][2][0]["value"] = "c" + G[4][3][0]["value"] = "d" + G.add_edge(0, 4, key="x", value="e") + G.add_edge(4, 3, key="x", value="f") + mapping = {0: 9, 1: 9, 2: 9, 3: 9} + H = nx.relabel_nodes(G, mapping, copy=cc) + # No ordering on keys enforced + assert {"value": "a"} in H[9][4].values() + assert {"value": "b"} in H[9][4].values() + assert {"value": "c"} in H[4][9].values() + assert len(H[4][9]) == 3 if G.is_directed() else 6 + assert {"value": "d"} in H[4][9].values() + assert {"value": "e"} in H[9][4].values() + assert {"value": "f"} in H[4][9].values() + assert len(H[9][4]) == 3 if G.is_directed() else 6 + + def test_relabel_multigraph_merge_inplace(self): + G = nx.MultiGraph([(0, 1), (0, 2), (0, 3), (0, 1), (0, 2), (0, 3)]) + G[0][1][0]["value"] = "a" + G[0][2][0]["value"] = "b" + G[0][3][0]["value"] = "c" + mapping = {1: 4, 2: 4, 3: 4} + nx.relabel_nodes(G, mapping, copy=False) + # No ordering on keys enforced + assert {"value": "a"} in G[0][4].values() + assert {"value": "b"} in G[0][4].values() + assert {"value": "c"} in G[0][4].values() + + def test_relabel_multidigraph_merge_inplace(self): + G = nx.MultiDiGraph([(0, 1), (0, 2), (0, 3)]) + G[0][1][0]["value"] = "a" + G[0][2][0]["value"] = "b" + G[0][3][0]["value"] = "c" + mapping = {1: 4, 2: 4, 3: 4} + nx.relabel_nodes(G, mapping, copy=False) + # No ordering on keys enforced + assert {"value": "a"} in G[0][4].values() + assert {"value": "b"} in G[0][4].values() + assert {"value": "c"} in G[0][4].values() + + def test_relabel_multidigraph_inout_copy(self): + G = nx.MultiDiGraph([(0, 4), (1, 4), (4, 2), (4, 3)]) + G[0][4][0]["value"] = "a" + G[1][4][0]["value"] = "b" + G[4][2][0]["value"] = "c" + G[4][3][0]["value"] = "d" + G.add_edge(0, 4, key="x", value="e") + G.add_edge(4, 3, key="x", value="f") + mapping = {0: 9, 1: 9, 2: 9, 3: 9} + H = nx.relabel_nodes(G, mapping, copy=True) + # No ordering on keys enforced + assert {"value": "a"} in H[9][4].values() + assert {"value": "b"} in H[9][4].values() + assert {"value": "c"} in H[4][9].values() + assert len(H[4][9]) == 3 + assert {"value": "d"} in H[4][9].values() + assert {"value": "e"} in H[9][4].values() + assert {"value": "f"} in H[4][9].values() + assert len(H[9][4]) == 3 + + def test_relabel_multigraph_merge_copy(self): + G = nx.MultiGraph([(0, 1), (0, 2), (0, 3)]) + G[0][1][0]["value"] = "a" + G[0][2][0]["value"] = "b" + G[0][3][0]["value"] = "c" + mapping = {1: 4, 2: 4, 3: 4} + H = nx.relabel_nodes(G, mapping, copy=True) + assert {"value": "a"} in H[0][4].values() + assert {"value": "b"} in H[0][4].values() + assert {"value": "c"} in H[0][4].values() + + def test_relabel_multidigraph_merge_copy(self): + G = nx.MultiDiGraph([(0, 1), (0, 2), (0, 3)]) + G[0][1][0]["value"] = "a" + G[0][2][0]["value"] = "b" + G[0][3][0]["value"] = "c" + mapping = {1: 4, 2: 4, 3: 4} + H = nx.relabel_nodes(G, mapping, copy=True) + assert {"value": "a"} in H[0][4].values() + assert {"value": "b"} in H[0][4].values() + assert {"value": "c"} in H[0][4].values() + + def test_relabel_multigraph_nonnumeric_key(self): + for MG in (nx.MultiGraph, nx.MultiDiGraph): + for cc in (True, False): + G = nx.MultiGraph() + G.add_edge(0, 1, key="I", value="a") + G.add_edge(0, 2, key="II", value="b") + G.add_edge(0, 3, key="II", value="c") + mapping = {1: 4, 2: 4, 3: 4} + nx.relabel_nodes(G, mapping, copy=False) + assert {"value": "a"} in G[0][4].values() + assert {"value": "b"} in G[0][4].values() + assert {"value": "c"} in G[0][4].values() + assert 0 in G[0][4] + assert "I" in G[0][4] + assert "II" in G[0][4] + + def test_relabel_circular(self): + G = nx.path_graph(3) + mapping = {0: 1, 1: 0} + H = nx.relabel_nodes(G, mapping, copy=True) + with pytest.raises(nx.NetworkXUnfeasible): + H = nx.relabel_nodes(G, mapping, copy=False) + + def test_relabel_preserve_node_order_full_mapping_with_copy_true(self): + G = nx.path_graph(3) + original_order = list(G.nodes()) + mapping = {2: "a", 1: "b", 0: "c"} # dictionary keys out of order on purpose + H = nx.relabel_nodes(G, mapping, copy=True) + new_order = list(H.nodes()) + assert [mapping.get(i, i) for i in original_order] == new_order + + def test_relabel_preserve_node_order_full_mapping_with_copy_false(self): + G = nx.path_graph(3) + original_order = list(G) + mapping = {2: "a", 1: "b", 0: "c"} # dictionary keys out of order on purpose + H = nx.relabel_nodes(G, mapping, copy=False) + new_order = list(H) + assert [mapping.get(i, i) for i in original_order] == new_order + + def test_relabel_preserve_node_order_partial_mapping_with_copy_true(self): + G = nx.path_graph(3) + original_order = list(G) + mapping = {1: "a", 0: "b"} # partial mapping and keys out of order on purpose + H = nx.relabel_nodes(G, mapping, copy=True) + new_order = list(H) + assert [mapping.get(i, i) for i in original_order] == new_order + + def test_relabel_preserve_node_order_partial_mapping_with_copy_false(self): + G = nx.path_graph(3) + original_order = list(G) + mapping = {1: "a", 0: "b"} # partial mapping and keys out of order on purpose + H = nx.relabel_nodes(G, mapping, copy=False) + new_order = list(H) + assert [mapping.get(i, i) for i in original_order] != new_order diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_removed_functions_exception_messages.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_removed_functions_exception_messages.py new file mode 100644 index 0000000000000000000000000000000000000000..4746345627689ef18d39660f51204622dec5dbdb --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/tests/test_removed_functions_exception_messages.py @@ -0,0 +1,8 @@ +import pytest + +import networkx as nx + + +def test_random_tree(): + with pytest.raises(AttributeError, match=".*Use `nx.random_labeled_tree` instead"): + nx.random_tree(3) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d6abb178e4fd014bcec4c781fe98b8917d3ec876 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__init__.py @@ -0,0 +1,8 @@ +from networkx.utils.misc import * +from networkx.utils.decorators import * +from networkx.utils.random_sequence import * +from networkx.utils.union_find import * +from networkx.utils.rcm import * +from networkx.utils.heaps import * +from networkx.utils.configs import * +from networkx.utils.backends import * diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2453937a279d4d3b8f0115ce67b6f5e7d21dda37 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/backends.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/backends.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..996c4be795e8f5a491660dc68a1005ba06069f48 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/backends.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/configs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/configs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfc849318ce4e82c98aadcb7b641b064e474c365 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/configs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/decorators.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/decorators.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0efe28d2ca8c96deb051d7ec2e773ea5000c30f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/decorators.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/heaps.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/heaps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7df0ff7b550cc1dac9f6014e31adece9ef6fb2a2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/heaps.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/mapped_queue.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/mapped_queue.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb20b830db4189f821396b5d07d4469d965aa338 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/mapped_queue.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/misc.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/misc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e09672985bbd17672b18db49e6a74afbc773226 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/misc.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/random_sequence.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/random_sequence.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7949fa6d7c24454934e9604784afb98dc5075f57 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/random_sequence.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/rcm.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/rcm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76014a2232d648b29ef8c2998fe0dda8a616abe9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/rcm.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/union_find.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/union_find.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b716ccc340babe3d8e81448e0759532b63112f1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/__pycache__/union_find.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/backends.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/backends.py new file mode 100644 index 0000000000000000000000000000000000000000..e7c99b5955957225cfc39d3868120e79b4f5af42 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/backends.py @@ -0,0 +1,2171 @@ +# Notes about NetworkX namespace objects set up here: +# +# nx.utils.backends.backends: +# dict keyed by backend name to the backend entry point object. +# Filled using ``_get_backends("networkx.backends")`` during import of this module. +# +# nx.utils.backends.backend_info: +# dict keyed by backend name to the metadata returned by the function indicated +# by the "networkx.backend_info" entry point. +# Created as an empty dict while importing this module, but later filled using +# ``_set_configs_from_environment()`` at end of importing ``networkx/__init__.py``. +# +# nx.config: +# Config object for NetworkX config setting. Created using +# ``_set_configs_from_environment()`` at end of importing ``networkx/__init__.py``. +# +# private dicts: +# nx.utils.backends._loaded_backends: +# dict used to memoize loaded backends. Keyed by backend name to loaded backends. +# +# nx.utils.backends._registered_algorithms: +# dict of all the dispatchable functions in networkx, keyed by _dispatchable +# function name to the wrapped function object. + +import inspect +import itertools +import logging +import os +import typing +import warnings +from functools import partial +from importlib.metadata import entry_points + +import networkx as nx + +from .configs import BackendPriorities, Config, NetworkXConfig +from .decorators import argmap + +__all__ = ["_dispatchable"] + +_logger = logging.getLogger(__name__) +FAILED_TO_CONVERT = "FAILED_TO_CONVERT" + + +def _get_backends(group, *, load_and_call=False): + """ + Retrieve NetworkX ``backends`` and ``backend_info`` from the entry points. + + Parameters + ----------- + group : str + The entry_point to be retrieved. + load_and_call : bool, optional + If True, load and call the backend. Defaults to False. + + Returns + -------- + dict + A dictionary mapping backend names to their respective backend objects. + + Notes + ------ + If a backend is defined more than once, a warning is issued. + If a backend name is not a valid Python identifier, the backend is + ignored and a warning is issued. + The "nx_loopback" backend is removed if it exists, as it is only available during testing. + A warning is displayed if an error occurs while loading a backend. + """ + items = entry_points(group=group) + rv = {} + for ep in items: + if not ep.name.isidentifier(): + warnings.warn( + f"networkx backend name is not a valid identifier: {ep.name!r}. Ignoring.", + RuntimeWarning, + stacklevel=2, + ) + elif ep.name in rv: + warnings.warn( + f"networkx backend defined more than once: {ep.name}", + RuntimeWarning, + stacklevel=2, + ) + elif load_and_call: + try: + rv[ep.name] = ep.load()() + except Exception as exc: + warnings.warn( + f"Error encountered when loading info for backend {ep.name}: {exc}", + RuntimeWarning, + stacklevel=2, + ) + else: + rv[ep.name] = ep + rv.pop("nx_loopback", None) + return rv + + +# Note: "networkx" is in `backend_info` but ignored in `backends` and `config.backends`. +# It is valid to use "networkx" as a backend argument and in `config.backend_priority`. +# If we make "networkx" a "proper" backend, put it in `backends` and `config.backends`. +backends = _get_backends("networkx.backends") + +# Use _set_configs_from_environment() below to fill backend_info dict as +# the last step in importing networkx +backend_info = {} + +# Load and cache backends on-demand +_loaded_backends = {} # type: ignore[var-annotated] +_registered_algorithms = {} + + +# Get default configuration from environment variables at import time +def _comma_sep_to_list(string): + return [x_strip for x in string.strip().split(",") if (x_strip := x.strip())] + + +def _set_configs_from_environment(): + """Initialize ``config.backend_priority``, load backend_info and config. + + This gets default values from environment variables (see ``nx.config`` for details). + This function is run at the very end of importing networkx. It is run at this time + to avoid loading backend_info before the rest of networkx is imported in case a + backend uses networkx for its backend_info (e.g. subclassing the Config class.) + """ + # backend_info is defined above as empty dict. Fill it after import finishes. + backend_info.update(_get_backends("networkx.backend_info", load_and_call=True)) + backend_info.update( + (backend, {}) for backend in backends.keys() - backend_info.keys() + ) + + # set up config based on backend_info and environment + backend_config = {} + for backend, info in backend_info.items(): + if "default_config" not in info: + cfg = Config() + else: + cfg = info["default_config"] + if not isinstance(cfg, Config): + cfg = Config(**cfg) + backend_config[backend] = cfg + backend_config = Config(**backend_config) + # Setting doc of backends_config type is not setting doc of Config + # Config has __new__ method that returns instance with a unique type! + type(backend_config).__doc__ = "All installed NetworkX backends and their configs." + + backend_priority = BackendPriorities(algos=[], generators=[], classes=[]) + + config = NetworkXConfig( + backend_priority=backend_priority, + backends=backend_config, + cache_converted_graphs=bool( + os.environ.get("NETWORKX_CACHE_CONVERTED_GRAPHS", True) + ), + fallback_to_nx=bool(os.environ.get("NETWORKX_FALLBACK_TO_NX", False)), + warnings_to_ignore=set( + _comma_sep_to_list(os.environ.get("NETWORKX_WARNINGS_TO_IGNORE", "")) + ), + ) + + # Add "networkx" item to backend_info now b/c backend_config is set up + backend_info["networkx"] = {} + + # NETWORKX_BACKEND_PRIORITY is the same as NETWORKX_BACKEND_PRIORITY_ALGOS + priorities = { + key[26:].lower(): val + for key, val in os.environ.items() + if key.startswith("NETWORKX_BACKEND_PRIORITY_") + } + backend_priority = config.backend_priority + backend_priority.algos = ( + _comma_sep_to_list(priorities.pop("algos")) + if "algos" in priorities + else _comma_sep_to_list( + os.environ.get( + "NETWORKX_BACKEND_PRIORITY", + os.environ.get("NETWORKX_AUTOMATIC_BACKENDS", ""), + ) + ) + ) + backend_priority.generators = _comma_sep_to_list(priorities.pop("generators", "")) + for key in sorted(priorities): + backend_priority[key] = _comma_sep_to_list(priorities[key]) + + return config + + +def _do_nothing(): + """This does nothing at all, yet it helps turn ``_dispatchable`` into functions. + + Use this with the ``argmap`` decorator to turn ``self`` into a function. It results + in some small additional overhead compared to calling ``_dispatchable`` directly, + but ``argmap`` has the property that it can stack with other ``argmap`` + decorators "for free". Being a function is better for REPRs and type-checkers. + """ + + +def _always_run(name, args, kwargs): + return True + + +def _load_backend(backend_name): + if backend_name in _loaded_backends: + return _loaded_backends[backend_name] + if backend_name not in backends: + raise ImportError(f"'{backend_name}' backend is not installed") + rv = _loaded_backends[backend_name] = backends[backend_name].load() + if not hasattr(rv, "can_run"): + rv.can_run = _always_run + if not hasattr(rv, "should_run"): + rv.should_run = _always_run + return rv + + +class _dispatchable: + _is_testing = False + + def __new__( + cls, + func=None, + *, + name=None, + graphs="G", + edge_attrs=None, + node_attrs=None, + preserve_edge_attrs=False, + preserve_node_attrs=False, + preserve_graph_attrs=False, + preserve_all_attrs=False, + mutates_input=False, + returns_graph=False, + implemented_by_nx=True, + ): + """A decorator function that is used to redirect the execution of ``func`` + function to its backend implementation. + + This decorator allows the function to dispatch to different backend + implementations based on the input graph types, and also manages the + extra keywords ``backend`` and ``**backend_kwargs``. + Usage can be any of the following decorator forms: + + - ``@_dispatchable`` + - ``@_dispatchable()`` + - ``@_dispatchable(name="override_name")`` + - ``@_dispatchable(graphs="graph_var_name")`` + - ``@_dispatchable(edge_attrs="weight")`` + - ``@_dispatchable(graphs={"G": 0, "H": 1}, edge_attrs={"weight": "default"})`` + with 0 and 1 giving the position in the signature function for graph + objects. When ``edge_attrs`` is a dict, keys are keyword names and values + are defaults. + + Parameters + ---------- + func : callable, optional (default: None) + The function to be decorated. If None, ``_dispatchable`` returns a + partial object that can be used to decorate a function later. If ``func`` + is a callable, returns a new callable object that dispatches to a backend + function based on input graph types. + + name : str, optional (default: name of `func`) + The dispatch name for the function. It defaults to the name of `func`, + but can be set manually to avoid conflicts in the global dispatch + namespace. A common pattern is to prefix the function name with its + module or submodule to make it unique. For example: + + - ``@_dispatchable(name="tournament_is_strongly_connected")`` + resolves conflict between ``nx.tournament.is_strongly_connected`` + and ``nx.is_strongly_connected``. + - ``@_dispatchable(name="approximate_node_connectivity")`` + resolves conflict between ``nx.approximation.node_connectivity`` + and ``nx.connectivity.node_connectivity``. + + graphs : str or dict or None, optional (default: "G") + If a string, the parameter name of the graph, which must be the first + argument of the wrapped function. If more than one graph is required + for the function (or if the graph is not the first argument), provide + a dict keyed by graph parameter name to the value parameter position. + A question mark in the name indicates an optional argument. + For example, ``@_dispatchable(graphs={"G": 0, "auxiliary?": 4})`` + indicates the 0th parameter ``G`` of the function is a required graph, + and the 4th parameter ``auxiliary?`` is an optional graph. + To indicate that an argument is a list of graphs, do ``"[graphs]"``. + Use ``graphs=None``, if *no* arguments are NetworkX graphs such as for + graph generators, readers, and conversion functions. + + edge_attrs : str or dict, optional (default: None) + ``edge_attrs`` holds information about edge attribute arguments + and default values for those edge attributes. + If a string, ``edge_attrs`` holds the function argument name that + indicates a single edge attribute to include in the converted graph. + The default value for this attribute is 1. To indicate that an argument + is a list of attributes (all with default value 1), use e.g. ``"[attrs]"``. + If a dict, ``edge_attrs`` holds a dict keyed by argument names, with + values that are either the default value or, if a string, the argument + name that indicates the default value. + If None, function does not use edge attributes. + + node_attrs : str or dict, optional + Like ``edge_attrs``, but for node attributes. + + preserve_edge_attrs : bool or str or dict, optional (default: False) + If bool, whether to preserve all edge attributes. + If a string, the parameter name that may indicate (with ``True`` or a + callable argument) whether all edge attributes should be preserved + when converting graphs to a backend graph type. + If a dict of form ``{graph_name: {attr: default}}``, indicate + pre-determined edge attributes (and defaults) to preserve for the + indicated input graph. + + preserve_node_attrs : bool or str or dict, optional (default: False) + Like ``preserve_edge_attrs``, but for node attributes. + + preserve_graph_attrs : bool or set, optional (default: False) + If bool, whether to preserve all graph attributes. + If set, which input graph arguments to preserve graph attributes. + + preserve_all_attrs : bool, optional (default: False) + Whether to preserve all edge, node and graph attributes. + If True, this overrides all the other preserve_*_attrs. + + mutates_input : bool or dict, optional (default: False) + If bool, whether the function mutates an input graph argument. + If dict of ``{arg_name: arg_pos}``, name and position of bool arguments + that indicate whether an input graph will be mutated, and ``arg_name`` + may begin with ``"not "`` to negate the logic (for example, ``"not copy"`` + means we mutate the input graph when the ``copy`` argument is False). + By default, dispatching doesn't convert input graphs to a different + backend for functions that mutate input graphs. + + returns_graph : bool, optional (default: False) + Whether the function can return or yield a graph object. By default, + dispatching doesn't convert input graphs to a different backend for + functions that return graphs. + + implemented_by_nx : bool, optional (default: True) + Whether the function is implemented by NetworkX. If it is not, then the + function is included in NetworkX only as an API to dispatch to backends. + Default is True. + """ + if func is None: + return partial( + _dispatchable, + name=name, + graphs=graphs, + edge_attrs=edge_attrs, + node_attrs=node_attrs, + preserve_edge_attrs=preserve_edge_attrs, + preserve_node_attrs=preserve_node_attrs, + preserve_graph_attrs=preserve_graph_attrs, + preserve_all_attrs=preserve_all_attrs, + mutates_input=mutates_input, + returns_graph=returns_graph, + implemented_by_nx=implemented_by_nx, + ) + if isinstance(func, str): + raise TypeError("'name' and 'graphs' must be passed by keyword") from None + # If name not provided, use the name of the function + if name is None: + name = func.__name__ + + self = object.__new__(cls) + + # standard function-wrapping stuff + # __annotations__ not used + self.__name__ = func.__name__ + # self.__doc__ = func.__doc__ # __doc__ handled as cached property + self.__defaults__ = func.__defaults__ + # Add `backend=` keyword argument to allow backend choice at call-time + if func.__kwdefaults__: + self.__kwdefaults__ = {**func.__kwdefaults__, "backend": None} + else: + self.__kwdefaults__ = {"backend": None} + self.__module__ = func.__module__ + self.__qualname__ = func.__qualname__ + self.__dict__.update(func.__dict__) + self.__wrapped__ = func + + # Supplement docstring with backend info; compute and cache when needed + self._orig_doc = func.__doc__ + self._cached_doc = None + + self.orig_func = func + self.name = name + self.edge_attrs = edge_attrs + self.node_attrs = node_attrs + self.preserve_edge_attrs = preserve_edge_attrs or preserve_all_attrs + self.preserve_node_attrs = preserve_node_attrs or preserve_all_attrs + self.preserve_graph_attrs = preserve_graph_attrs or preserve_all_attrs + self.mutates_input = mutates_input + # Keep `returns_graph` private for now, b/c we may extend info on return types + self._returns_graph = returns_graph + + if edge_attrs is not None and not isinstance(edge_attrs, str | dict): + raise TypeError( + f"Bad type for edge_attrs: {type(edge_attrs)}. Expected str or dict." + ) from None + if node_attrs is not None and not isinstance(node_attrs, str | dict): + raise TypeError( + f"Bad type for node_attrs: {type(node_attrs)}. Expected str or dict." + ) from None + if not isinstance(self.preserve_edge_attrs, bool | str | dict): + raise TypeError( + f"Bad type for preserve_edge_attrs: {type(self.preserve_edge_attrs)}." + " Expected bool, str, or dict." + ) from None + if not isinstance(self.preserve_node_attrs, bool | str | dict): + raise TypeError( + f"Bad type for preserve_node_attrs: {type(self.preserve_node_attrs)}." + " Expected bool, str, or dict." + ) from None + if not isinstance(self.preserve_graph_attrs, bool | set): + raise TypeError( + f"Bad type for preserve_graph_attrs: {type(self.preserve_graph_attrs)}." + " Expected bool or set." + ) from None + if not isinstance(self.mutates_input, bool | dict): + raise TypeError( + f"Bad type for mutates_input: {type(self.mutates_input)}." + " Expected bool or dict." + ) from None + if not isinstance(self._returns_graph, bool): + raise TypeError( + f"Bad type for returns_graph: {type(self._returns_graph)}." + " Expected bool." + ) from None + + if isinstance(graphs, str): + graphs = {graphs: 0} + elif graphs is None: + pass + elif not isinstance(graphs, dict): + raise TypeError( + f"Bad type for graphs: {type(graphs)}. Expected str or dict." + ) from None + elif len(graphs) == 0: + raise KeyError("'graphs' must contain at least one variable name") from None + + # This dict comprehension is complicated for better performance; equivalent shown below. + self.optional_graphs = set() + self.list_graphs = set() + if graphs is None: + self.graphs = {} + else: + self.graphs = { + self.optional_graphs.add(val := k[:-1]) or val + if (last := k[-1]) == "?" + else self.list_graphs.add(val := k[1:-1]) or val + if last == "]" + else k: v + for k, v in graphs.items() + } + # The above is equivalent to: + # self.optional_graphs = {k[:-1] for k in graphs if k[-1] == "?"} + # self.list_graphs = {k[1:-1] for k in graphs if k[-1] == "]"} + # self.graphs = {k[:-1] if k[-1] == "?" else k: v for k, v in graphs.items()} + + # Compute and cache the signature on-demand + self._sig = None + + # Which backends implement this function? + self.backends = { + backend + for backend, info in backend_info.items() + if "functions" in info and name in info["functions"] + } + if implemented_by_nx: + self.backends.add("networkx") + + if name in _registered_algorithms: + raise KeyError( + f"Algorithm already exists in dispatch namespace: {name}. " + "Fix by assigning a unique `name=` in the `@_dispatchable` decorator." + ) from None + # Use the `argmap` decorator to turn `self` into a function. This does result + # in small additional overhead compared to calling `_dispatchable` directly, + # but `argmap` has the property that it can stack with other `argmap` + # decorators "for free". Being a function is better for REPRs and type-checkers. + # It also allows `_dispatchable` to be used on class methods, since functions + # define `__get__`. Without using `argmap`, we would need to define `__get__`. + self = argmap(_do_nothing)(self) + _registered_algorithms[name] = self + return self + + @property + def __doc__(self): + """If the cached documentation exists, it is returned. + Otherwise, the documentation is generated using _make_doc() method, + cached, and then returned.""" + + rv = self._cached_doc + if rv is None: + rv = self._cached_doc = self._make_doc() + return rv + + @__doc__.setter + def __doc__(self, val): + """Sets the original documentation to the given value and resets the + cached documentation.""" + + self._orig_doc = val + self._cached_doc = None + + @property + def __signature__(self): + """Return the signature of the original function, with the addition of + the `backend` and `backend_kwargs` parameters.""" + + if self._sig is None: + sig = inspect.signature(self.orig_func) + # `backend` is now a reserved argument used by dispatching. + # assert "backend" not in sig.parameters + if not any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ): + sig = sig.replace( + parameters=[ + *sig.parameters.values(), + inspect.Parameter( + "backend", inspect.Parameter.KEYWORD_ONLY, default=None + ), + inspect.Parameter( + "backend_kwargs", inspect.Parameter.VAR_KEYWORD + ), + ] + ) + else: + *parameters, var_keyword = sig.parameters.values() + sig = sig.replace( + parameters=[ + *parameters, + inspect.Parameter( + "backend", inspect.Parameter.KEYWORD_ONLY, default=None + ), + var_keyword, + ] + ) + self._sig = sig + return self._sig + + # Fast, simple path if no backends are installed + def _call_if_no_backends_installed(self, /, *args, backend=None, **kwargs): + """Returns the result of the original function (no backends installed).""" + if backend is not None and backend != "networkx": + raise ImportError(f"'{backend}' backend is not installed") + if "networkx" not in self.backends: + raise NotImplementedError( + f"'{self.name}' is not implemented by 'networkx' backend. " + "This function is included in NetworkX as an API to dispatch to " + "other backends." + ) + return self.orig_func(*args, **kwargs) + + # Dispatch to backends based on inputs, `backend=` arg, or configuration + def _call_if_any_backends_installed(self, /, *args, backend=None, **kwargs): + """Returns the result of the original function, or the backend function if + the backend is specified and that backend implements `func`.""" + # Use `backend_name` in this function instead of `backend`. + # This is purely for aesthetics and to make it easier to search for this + # variable since "backend" is used in many comments and log/error messages. + backend_name = backend + if backend_name is not None and backend_name not in backend_info: + raise ImportError(f"'{backend_name}' backend is not installed") + + graphs_resolved = {} + for gname, pos in self.graphs.items(): + if pos < len(args): + if gname in kwargs: + raise TypeError(f"{self.name}() got multiple values for {gname!r}") + graph = args[pos] + elif gname in kwargs: + graph = kwargs[gname] + elif gname not in self.optional_graphs: + raise TypeError( + f"{self.name}() missing required graph argument: {gname}" + ) + else: + continue + if graph is None: + if gname not in self.optional_graphs: + raise TypeError( + f"{self.name}() required graph argument {gname!r} is None; must be a graph" + ) + else: + graphs_resolved[gname] = graph + + # Alternative to the above that does not check duplicated args or missing required graphs. + # graphs_resolved = { + # gname: graph + # for gname, pos in self.graphs.items() + # if (graph := args[pos] if pos < len(args) else kwargs.get(gname)) is not None + # } + + # Check if any graph comes from a backend + if self.list_graphs: + # Make sure we don't lose values by consuming an iterator + args = list(args) + for gname in self.list_graphs & graphs_resolved.keys(): + list_of_graphs = list(graphs_resolved[gname]) + graphs_resolved[gname] = list_of_graphs + if gname in kwargs: + kwargs[gname] = list_of_graphs + else: + args[self.graphs[gname]] = list_of_graphs + + graph_backend_names = { + getattr(g, "__networkx_backend__", None) + for gname, g in graphs_resolved.items() + if gname not in self.list_graphs + } + for gname in self.list_graphs & graphs_resolved.keys(): + graph_backend_names.update( + getattr(g, "__networkx_backend__", None) + for g in graphs_resolved[gname] + ) + else: + graph_backend_names = { + getattr(g, "__networkx_backend__", None) + for g in graphs_resolved.values() + } + + backend_priority = nx.config.backend_priority.get( + self.name, + nx.config.backend_priority.classes + if self.name.endswith("__new__") + else nx.config.backend_priority.generators + if self._returns_graph + else nx.config.backend_priority.algos, + ) + fallback_to_nx = nx.config.fallback_to_nx and "networkx" in self.backends + if self._is_testing and backend_priority and backend_name is None: + # Special path if we are running networkx tests with a backend. + # This even runs for (and handles) functions that mutate input graphs. + return self._convert_and_call_for_tests( + backend_priority[0], + args, + kwargs, + fallback_to_nx=fallback_to_nx, + ) + + graph_backend_names.discard(None) + if backend_name is not None: + # Must run with the given backend. + # `can_run` only used for better log and error messages. + # Check `mutates_input` for logging, not behavior. + backend_kwarg_msg = ( + "No other backends will be attempted, because the backend was " + f"specified with the `backend='{backend_name}'` keyword argument." + ) + extra_message = ( + f"'{backend_name}' backend raised NotImplementedError when calling " + f"'{self.name}'. {backend_kwarg_msg}" + ) + if not graph_backend_names or graph_backend_names == {backend_name}: + # All graphs are backend graphs--no need to convert! + if self._can_backend_run(backend_name, args, kwargs): + return self._call_with_backend( + backend_name, args, kwargs, extra_message=extra_message + ) + if self._does_backend_have(backend_name): + extra = " for the given arguments" + else: + extra = "" + raise NotImplementedError( + f"'{self.name}' is not implemented by '{backend_name}' backend" + f"{extra}. {backend_kwarg_msg}" + ) + if self._can_convert(backend_name, graph_backend_names): + if self._can_backend_run(backend_name, args, kwargs): + if self._will_call_mutate_input(args, kwargs): + _logger.debug( + "'%s' will mutate an input graph. This prevents automatic conversion " + "to, and use of, backends listed in `nx.config.backend_priority`. " + "Using backend specified by the " + "`backend='%s'` keyword argument. This may change behavior by not " + "mutating inputs.", + self.name, + backend_name, + ) + mutations = [] + else: + mutations = None + rv = self._convert_and_call( + backend_name, + graph_backend_names, + args, + kwargs, + extra_message=extra_message, + mutations=mutations, + ) + if mutations: + for cache, key in mutations: + # If the call mutates inputs, then remove all inputs gotten + # from cache. We do this after all conversions (and call) so + # that a graph can be gotten from a cache multiple times. + cache.pop(key, None) + return rv + if self._does_backend_have(backend_name): + extra = " for the given arguments" + else: + extra = "" + raise NotImplementedError( + f"'{self.name}' is not implemented by '{backend_name}' backend" + f"{extra}. {backend_kwarg_msg}" + ) + if len(graph_backend_names) == 1: + maybe_s = "" + graph_backend_names = f"'{next(iter(graph_backend_names))}'" + else: + maybe_s = "s" + raise TypeError( + f"'{self.name}' is unable to convert graph from backend{maybe_s} " + f"{graph_backend_names} to '{backend_name}' backend, which was " + f"specified with the `backend='{backend_name}'` keyword argument. " + f"{backend_kwarg_msg}" + ) + + if self._will_call_mutate_input(args, kwargs): + # The current behavior for functions that mutate input graphs: + # + # 1. If backend is specified by `backend=` keyword, use it (done above). + # 2. If inputs are from one backend, try to use it. + # 3. If all input graphs are instances of `nx.Graph`, then run with the + # default "networkx" implementation. + # + # Do not automatically convert if a call will mutate inputs, because doing + # so would change behavior. Hence, we should fail if there are multiple input + # backends or if the input backend does not implement the function. However, + # we offer a way for backends to circumvent this if they do not implement + # this function: we will fall back to the default "networkx" implementation + # without using conversions if all input graphs are subclasses of `nx.Graph`. + mutate_msg = ( + "conversions between backends (if configured) will not be attempted " + "because the original input graph would not be mutated. Using the " + "backend keyword e.g. `backend='some_backend'` will force conversions " + "and not mutate the original input graph." + ) + fallback_msg = ( + "This call will mutate inputs, so fall back to 'networkx' " + "backend (without converting) since all input graphs are " + "instances of nx.Graph and are hopefully compatible." + ) + if len(graph_backend_names) == 1: + [backend_name] = graph_backend_names + msg_template = ( + f"Backend '{backend_name}' does not implement '{self.name}'%s. " + f"This call will mutate an input, so automatic {mutate_msg}" + ) + # `can_run` is only used for better log and error messages + try: + if self._can_backend_run(backend_name, args, kwargs): + return self._call_with_backend( + backend_name, + args, + kwargs, + extra_message=msg_template % " with these arguments", + ) + except NotImplementedError as exc: + if all(isinstance(g, nx.Graph) for g in graphs_resolved.values()): + _logger.debug( + "Backend '%s' raised when calling '%s': %s. %s", + backend_name, + self.name, + exc, + fallback_msg, + ) + else: + raise + else: + if fallback_to_nx and all( + # Consider dropping the `isinstance` check here to allow + # duck-type graphs, but let's wait for a backend to ask us. + isinstance(g, nx.Graph) + for g in graphs_resolved.values() + ): + # Log that we are falling back to networkx + _logger.debug( + "Backend '%s' can't run '%s'. %s", + backend_name, + self.name, + fallback_msg, + ) + else: + if self._does_backend_have(backend_name): + extra = " with these arguments" + else: + extra = "" + raise NotImplementedError(msg_template % extra) + elif fallback_to_nx and all( + # Consider dropping the `isinstance` check here to allow + # duck-type graphs, but let's wait for a backend to ask us. + isinstance(g, nx.Graph) + for g in graphs_resolved.values() + ): + # Log that we are falling back to networkx + _logger.debug( + "'%s' was called with inputs from multiple backends: %s. %s", + self.name, + graph_backend_names, + fallback_msg, + ) + else: + raise RuntimeError( + f"'{self.name}' will mutate an input, but it was called with " + f"inputs from multiple backends: {graph_backend_names}. " + f"Automatic {mutate_msg}" + ) + # At this point, no backends are available to handle the call with + # the input graph types, but if the input graphs are compatible + # nx.Graph instances, fall back to networkx without converting. + return self.orig_func(*args, **kwargs) + + # We may generalize fallback configuration as e.g. `nx.config.backend_fallback` + if fallback_to_nx or not graph_backend_names: + # Use "networkx" by default if there are no inputs from backends. + # For example, graph generators should probably return NetworkX graphs + # instead of raising NotImplementedError. + backend_fallback = ["networkx"] + else: + backend_fallback = [] + + # ########################## + # # How this behaves today # + # ########################## + # + # The prose below describes the implementation and a *possible* way to + # generalize "networkx" as "just another backend". The code is structured + # to perhaps someday support backend-to-backend conversions (including + # simply passing objects from one backend directly to another backend; + # the dispatch machinery does not necessarily need to perform conversions), + # but since backend-to-backend matching is not yet supported, the following + # code is merely a convenient way to implement dispatch behaviors that have + # been carefully developed since NetworkX 3.0 and to include falling back + # to the default NetworkX implementation. + # + # The current behavior for functions that don't mutate input graphs: + # + # 1. If backend is specified by `backend=` keyword, use it (done above). + # 2. If input is from a backend other than "networkx", try to use it. + # - Note: if present, "networkx" graphs will be converted to the backend. + # 3. If input is from "networkx" (or no backend), try to use backends from + # `backend_priority` before running with the default "networkx" implementation. + # 4. If configured, "fall back" and run with the default "networkx" implementation. + # + # ################################################ + # # How this is implemented and may work someday # + # ################################################ + # + # Let's determine the order of backends we should try according + # to `backend_priority`, `backend_fallback`, and input backends. + # There are two† dimensions of priorities to consider: + # backend_priority > unspecified > backend_fallback + # and + # backend of an input > not a backend of an input + # These are combined to form five groups of priorities as such: + # + # input ~input + # +-------+-------+ + # backend_priority | 1 | 2 | + # unspecified | 3 | N/A | (if only 1) + # backend_fallback | 4 | 5 | + # +-------+-------+ + # + # This matches the behaviors we developed in versions 3.0 to 3.2, it + # ought to cover virtually all use cases we expect, and I (@eriknw) don't + # think it can be done any simpler (although it can be generalized further + # and made to be more complicated to capture 100% of *possible* use cases). + # Some observations: + # + # 1. If an input is in `backend_priority`, it will be used before trying a + # backend that is higher priority in `backend_priority` and not an input. + # 2. To prioritize converting from one backend to another even if both implement + # a function, list one in `backend_priority` and one in `backend_fallback`. + # 3. To disable conversions, set `backend_priority` and `backend_fallback` to []. + # + # †: There is actually a third dimension of priorities: + # should_run == True > should_run == False + # Backends with `can_run == True` and `should_run == False` are tried last. + # + seen = set() + group1 = [] # In backend_priority, and an input + group2 = [] # In backend_priority, but not an input + for name in backend_priority: + if name in seen: + continue + seen.add(name) + if name in graph_backend_names: + group1.append(name) + else: + group2.append(name) + group4 = [] # In backend_fallback, and an input + group5 = [] # In backend_fallback, but not an input + for name in backend_fallback: + if name in seen: + continue + seen.add(name) + if name in graph_backend_names: + group4.append(name) + else: + group5.append(name) + # An input, but not in backend_priority or backend_fallback. + group3 = graph_backend_names - seen + if len(group3) > 1: + # `group3` backends are not configured for automatic conversion or fallback. + # There are at least two issues if this group contains multiple backends: + # + # 1. How should we prioritize them? We have no good way to break ties. + # Although we could arbitrarily choose alphabetical or left-most, + # let's follow the Zen of Python and refuse the temptation to guess. + # 2. We probably shouldn't automatically convert to these backends, + # because we are not configured to do so. + # + # (2) is important to allow disabling all conversions by setting both + # `nx.config.backend_priority` and `nx.config.backend_fallback` to []. + # + # If there is a single backend in `group3`, then giving it priority over + # the fallback backends is what is generally expected. For example, this + # allows input graphs of `backend_fallback` backends (such as "networkx") + # to be converted to, and run with, the unspecified backend. + _logger.debug( + "Call to '%s' has inputs from multiple backends, %s, that " + "have no priority set in `nx.config.backend_priority`, " + "so automatic conversions to " + "these backends will not be attempted.", + self.name, + group3, + ) + group3 = () + + try_order = list(itertools.chain(group1, group2, group3, group4, group5)) + if len(try_order) > 1: + # Should we consider adding an option for more verbose logging? + # For example, we could explain the order of `try_order` in detail. + _logger.debug( + "Call to '%s' has inputs from %s backends, and will try to use " + "backends in the following order: %s", + self.name, + graph_backend_names or "no", + try_order, + ) + backends_to_try_again = [] + for is_not_first, backend_name in enumerate(try_order): + if is_not_first: + _logger.debug("Trying next backend: '%s'", backend_name) + try: + if not graph_backend_names or graph_backend_names == {backend_name}: + if self._can_backend_run(backend_name, args, kwargs): + return self._call_with_backend(backend_name, args, kwargs) + elif self._can_convert( + backend_name, graph_backend_names + ) and self._can_backend_run(backend_name, args, kwargs): + if self._should_backend_run(backend_name, args, kwargs): + rv = self._convert_and_call( + backend_name, graph_backend_names, args, kwargs + ) + if ( + self._returns_graph + and graph_backend_names + and backend_name not in graph_backend_names + ): + # If the function has graph inputs and graph output, we try + # to make it so the backend of the return type will match the + # backend of the input types. In case this is not possible, + # let's tell the user that the backend of the return graph + # has changed. Perhaps we could try to convert back, but + # "fallback" backends for graph generators should typically + # be compatible with NetworkX graphs. + _logger.debug( + "Call to '%s' is returning a graph from a different " + "backend! It has inputs from %s backends, but ran with " + "'%s' backend and is returning graph from '%s' backend", + self.name, + graph_backend_names, + backend_name, + backend_name, + ) + return rv + # `should_run` is False, but `can_run` is True, so try again later + backends_to_try_again.append(backend_name) + except NotImplementedError as exc: + _logger.debug( + "Backend '%s' raised when calling '%s': %s", + backend_name, + self.name, + exc, + ) + + # We are about to fail. Let's try backends with can_run=True and should_run=False. + # This is unlikely to help today since we try to run with "networkx" before this. + for backend_name in backends_to_try_again: + _logger.debug( + "Trying backend: '%s' (ignoring `should_run=False`)", backend_name + ) + try: + rv = self._convert_and_call( + backend_name, graph_backend_names, args, kwargs + ) + if ( + self._returns_graph + and graph_backend_names + and backend_name not in graph_backend_names + ): + _logger.debug( + "Call to '%s' is returning a graph from a different " + "backend! It has inputs from %s backends, but ran with " + "'%s' backend and is returning graph from '%s' backend", + self.name, + graph_backend_names, + backend_name, + backend_name, + ) + return rv + except NotImplementedError as exc: + _logger.debug( + "Backend '%s' raised when calling '%s': %s", + backend_name, + self.name, + exc, + ) + # As a final effort, we could try to convert and run with `group3` backends + # that we discarded when `len(group3) > 1`, but let's not consider doing + # so until there is a reasonable request for it. + + if len(unspecified_backends := graph_backend_names - seen) > 1: + raise TypeError( + f"Unable to convert inputs from {graph_backend_names} backends and " + f"run '{self.name}'. NetworkX is configured to automatically convert " + f"to {try_order} backends. To remedy this, you may enable automatic " + f"conversion to {unspecified_backends} backends by adding them to " + "`nx.config.backend_priority`, or you " + "may specify a backend to use with the `backend=` keyword argument." + ) + if "networkx" not in self.backends: + extra = ( + " This function is included in NetworkX as an API to dispatch to " + "other backends." + ) + else: + extra = "" + raise NotImplementedError( + f"'{self.name}' is not implemented by {try_order} backends. To remedy " + "this, you may enable automatic conversion to more backends (including " + "'networkx') by adding them to `nx.config.backend_priority`, " + "or you may specify a backend to use with " + f"the `backend=` keyword argument.{extra}" + ) + + # Dispatch only if there exist any installed backend(s) + __call__: typing.Callable = ( + _call_if_any_backends_installed if backends else _call_if_no_backends_installed + ) + + def _will_call_mutate_input(self, args, kwargs): + # Fairly few nx functions mutate the input graph. Most that do, always do. + # So a boolean input indicates "always" or "never". + if isinstance((mutates_input := self.mutates_input), bool): + return mutates_input + + # The ~10 other nx functions either use "copy=True" to control mutation or + # an arg naming an edge/node attribute to mutate (None means no mutation). + # Now `mutates_input` is a dict keyed by arg_name to its func-sig position. + # The `copy=` args are keyed as "not copy" to mean "negate the copy argument". + # Keys w/o "not " mean the call mutates only when the arg value `is not None`. + # + # This section might need different code if new functions mutate in new ways. + # + # NetworkX doesn't have any `mutates_input` dicts with more than 1 item. + # But we treat it like it might have more than 1 item for generality. + n = len(args) + return any( + (args[arg_pos] if n > arg_pos else kwargs.get(arg_name)) is not None + if not arg_name.startswith("not ") + # This assumes that e.g. `copy=True` is the default + else not (args[arg_pos] if n > arg_pos else kwargs.get(arg_name[4:], True)) + for arg_name, arg_pos in mutates_input.items() + ) + + def _can_convert(self, backend_name, graph_backend_names): + # Backend-to-backend conversion not supported yet. + # We can only convert to and from networkx. + rv = backend_name == "networkx" or graph_backend_names.issubset( + {"networkx", backend_name} + ) + if not rv: + _logger.debug( + "Unable to convert from %s backends to '%s' backend", + graph_backend_names, + backend_name, + ) + return rv + + def _does_backend_have(self, backend_name): + """Does the specified backend have this algorithm?""" + if backend_name == "networkx": + return "networkx" in self.backends + # Inspect the backend; don't trust metadata used to create `self.backends` + backend = _load_backend(backend_name) + return hasattr(backend, self.name) + + def _can_backend_run(self, backend_name, args, kwargs): + """Can the specified backend run this algorithm with these arguments?""" + if backend_name == "networkx": + return "networkx" in self.backends + backend = _load_backend(backend_name) + # `backend.can_run` and `backend.should_run` may return strings that describe + # why they can't or shouldn't be run. + if not hasattr(backend, self.name): + _logger.debug( + "Backend '%s' does not implement '%s'", backend_name, self.name + ) + return False + can_run = backend.can_run(self.name, args, kwargs) + if isinstance(can_run, str) or not can_run: + reason = f", because: {can_run}" if isinstance(can_run, str) else "" + _logger.debug( + "Backend '%s' can't run `%s` with arguments: %s%s", + backend_name, + self.name, + _LazyArgsRepr(self, args, kwargs), + reason, + ) + return False + return True + + def _should_backend_run(self, backend_name, args, kwargs): + """Should the specified backend run this algorithm with these arguments? + + Note that this does not check ``backend.can_run``. + """ + # `backend.can_run` and `backend.should_run` may return strings that describe + # why they can't or shouldn't be run. + # `_should_backend_run` may assume that `_can_backend_run` returned True. + if backend_name == "networkx": + return True + backend = _load_backend(backend_name) + should_run = backend.should_run(self.name, args, kwargs) + if isinstance(should_run, str) or not should_run: + reason = f", because: {should_run}" if isinstance(should_run, str) else "" + _logger.debug( + "Backend '%s' shouldn't run `%s` with arguments: %s%s", + backend_name, + self.name, + _LazyArgsRepr(self, args, kwargs), + reason, + ) + return False + return True + + def _convert_arguments(self, backend_name, args, kwargs, *, use_cache, mutations): + """Convert graph arguments to the specified backend. + + Returns + ------- + args tuple and kwargs dict + """ + bound = self.__signature__.bind(*args, **kwargs) + bound.apply_defaults() + if not self.graphs: + bound_kwargs = bound.kwargs + del bound_kwargs["backend"] + return bound.args, bound_kwargs + if backend_name == "networkx": + # `backend_interface.convert_from_nx` preserves everything + preserve_edge_attrs = preserve_node_attrs = preserve_graph_attrs = True + else: + preserve_edge_attrs = self.preserve_edge_attrs + preserve_node_attrs = self.preserve_node_attrs + preserve_graph_attrs = self.preserve_graph_attrs + edge_attrs = self.edge_attrs + node_attrs = self.node_attrs + # Convert graphs into backend graph-like object + # Include the edge and/or node labels if provided to the algorithm + if preserve_edge_attrs is False: + # e.g. `preserve_edge_attrs=False` + pass + elif preserve_edge_attrs is True: + # e.g. `preserve_edge_attrs=True` + edge_attrs = None + elif isinstance(preserve_edge_attrs, str): + if bound.arguments[preserve_edge_attrs] is True or callable( + bound.arguments[preserve_edge_attrs] + ): + # e.g. `preserve_edge_attrs="attr"` and `func(attr=True)` + # e.g. `preserve_edge_attrs="attr"` and `func(attr=myfunc)` + preserve_edge_attrs = True + edge_attrs = None + elif bound.arguments[preserve_edge_attrs] is False and ( + isinstance(edge_attrs, str) + and edge_attrs == preserve_edge_attrs + or isinstance(edge_attrs, dict) + and preserve_edge_attrs in edge_attrs + ): + # e.g. `preserve_edge_attrs="attr"` and `func(attr=False)` + # Treat `False` argument as meaning "preserve_edge_data=False" + # and not `False` as the edge attribute to use. + preserve_edge_attrs = False + edge_attrs = None + else: + # e.g. `preserve_edge_attrs="attr"` and `func(attr="weight")` + preserve_edge_attrs = False + # Else: e.g. `preserve_edge_attrs={"G": {"weight": 1}}` + + if edge_attrs is None: + # May have been set to None above b/c all attributes are preserved + pass + elif isinstance(edge_attrs, str): + if edge_attrs[0] == "[": + # e.g. `edge_attrs="[edge_attributes]"` (argument of list of attributes) + # e.g. `func(edge_attributes=["foo", "bar"])` + edge_attrs = { + edge_attr: 1 for edge_attr in bound.arguments[edge_attrs[1:-1]] + } + elif callable(bound.arguments[edge_attrs]): + # e.g. `edge_attrs="weight"` and `func(weight=myfunc)` + preserve_edge_attrs = True + edge_attrs = None + elif bound.arguments[edge_attrs] is not None: + # e.g. `edge_attrs="weight"` and `func(weight="foo")` (default of 1) + edge_attrs = {bound.arguments[edge_attrs]: 1} + elif self.name == "to_numpy_array" and hasattr( + bound.arguments["dtype"], "names" + ): + # Custom handling: attributes may be obtained from `dtype` + edge_attrs = { + edge_attr: 1 for edge_attr in bound.arguments["dtype"].names + } + else: + # e.g. `edge_attrs="weight"` and `func(weight=None)` + edge_attrs = None + else: + # e.g. `edge_attrs={"attr": "default"}` and `func(attr="foo", default=7)` + # e.g. `edge_attrs={"attr": 0}` and `func(attr="foo")` + edge_attrs = { + edge_attr: bound.arguments.get(val, 1) if isinstance(val, str) else val + for key, val in edge_attrs.items() + if (edge_attr := bound.arguments[key]) is not None + } + + if preserve_node_attrs is False: + # e.g. `preserve_node_attrs=False` + pass + elif preserve_node_attrs is True: + # e.g. `preserve_node_attrs=True` + node_attrs = None + elif isinstance(preserve_node_attrs, str): + if bound.arguments[preserve_node_attrs] is True or callable( + bound.arguments[preserve_node_attrs] + ): + # e.g. `preserve_node_attrs="attr"` and `func(attr=True)` + # e.g. `preserve_node_attrs="attr"` and `func(attr=myfunc)` + preserve_node_attrs = True + node_attrs = None + elif bound.arguments[preserve_node_attrs] is False and ( + isinstance(node_attrs, str) + and node_attrs == preserve_node_attrs + or isinstance(node_attrs, dict) + and preserve_node_attrs in node_attrs + ): + # e.g. `preserve_node_attrs="attr"` and `func(attr=False)` + # Treat `False` argument as meaning "preserve_node_data=False" + # and not `False` as the node attribute to use. Is this used? + preserve_node_attrs = False + node_attrs = None + else: + # e.g. `preserve_node_attrs="attr"` and `func(attr="weight")` + preserve_node_attrs = False + # Else: e.g. `preserve_node_attrs={"G": {"pos": None}}` + + if node_attrs is None: + # May have been set to None above b/c all attributes are preserved + pass + elif isinstance(node_attrs, str): + if node_attrs[0] == "[": + # e.g. `node_attrs="[node_attributes]"` (argument of list of attributes) + # e.g. `func(node_attributes=["foo", "bar"])` + node_attrs = { + node_attr: None for node_attr in bound.arguments[node_attrs[1:-1]] + } + elif callable(bound.arguments[node_attrs]): + # e.g. `node_attrs="weight"` and `func(weight=myfunc)` + preserve_node_attrs = True + node_attrs = None + elif bound.arguments[node_attrs] is not None: + # e.g. `node_attrs="weight"` and `func(weight="foo")` + node_attrs = {bound.arguments[node_attrs]: None} + else: + # e.g. `node_attrs="weight"` and `func(weight=None)` + node_attrs = None + else: + # e.g. `node_attrs={"attr": "default"}` and `func(attr="foo", default=7)` + # e.g. `node_attrs={"attr": 0}` and `func(attr="foo")` + node_attrs = { + node_attr: bound.arguments.get(val) if isinstance(val, str) else val + for key, val in node_attrs.items() + if (node_attr := bound.arguments[key]) is not None + } + + # It should be safe to assume that we either have networkx graphs or backend graphs. + # Future work: allow conversions between backends. + for gname in self.graphs: + if gname in self.list_graphs: + bound.arguments[gname] = [ + self._convert_graph( + backend_name, + g, + edge_attrs=edge_attrs, + node_attrs=node_attrs, + preserve_edge_attrs=preserve_edge_attrs, + preserve_node_attrs=preserve_node_attrs, + preserve_graph_attrs=preserve_graph_attrs, + graph_name=gname, + use_cache=use_cache, + mutations=mutations, + ) + if getattr(g, "__networkx_backend__", "networkx") != backend_name + else g + for g in bound.arguments[gname] + ] + else: + graph = bound.arguments[gname] + if graph is None: + if gname in self.optional_graphs: + continue + raise TypeError( + f"Missing required graph argument `{gname}` in {self.name} function" + ) + if isinstance(preserve_edge_attrs, dict): + preserve_edges = False + edges = preserve_edge_attrs.get(gname, edge_attrs) + else: + preserve_edges = preserve_edge_attrs + edges = edge_attrs + if isinstance(preserve_node_attrs, dict): + preserve_nodes = False + nodes = preserve_node_attrs.get(gname, node_attrs) + else: + preserve_nodes = preserve_node_attrs + nodes = node_attrs + if isinstance(preserve_graph_attrs, set): + preserve_graph = gname in preserve_graph_attrs + else: + preserve_graph = preserve_graph_attrs + if getattr(graph, "__networkx_backend__", "networkx") != backend_name: + bound.arguments[gname] = self._convert_graph( + backend_name, + graph, + edge_attrs=edges, + node_attrs=nodes, + preserve_edge_attrs=preserve_edges, + preserve_node_attrs=preserve_nodes, + preserve_graph_attrs=preserve_graph, + graph_name=gname, + use_cache=use_cache, + mutations=mutations, + ) + bound_kwargs = bound.kwargs + del bound_kwargs["backend"] + return bound.args, bound_kwargs + + def _convert_graph( + self, + backend_name, + graph, + *, + edge_attrs, + node_attrs, + preserve_edge_attrs, + preserve_node_attrs, + preserve_graph_attrs, + graph_name, + use_cache, + mutations, + ): + nx_cache = getattr(graph, "__networkx_cache__", None) if use_cache else None + if nx_cache is not None: + cache = nx_cache.setdefault("backends", {}).setdefault(backend_name, {}) + key = _get_cache_key( + edge_attrs=edge_attrs, + node_attrs=node_attrs, + preserve_edge_attrs=preserve_edge_attrs, + preserve_node_attrs=preserve_node_attrs, + preserve_graph_attrs=preserve_graph_attrs, + ) + compat_key, rv = _get_from_cache(cache, key, mutations=mutations) + if rv is not None: + if "cache" not in nx.config.warnings_to_ignore: + warnings.warn( + "Note: conversions to backend graphs are saved to cache " + "(`G.__networkx_cache__` on the original graph) by default." + "\n\nThis warning means the cached graph is being used " + f"for the {backend_name!r} backend in the " + f"call to {self.name}.\n\nFor the cache to be consistent " + "(i.e., correct), the input graph must not have been " + "manually mutated since the cached graph was created. " + "Examples of manually mutating the graph data structures " + "resulting in an inconsistent cache include:\n\n" + " >>> G[u][v][key] = val\n\n" + "and\n\n" + " >>> for u, v, d in G.edges(data=True):\n" + " ... d[key] = val\n\n" + "Using methods such as `G.add_edge(u, v, weight=val)` " + "will correctly clear the cache to keep it consistent. " + "You may also use `G.__networkx_cache__.clear()` to " + "manually clear the cache, or set `G.__networkx_cache__` " + "to None to disable caching for G. Enable or disable caching " + "globally via `nx.config.cache_converted_graphs` config.\n\n" + "To disable this warning:\n\n" + ' >>> nx.config.warnings_to_ignore.add("cache")\n' + ) + if rv == FAILED_TO_CONVERT: + # NotImplementedError is reasonable to use since the backend doesn't + # implement this conversion. However, this will be different than + # the original exception that the backend raised when it failed. + # Using NotImplementedError allows the next backend to be attempted. + raise NotImplementedError( + "Graph conversion aborted: unable to convert graph to " + f"'{backend_name}' backend in call to `{self.name}', " + "because this conversion has previously failed." + ) + _logger.debug( + "Using cached converted graph (from '%s' to '%s' backend) " + "in call to '%s' for '%s' argument", + getattr(graph, "__networkx_backend__", None), + backend_name, + self.name, + graph_name, + ) + return rv + + if backend_name == "networkx": + # Perhaps we should check that "__networkx_backend__" attribute exists + # and return the original object if not. + if not hasattr(graph, "__networkx_backend__"): + _logger.debug( + "Unable to convert input to 'networkx' backend in call to '%s' for " + "'%s argument, because it is not from a backend (i.e., it does not " + "have `G.__networkx_backend__` attribute). Using the original " + "object: %s", + self.name, + graph_name, + graph, + ) + # This may fail, but let it fail in the networkx function + return graph + backend = _load_backend(graph.__networkx_backend__) + try: + rv = backend.convert_to_nx(graph) + except Exception: + if nx_cache is not None: + _set_to_cache(cache, key, FAILED_TO_CONVERT) + raise + else: + backend = _load_backend(backend_name) + try: + rv = backend.convert_from_nx( + graph, + edge_attrs=edge_attrs, + node_attrs=node_attrs, + preserve_edge_attrs=preserve_edge_attrs, + preserve_node_attrs=preserve_node_attrs, + # Always preserve graph attrs when we are caching b/c this should be + # cheap and may help prevent extra (unnecessary) conversions. Because + # we do this, we don't need `preserve_graph_attrs` in the cache key. + preserve_graph_attrs=preserve_graph_attrs or nx_cache is not None, + name=self.name, + graph_name=graph_name, + ) + except Exception: + if nx_cache is not None: + _set_to_cache(cache, key, FAILED_TO_CONVERT) + raise + if nx_cache is not None: + _set_to_cache(cache, key, rv) + _logger.debug( + "Caching converted graph (from '%s' to '%s' backend) " + "in call to '%s' for '%s' argument", + getattr(graph, "__networkx_backend__", None), + backend_name, + self.name, + graph_name, + ) + + return rv + + def _call_with_backend(self, backend_name, args, kwargs, *, extra_message=None): + """Call this dispatchable function with a backend without converting inputs.""" + if backend_name == "networkx": + return self.orig_func(*args, **kwargs) + backend = _load_backend(backend_name) + _logger.debug( + "Using backend '%s' for call to '%s' with arguments: %s", + backend_name, + self.name, + _LazyArgsRepr(self, args, kwargs), + ) + try: + return getattr(backend, self.name)(*args, **kwargs) + except NotImplementedError as exc: + if extra_message is not None: + _logger.debug( + "Backend '%s' raised when calling '%s': %s", + backend_name, + self.name, + exc, + ) + raise NotImplementedError(extra_message) from exc + raise + + def _convert_and_call( + self, + backend_name, + input_backend_names, + args, + kwargs, + *, + extra_message=None, + mutations=None, + ): + """Call this dispatchable function with a backend after converting inputs. + + Parameters + ---------- + backend_name : str + input_backend_names : set[str] + args : arguments tuple + kwargs : keywords dict + extra_message : str, optional + Additional message to log if NotImplementedError is raised by backend. + mutations : list, optional + Used to clear objects gotten from cache if inputs will be mutated. + """ + if backend_name == "networkx": + func = self.orig_func + else: + backend = _load_backend(backend_name) + func = getattr(backend, self.name) + other_backend_names = input_backend_names - {backend_name} + _logger.debug( + "Converting input graphs from %s backend%s to '%s' backend for call to '%s'", + other_backend_names + if len(other_backend_names) > 1 + else f"'{next(iter(other_backend_names))}'", + "s" if len(other_backend_names) > 1 else "", + backend_name, + self.name, + ) + try: + converted_args, converted_kwargs = self._convert_arguments( + backend_name, + args, + kwargs, + use_cache=nx.config.cache_converted_graphs, + mutations=mutations, + ) + except NotImplementedError as exc: + # Only log the exception if we are adding an extra message + # because we don't want to lose any information. + _logger.debug( + "Failed to convert graphs from %s to '%s' backend for call to '%s'" + + ("" if extra_message is None else ": %s"), + input_backend_names, + backend_name, + self.name, + *(() if extra_message is None else (exc,)), + ) + if extra_message is not None: + raise NotImplementedError(extra_message) from exc + raise + if backend_name != "networkx": + _logger.debug( + "Using backend '%s' for call to '%s' with arguments: %s", + backend_name, + self.name, + _LazyArgsRepr(self, converted_args, converted_kwargs), + ) + try: + return func(*converted_args, **converted_kwargs) + except NotImplementedError as exc: + if extra_message is not None: + _logger.debug( + "Backend '%s' raised when calling '%s': %s", + backend_name, + self.name, + exc, + ) + raise NotImplementedError(extra_message) from exc + raise + + def _convert_and_call_for_tests( + self, backend_name, args, kwargs, *, fallback_to_nx=False + ): + """Call this dispatchable function with a backend; for use with testing.""" + backend = _load_backend(backend_name) + if not self._can_backend_run(backend_name, args, kwargs): + if fallback_to_nx or not self.graphs: + if fallback_to_nx: + _logger.debug( + "Falling back to use 'networkx' instead of '%s' backend " + "for call to '%s' with arguments: %s", + backend_name, + self.name, + _LazyArgsRepr(self, args, kwargs), + ) + return self.orig_func(*args, **kwargs) + + import pytest + + msg = f"'{self.name}' not implemented by {backend_name}" + if hasattr(backend, self.name): + msg += " with the given arguments" + pytest.xfail(msg) + + from collections.abc import Iterable, Iterator, Mapping + from copy import copy, deepcopy + from io import BufferedReader, BytesIO, StringIO, TextIOWrapper + from itertools import tee + from random import Random + + import numpy as np + from numpy.random import Generator, RandomState + from scipy.sparse import sparray + + # We sometimes compare the backend result (or input graphs) to the + # original result (or input graphs), so we need two sets of arguments. + compare_result_to_nx = ( + self._returns_graph + and "networkx" in self.backends + and self.name + not in { + # Has graphs as node values (unable to compare) + "quotient_graph", + # We don't handle tempfile.NamedTemporaryFile arguments + "read_gml", + "read_graph6", + "read_sparse6", + # We don't handle io.BufferedReader or io.TextIOWrapper arguments + "bipartite_read_edgelist", + "read_adjlist", + "read_edgelist", + "read_graphml", + "read_multiline_adjlist", + "read_pajek", + "from_pydot", + "pydot_read_dot", + "agraph_read_dot", + # graph comparison fails b/c of nan values + "read_gexf", + } + ) + compare_inputs_to_nx = ( + "networkx" in self.backends and self._will_call_mutate_input(args, kwargs) + ) + + # Tee iterators and copy random state so that they may be used twice. + if not args or not compare_result_to_nx and not compare_inputs_to_nx: + args_to_convert = args_nx = args + else: + args_to_convert, args_nx = zip( + *( + (arg, deepcopy(arg)) + if isinstance(arg, RandomState) + else (arg, copy(arg)) + if isinstance(arg, BytesIO | StringIO | Random | Generator) + else tee(arg) + if isinstance(arg, Iterator) + and not isinstance(arg, BufferedReader | TextIOWrapper) + else (arg, arg) + for arg in args + ) + ) + if not kwargs or not compare_result_to_nx and not compare_inputs_to_nx: + kwargs_to_convert = kwargs_nx = kwargs + else: + kwargs_to_convert, kwargs_nx = zip( + *( + ((k, v), (k, deepcopy(v))) + if isinstance(v, RandomState) + else ((k, v), (k, copy(v))) + if isinstance(v, BytesIO | StringIO | Random | Generator) + else ((k, (teed := tee(v))[0]), (k, teed[1])) + if isinstance(v, Iterator) + and not isinstance(v, BufferedReader | TextIOWrapper) + else ((k, v), (k, v)) + for k, v in kwargs.items() + ) + ) + kwargs_to_convert = dict(kwargs_to_convert) + kwargs_nx = dict(kwargs_nx) + + try: + converted_args, converted_kwargs = self._convert_arguments( + backend_name, + args_to_convert, + kwargs_to_convert, + use_cache=False, + mutations=None, + ) + except NotImplementedError as exc: + if fallback_to_nx: + _logger.debug( + "Graph conversion failed; falling back to use 'networkx' instead " + "of '%s' backend for call to '%s'", + backend_name, + self.name, + ) + return self.orig_func(*args_nx, **kwargs_nx) + import pytest + + pytest.xfail( + exc.args[0] if exc.args else f"{self.name} raised {type(exc).__name__}" + ) + + if compare_inputs_to_nx: + # Ensure input graphs are different if the function mutates an input graph. + bound_backend = self.__signature__.bind(*converted_args, **converted_kwargs) + bound_backend.apply_defaults() + bound_nx = self.__signature__.bind(*args_nx, **kwargs_nx) + bound_nx.apply_defaults() + for gname in self.graphs: + graph_nx = bound_nx.arguments[gname] + if bound_backend.arguments[gname] is graph_nx is not None: + bound_nx.arguments[gname] = graph_nx.copy() + args_nx = bound_nx.args + kwargs_nx = bound_nx.kwargs + kwargs_nx.pop("backend", None) + + _logger.debug( + "Using backend '%s' for call to '%s' with arguments: %s", + backend_name, + self.name, + _LazyArgsRepr(self, converted_args, converted_kwargs), + ) + try: + result = getattr(backend, self.name)(*converted_args, **converted_kwargs) + except NotImplementedError as exc: + if fallback_to_nx: + _logger.debug( + "Backend '%s' raised when calling '%s': %s; " + "falling back to use 'networkx' instead.", + backend_name, + self.name, + exc, + ) + return self.orig_func(*args_nx, **kwargs_nx) + import pytest + + pytest.xfail( + exc.args[0] if exc.args else f"{self.name} raised {type(exc).__name__}" + ) + + # Verify that `self._returns_graph` is correct. This compares the return type + # to the type expected from `self._returns_graph`. This handles tuple and list + # return types, but *does not* catch functions that yield graphs. + if ( + self._returns_graph + != ( + isinstance(result, nx.Graph) + or hasattr(result, "__networkx_backend__") + or isinstance(result, tuple | list) + and any( + isinstance(x, nx.Graph) or hasattr(x, "__networkx_backend__") + for x in result + ) + ) + and not ( + # May return Graph or None + self.name in {"check_planarity", "check_planarity_recursive"} + and any(x is None for x in result) + ) + and not ( + # May return Graph or dict + self.name in {"held_karp_ascent"} + and any(isinstance(x, dict) for x in result) + ) + and self.name + not in { + # yields graphs + "all_triads", + "general_k_edge_subgraphs", + # yields graphs or arrays + "nonisomorphic_trees", + } + ): + raise RuntimeError(f"`returns_graph` is incorrect for {self.name}") + + def check_result(val, depth=0): + if isinstance(val, np.number): + raise RuntimeError( + f"{self.name} returned a numpy scalar {val} ({type(val)}, depth={depth})" + ) + if isinstance(val, np.ndarray | sparray): + return + if isinstance(val, nx.Graph): + check_result(val._node, depth=depth + 1) + check_result(val._adj, depth=depth + 1) + return + if isinstance(val, Iterator): + raise NotImplementedError + if isinstance(val, Iterable) and not isinstance(val, str): + for x in val: + check_result(x, depth=depth + 1) + if isinstance(val, Mapping): + for x in val.values(): + check_result(x, depth=depth + 1) + + def check_iterator(it): + for val in it: + try: + check_result(val) + except RuntimeError as exc: + raise RuntimeError( + f"{self.name} returned a numpy scalar {val} ({type(val)})" + ) from exc + yield val + + if self.name in {"from_edgelist"}: + # numpy scalars are explicitly given as values in some tests + pass + elif isinstance(result, Iterator): + result = check_iterator(result) + else: + try: + check_result(result) + except RuntimeError as exc: + raise RuntimeError( + f"{self.name} returned a numpy scalar {result} ({type(result)})" + ) from exc + check_result(result) + + if self.name.endswith("__new__"): + # Graph is not yet done initializing; no sense doing more here + return result + + def assert_graphs_equal(G1, G2, strict=True): + assert G1.number_of_nodes() == G2.number_of_nodes() + assert G1.number_of_edges() == G2.number_of_edges() + assert G1.is_directed() is G2.is_directed() + assert G1.is_multigraph() is G2.is_multigraph() + if strict: + assert G1.graph == G2.graph + assert G1._node == G2._node + assert G1._adj == G2._adj + else: + assert set(G1) == set(G2) + assert set(G1.edges) == set(G2.edges) + + if compare_inputs_to_nx: + # Special-case algorithms that mutate input graphs + result_nx = self.orig_func(*args_nx, **kwargs_nx) + for gname in self.graphs: + G0 = bound_backend.arguments[gname] + G1 = bound_nx.arguments[gname] + if G0 is not None or G1 is not None: + G1 = backend.convert_to_nx(G1) + assert_graphs_equal(G0, G1, strict=False) + + converted_result = backend.convert_to_nx(result) + if compare_result_to_nx and isinstance(converted_result, nx.Graph): + # For graph return types (e.g. generators), we compare that results are + # the same between the backend and networkx, then return the original + # networkx result so the iteration order will be consistent in tests. + if compare_inputs_to_nx: + G = result_nx + else: + G = self.orig_func(*args_nx, **kwargs_nx) + assert_graphs_equal(G, converted_result) + return G + + return converted_result + + def _make_doc(self): + """Generate the backends section at the end for functions having an alternate + backend implementation(s) using the `backend_info` entry-point.""" + + if self.backends == {"networkx"}: + return self._orig_doc + # Add "Backends" section to the bottom of the docstring (if there are backends) + lines = [ + "Backends", + "--------", + ] + for backend in sorted(self.backends - {"networkx"}): + info = backend_info[backend] + if "short_summary" in info: + lines.append(f"{backend} : {info['short_summary']}") + else: + lines.append(backend) + if "functions" not in info or self.name not in info["functions"]: + lines.append("") + continue + + func_info = info["functions"][self.name] + + # Renaming extra_docstring to additional_docs + if func_docs := ( + func_info.get("additional_docs") or func_info.get("extra_docstring") + ): + lines.extend( + f" {line}" if line else line for line in func_docs.split("\n") + ) + add_gap = True + else: + add_gap = False + + # Renaming extra_parameters to additional_parameters + if extra_parameters := ( + func_info.get("extra_parameters") + or func_info.get("additional_parameters") + ): + if add_gap: + lines.append("") + lines.append(" Additional parameters:") + for param in sorted(extra_parameters): + lines.append(f" {param}") + if desc := extra_parameters[param]: + lines.append(f" {desc}") + lines.append("") + else: + lines.append("") + + if func_url := func_info.get("url"): + lines.append(f"[`Source <{func_url}>`_]") + lines.append("") + + # We assume the docstrings are indented by four spaces (true for now) + new_doc = self._orig_doc or "" + if not new_doc.rstrip(): + new_doc = f"The original docstring for {self.name} was empty." + if self.backends: + lines.pop() # Remove last empty line + to_add = "\n ".join(lines) + new_doc = f"{new_doc.rstrip()}\n\n {to_add}" + + # For backend-only funcs, add "Attention" admonishment after the one line summary + if "networkx" not in self.backends: + lines = new_doc.split("\n") + index = 0 + while not lines[index].strip(): + index += 1 + while index < len(lines) and lines[index].strip(): + index += 1 + backends = sorted(self.backends) + if len(backends) == 0: + example = "" + elif len(backends) == 1: + example = f' such as "{backends[0]}"' + elif len(backends) == 2: + example = f' such as "{backends[0]} or "{backends[1]}"' + else: + example = ( + " such as " + + ", ".join(f'"{x}"' for x in backends[:-1]) + + f', or "{backends[-1]}"' # Oxford comma + ) + to_add = ( + "\n .. attention:: This function does not have a default NetworkX implementation.\n" + " It may only be run with an installable :doc:`backend ` that\n" + f" supports it{example}.\n\n" + " Hint: use ``backend=...`` keyword argument to specify a backend or add\n" + " backends to ``nx.config.backend_priority``." + ) + lines.insert(index, to_add) + new_doc = "\n".join(lines) + return new_doc + + def __reduce__(self): + """Allow this object to be serialized with pickle. + + This uses the global registry `_registered_algorithms` to deserialize. + """ + return _restore_dispatchable, (self.name,) + + +def _restore_dispatchable(name): + return _registered_algorithms[name].__wrapped__ + + +def _get_cache_key( + *, + edge_attrs, + node_attrs, + preserve_edge_attrs, + preserve_node_attrs, + preserve_graph_attrs, +): + """Return key used by networkx caching given arguments for ``convert_from_nx``.""" + # edge_attrs: dict | None + # node_attrs: dict | None + # preserve_edge_attrs: bool (False if edge_attrs is not None) + # preserve_node_attrs: bool (False if node_attrs is not None) + return ( + frozenset(edge_attrs.items()) + if edge_attrs is not None + else preserve_edge_attrs, + frozenset(node_attrs.items()) + if node_attrs is not None + else preserve_node_attrs, + ) + + +def _get_from_cache(cache, key, *, backend_name=None, mutations=None): + """Search the networkx cache for a graph that is compatible with ``key``. + + Parameters + ---------- + cache : dict + If ``backend_name`` is given, then this is treated as ``G.__networkx_cache__``, + but if ``backend_name`` is None, then this is treated as the resolved inner + cache such as ``G.__networkx_cache__["backends"][backend_name]``. + key : tuple + Cache key from ``_get_cache_key``. + backend_name : str, optional + Name of the backend to control how ``cache`` is interpreted. + mutations : list, optional + Used internally to clear objects gotten from cache if inputs will be mutated. + + Returns + ------- + tuple or None + The key of the compatible graph found in the cache. + graph or "FAILED_TO_CONVERT" or None + A compatible graph if possible. "FAILED_TO_CONVERT" indicates that a previous + conversion attempt failed for this cache key. + """ + if backend_name is not None: + cache = cache.get("backends", {}).get(backend_name, {}) + if not cache: + return None, None + + # Do a simple search for a cached graph with compatible data. + # For example, if we need a single attribute, then it's okay + # to use a cached graph that preserved all attributes. + # This looks for an exact match first. + edge_key, node_key = key + for compat_key in itertools.product( + (edge_key, True) if edge_key is not True else (True,), + (node_key, True) if node_key is not True else (True,), + ): + if (rv := cache.get(compat_key)) is not None and ( + rv != FAILED_TO_CONVERT or key == compat_key + ): + if mutations is not None: + # Remove this item from the cache (after all conversions) if + # the call to this dispatchable function will mutate an input. + mutations.append((cache, compat_key)) + return compat_key, rv + + # Iterate over the items in `cache` to see if any are compatible. + # For example, if no edge attributes are needed, then a graph + # with any edge attribute will suffice. We use the same logic + # below (but switched) to clear unnecessary items from the cache. + # Use `list(cache.items())` to be thread-safe. + for (ekey, nkey), graph in list(cache.items()): + if graph == FAILED_TO_CONVERT: + # Return FAILED_TO_CONVERT if any cache key that requires a subset + # of the edge/node attributes of the given cache key has previously + # failed to convert. This logic is similar to `_set_to_cache`. + if ekey is False or edge_key is True: + pass + elif ekey is True or edge_key is False or not ekey.issubset(edge_key): + continue + if nkey is False or node_key is True: # or nkey == node_key: + pass + elif nkey is True or node_key is False or not nkey.issubset(node_key): + continue + # Save to cache for faster subsequent lookups + cache[key] = FAILED_TO_CONVERT + elif edge_key is False or ekey is True: + pass # Cache works for edge data! + elif edge_key is True or ekey is False or not edge_key.issubset(ekey): + continue # Cache missing required edge data; does not work + if node_key is False or nkey is True: + pass # Cache works for node data! + elif node_key is True or nkey is False or not node_key.issubset(nkey): + continue # Cache missing required node data; does not work + if mutations is not None: + # Remove this item from the cache (after all conversions) if + # the call to this dispatchable function will mutate an input. + mutations.append((cache, (ekey, nkey))) + return (ekey, nkey), graph + + return None, None + + +def _set_to_cache(cache, key, graph, *, backend_name=None): + """Set a backend graph to the cache, and remove unnecessary cached items. + + Parameters + ---------- + cache : dict + If ``backend_name`` is given, then this is treated as ``G.__networkx_cache__``, + but if ``backend_name`` is None, then this is treated as the resolved inner + cache such as ``G.__networkx_cache__["backends"][backend_name]``. + key : tuple + Cache key from ``_get_cache_key``. + graph : graph or "FAILED_TO_CONVERT" + Setting value to "FAILED_TO_CONVERT" prevents this conversion from being + attempted in future calls. + backend_name : str, optional + Name of the backend to control how ``cache`` is interpreted. + + Returns + ------- + dict + The items that were removed from the cache. + """ + if backend_name is not None: + cache = cache.setdefault("backends", {}).setdefault(backend_name, {}) + # Remove old cached items that are no longer necessary since they + # are dominated/subsumed/outdated by what was just calculated. + # This uses the same logic as above, but with keys switched. + # Also, don't update the cache here if the call will mutate an input. + removed = {} + edge_key, node_key = key + cache[key] = graph # Set at beginning to be thread-safe + if graph == FAILED_TO_CONVERT: + return removed + for cur_key in list(cache): + if cur_key == key: + continue + ekey, nkey = cur_key + if ekey is False or edge_key is True: + pass + elif ekey is True or edge_key is False or not ekey.issubset(edge_key): + continue + if nkey is False or node_key is True: + pass + elif nkey is True or node_key is False or not nkey.issubset(node_key): + continue + # Use pop instead of del to try to be thread-safe + if (graph := cache.pop(cur_key, None)) is not None: + removed[cur_key] = graph + return removed + + +class _LazyArgsRepr: + """Simple wrapper to display arguments of dispatchable functions in logging calls.""" + + def __init__(self, func, args, kwargs): + self.func = func + self.args = args + self.kwargs = kwargs + self.value = None + + def __repr__(self): + if self.value is None: + bound = self.func.__signature__.bind_partial(*self.args, **self.kwargs) + inner = ", ".join(f"{key}={val!r}" for key, val in bound.arguments.items()) + self.value = f"({inner})" + return self.value + + +if os.environ.get("_NETWORKX_BUILDING_DOCS_"): + # When building docs with Sphinx, use the original function with the + # dispatched __doc__, b/c Sphinx renders normal Python functions better. + # This doesn't show e.g. `*, backend=None, **backend_kwargs` in the + # signatures, which is probably okay. It does allow the docstring to be + # updated based on the installed backends. + _orig_dispatchable = _dispatchable + + def _dispatchable(func=None, **kwargs): # type: ignore[no-redef] + if func is None: + return partial(_dispatchable, **kwargs) + dispatched_func = _orig_dispatchable(func, **kwargs) + func.__doc__ = dispatched_func.__doc__ + return func + + _dispatchable.__doc__ = _orig_dispatchable.__new__.__doc__ # type: ignore[method-assign,assignment] + _sig = inspect.signature(_orig_dispatchable.__new__) + _dispatchable.__signature__ = _sig.replace( # type: ignore[method-assign,assignment] + parameters=[v for k, v in _sig.parameters.items() if k != "cls"] + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/configs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/configs.py new file mode 100644 index 0000000000000000000000000000000000000000..5da4bdc083e1948a353342004a0925c6bd02ae45 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/configs.py @@ -0,0 +1,396 @@ +import collections +import typing +from dataclasses import dataclass + +__all__ = ["Config"] + + +@dataclass(init=False, eq=False, slots=True, kw_only=True, match_args=False) +class Config: + """The base class for NetworkX configuration. + + There are two ways to use this to create configurations. The recommended way + is to subclass ``Config`` with docs and annotations. + + >>> class MyConfig(Config): + ... '''Breakfast!''' + ... + ... eggs: int + ... spam: int + ... + ... def _on_setattr(self, key, value): + ... assert isinstance(value, int) and value >= 0 + ... return value + >>> cfg = MyConfig(eggs=1, spam=5) + + Another way is to simply pass the initial configuration as keyword arguments to + the ``Config`` instance: + + >>> cfg1 = Config(eggs=1, spam=5) + >>> cfg1 + Config(eggs=1, spam=5) + + Once defined, config items may be modified, but can't be added or deleted by default. + ``Config`` is a ``Mapping``, and can get and set configs via attributes or brackets: + + >>> cfg.eggs = 2 + >>> cfg.eggs + 2 + >>> cfg["spam"] = 42 + >>> cfg["spam"] + 42 + + For convenience, it can also set configs within a context with the "with" statement: + + >>> with cfg(spam=3): + ... print("spam (in context):", cfg.spam) + spam (in context): 3 + >>> print("spam (after context):", cfg.spam) + spam (after context): 42 + + Subclasses may also define ``_on_setattr`` (as done in the example above) + to ensure the value being assigned is valid: + + >>> cfg.spam = -1 + Traceback (most recent call last): + ... + AssertionError + + If a more flexible configuration object is needed that allows adding and deleting + configurations, then pass ``strict=False`` when defining the subclass: + + >>> class FlexibleConfig(Config, strict=False): + ... default_greeting: str = "Hello" + >>> flexcfg = FlexibleConfig() + >>> flexcfg.name = "Mr. Anderson" + >>> flexcfg + FlexibleConfig(default_greeting='Hello', name='Mr. Anderson') + """ + + def __init_subclass__(cls, strict=True): + cls._strict = strict + + def __new__(cls, **kwargs): + orig_class = cls + if cls is Config: + # Enable the "simple" case of accepting config definition as keywords + cls = type( + cls.__name__, + (cls,), + {"__annotations__": {key: typing.Any for key in kwargs}}, + ) + cls = dataclass( + eq=False, + repr=cls._strict, + slots=cls._strict, + kw_only=True, + match_args=False, + )(cls) + if not cls._strict: + cls.__repr__ = _flexible_repr + cls._orig_class = orig_class # Save original class so we can pickle + cls._prev = None # Stage previous configs to enable use as context manager + cls._context_stack = [] # Stack of previous configs when used as context + instance = object.__new__(cls) + instance.__init__(**kwargs) + return instance + + def _on_setattr(self, key, value): + """Process config value and check whether it is valid. Useful for subclasses.""" + return value + + def _on_delattr(self, key): + """Callback for when a config item is being deleted. Useful for subclasses.""" + + # Control behavior of attributes + def __dir__(self): + return self.__dataclass_fields__.keys() + + def __setattr__(self, key, value): + if self._strict and key not in self.__dataclass_fields__: + raise AttributeError(f"Invalid config name: {key!r}") + value = self._on_setattr(key, value) + object.__setattr__(self, key, value) + self.__class__._prev = None + + def __delattr__(self, key): + if self._strict: + raise TypeError( + f"Configuration items can't be deleted (can't delete {key!r})." + ) + self._on_delattr(key) + object.__delattr__(self, key) + self.__class__._prev = None + + # Be a `collection.abc.Collection` + def __contains__(self, key): + return ( + key in self.__dataclass_fields__ if self._strict else key in self.__dict__ + ) + + def __iter__(self): + return iter(self.__dataclass_fields__ if self._strict else self.__dict__) + + def __len__(self): + return len(self.__dataclass_fields__ if self._strict else self.__dict__) + + def __reversed__(self): + return reversed(self.__dataclass_fields__ if self._strict else self.__dict__) + + # Add dunder methods for `collections.abc.Mapping` + def __getitem__(self, key): + try: + return getattr(self, key) + except AttributeError as err: + raise KeyError(*err.args) from None + + def __setitem__(self, key, value): + try: + self.__setattr__(key, value) + except AttributeError as err: + raise KeyError(*err.args) from None + + def __delitem__(self, key): + try: + self.__delattr__(key) + except AttributeError as err: + raise KeyError(*err.args) from None + + _ipython_key_completions_ = __dir__ # config[" + + # Go ahead and make it a `collections.abc.Mapping` + def get(self, key, default=None): + return getattr(self, key, default) + + def items(self): + return collections.abc.ItemsView(self) + + def keys(self): + return collections.abc.KeysView(self) + + def values(self): + return collections.abc.ValuesView(self) + + # dataclass can define __eq__ for us, but do it here so it works after pickling + def __eq__(self, other): + if not isinstance(other, Config): + return NotImplemented + return self._orig_class == other._orig_class and self.items() == other.items() + + # Make pickle work + def __reduce__(self): + return self._deserialize, (self._orig_class, dict(self)) + + @staticmethod + def _deserialize(cls, kwargs): + return cls(**kwargs) + + # Allow to be used as context manager + def __call__(self, **kwargs): + kwargs = {key: self._on_setattr(key, val) for key, val in kwargs.items()} + prev = dict(self) + for key, val in kwargs.items(): + setattr(self, key, val) + self.__class__._prev = prev + return self + + def __enter__(self): + if self.__class__._prev is None: + raise RuntimeError( + "Config being used as a context manager without config items being set. " + "Set config items via keyword arguments when calling the config object. " + "For example, using config as a context manager should be like:\n\n" + ' >>> with cfg(breakfast="spam"):\n' + " ... ... # Do stuff\n" + ) + self.__class__._context_stack.append(self.__class__._prev) + self.__class__._prev = None + return self + + def __exit__(self, exc_type, exc_value, traceback): + prev = self.__class__._context_stack.pop() + for key, val in prev.items(): + setattr(self, key, val) + + +def _flexible_repr(self): + return ( + f"{self.__class__.__qualname__}(" + + ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items()) + + ")" + ) + + +# Register, b/c `Mapping.__subclasshook__` returns `NotImplemented` +collections.abc.Mapping.register(Config) + + +class BackendPriorities(Config, strict=False): + """Configuration to control automatic conversion to and calling of backends. + + Priority is given to backends listed earlier. + + Parameters + ---------- + algos : list of backend names + This controls "algorithms" such as ``nx.pagerank`` that don't return a graph. + generators : list of backend names + This controls "generators" such as ``nx.from_pandas_edgelist`` that return a graph. + classes : list of backend names + This controls graph classes such as ``nx.Graph()``. + kwargs : variadic keyword arguments of function name to list of backend names + This allows each function to be configured separately and will override the config + in ``algos`` or ``generators`` if present. The dispatchable function name may be + gotten from the ``.name`` attribute such as ``nx.pagerank.name`` (it's typically + the same as the name of the function). + """ + + algos: list[str] + generators: list[str] + classes: list[str] + + def _on_setattr(self, key, value): + from .backends import _registered_algorithms, backend_info + + if key in {"algos", "generators", "classes"}: + pass + elif key not in _registered_algorithms: + raise AttributeError( + f"Invalid config name: {key!r}. Expected 'algos', 'generators', " + "'classes', or a name of a dispatchable function " + "(e.g. `.name` attribute of the function)." + ) + if not (isinstance(value, list) and all(isinstance(x, str) for x in value)): + raise TypeError( + f"{key!r} config must be a list of backend names; got {value!r}" + ) + if missing := {x for x in value if x not in backend_info}: + missing = ", ".join(map(repr, sorted(missing))) + raise ValueError(f"Unknown backend when setting {key!r}: {missing}") + return value + + def _on_delattr(self, key): + if key in {"algos", "generators", "classes"}: + raise TypeError(f"{key!r} configuration item can't be deleted.") + + +class NetworkXConfig(Config): + """Configuration for NetworkX that controls behaviors such as how to use backends. + + Attribute and bracket notation are supported for getting and setting configurations:: + + >>> nx.config.backend_priority == nx.config["backend_priority"] + True + + Parameters + ---------- + backend_priority : list of backend names or dict or BackendPriorities + Enable automatic conversion of graphs to backend graphs for functions + implemented by the backend. Priority is given to backends listed earlier. + This is a nested configuration with keys ``algos``, ``generators``, + ``classes``, and, optionally, function names. Setting this value to a + list of backend names will set ``nx.config.backend_priority.algos``. + For more information, see ``help(nx.config.backend_priority)``. + Default is empty list. + + backends : Config mapping of backend names to backend Config + The keys of the Config mapping are names of all installed NetworkX backends, + and the values are their configurations as Config mappings. + + cache_converted_graphs : bool + If True, then save converted graphs to the cache of the input graph. Graph + conversion may occur when automatically using a backend from `backend_priority` + or when using the `backend=` keyword argument to a function call. Caching can + improve performance by avoiding repeated conversions, but it uses more memory. + Care should be taken to not manually mutate a graph that has cached graphs; for + example, ``G[u][v][k] = val`` changes the graph, but does not clear the cache. + Using methods such as ``G.add_edge(u, v, weight=val)`` will clear the cache to + keep it consistent. ``G.__networkx_cache__.clear()`` manually clears the cache. + Default is True. + + fallback_to_nx : bool + If True, then "fall back" and run with the default "networkx" implementation + for dispatchable functions not implemented by backends of input graphs. When a + backend graph is passed to a dispatchable function, the default behavior is to + use the implementation from that backend if possible and raise if not. Enabling + ``fallback_to_nx`` makes the networkx implementation the fallback to use instead + of raising, and will convert the backend graph to a networkx-compatible graph. + Default is False. + + warnings_to_ignore : set of strings + Control which warnings from NetworkX are not emitted. Valid elements: + + - `"cache"`: when a cached value is used from ``G.__networkx_cache__``. + + Notes + ----- + Environment variables may be used to control some default configurations: + + - ``NETWORKX_BACKEND_PRIORITY``: set ``backend_priority.algos`` from comma-separated names. + - ``NETWORKX_CACHE_CONVERTED_GRAPHS``: set ``cache_converted_graphs`` to True if nonempty. + - ``NETWORKX_FALLBACK_TO_NX``: set ``fallback_to_nx`` to True if nonempty. + - ``NETWORKX_WARNINGS_TO_IGNORE``: set `warnings_to_ignore` from comma-separated names. + + and can be used for finer control of ``backend_priority`` such as: + + - ``NETWORKX_BACKEND_PRIORITY_ALGOS``: same as ``NETWORKX_BACKEND_PRIORITY`` + to set ``backend_priority.algos``. + + This is a global configuration. Use with caution when using from multiple threads. + """ + + backend_priority: BackendPriorities + backends: Config + cache_converted_graphs: bool + fallback_to_nx: bool + warnings_to_ignore: set[str] + + def _on_setattr(self, key, value): + from .backends import backend_info + + if key == "backend_priority": + if isinstance(value, list): + # `config.backend_priority = [backend]` sets `backend_priority.algos` + value = BackendPriorities( + **dict( + self.backend_priority, + algos=self.backend_priority._on_setattr("algos", value), + ) + ) + elif isinstance(value, dict): + kwargs = value + value = BackendPriorities(algos=[], generators=[], classes=[]) + for key, val in kwargs.items(): + setattr(value, key, val) + elif not isinstance(value, BackendPriorities): + raise TypeError( + f"{key!r} config must be a dict of lists of backend names; got {value!r}" + ) + elif key == "backends": + if not ( + isinstance(value, Config) + and all(isinstance(key, str) for key in value) + and all(isinstance(val, Config) for val in value.values()) + ): + raise TypeError( + f"{key!r} config must be a Config of backend configs; got {value!r}" + ) + if missing := {x for x in value if x not in backend_info}: + missing = ", ".join(map(repr, sorted(missing))) + raise ValueError(f"Unknown backend when setting {key!r}: {missing}") + elif key in {"cache_converted_graphs", "fallback_to_nx"}: + if not isinstance(value, bool): + raise TypeError(f"{key!r} config must be True or False; got {value!r}") + elif key == "warnings_to_ignore": + if not (isinstance(value, set) and all(isinstance(x, str) for x in value)): + raise TypeError( + f"{key!r} config must be a set of warning names; got {value!r}" + ) + known_warnings = {"cache"} + if missing := {x for x in value if x not in known_warnings}: + missing = ", ".join(map(repr, sorted(missing))) + raise ValueError( + f"Unknown warning when setting {key!r}: {missing}. Valid entries: " + + ", ".join(sorted(known_warnings)) + ) + return value diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/decorators.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..f222744fb939454dc504bf31078fc58b3cfe0da8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/decorators.py @@ -0,0 +1,1233 @@ +import bz2 +import collections +import gzip +import inspect +import itertools +import re +from collections import defaultdict +from os.path import splitext +from pathlib import Path + +import networkx as nx +from networkx.utils import create_py_random_state, create_random_state + +__all__ = [ + "not_implemented_for", + "open_file", + "nodes_or_number", + "np_random_state", + "py_random_state", + "argmap", +] + + +def not_implemented_for(*graph_types): + """Decorator to mark algorithms as not implemented + + Parameters + ---------- + graph_types : container of strings + Entries must be one of "directed", "undirected", "multigraph", or "graph". + + Returns + ------- + _require : function + The decorated function. + + Raises + ------ + NetworkXNotImplemented + If any of the packages cannot be imported + + Notes + ----- + Multiple types are joined logically with "and". + For "or" use multiple @not_implemented_for() lines. + + Examples + -------- + Decorate functions like this:: + + @not_implemented_for("directed") + def sp_function(G): + pass + + + # rule out MultiDiGraph + @not_implemented_for("directed", "multigraph") + def sp_np_function(G): + pass + + + # rule out all except DiGraph + @not_implemented_for("undirected") + @not_implemented_for("multigraph") + def sp_np_function(G): + pass + """ + if ("directed" in graph_types) and ("undirected" in graph_types): + raise ValueError("Function not implemented on directed AND undirected graphs?") + if ("multigraph" in graph_types) and ("graph" in graph_types): + raise ValueError("Function not implemented on graph AND multigraphs?") + if not set(graph_types) < {"directed", "undirected", "multigraph", "graph"}: + raise KeyError( + "use one or more of directed, undirected, multigraph, graph. " + f"You used {graph_types}" + ) + + # 3-way logic: True if "directed" input, False if "undirected" input, else None + dval = ("directed" in graph_types) or "undirected" not in graph_types and None + mval = ("multigraph" in graph_types) or "graph" not in graph_types and None + errmsg = f"not implemented for {' '.join(graph_types)} type" + + def _not_implemented_for(g): + if (mval is None or mval == g.is_multigraph()) and ( + dval is None or dval == g.is_directed() + ): + raise nx.NetworkXNotImplemented(errmsg) + + return g + + return argmap(_not_implemented_for, 0) + + +# To handle new extensions, define a function accepting a `path` and `mode`. +# Then add the extension to _dispatch_dict. +fopeners = { + ".gz": gzip.open, + ".gzip": gzip.open, + ".bz2": bz2.BZ2File, +} +_dispatch_dict = defaultdict(lambda: open, **fopeners) + + +def open_file(path_arg, mode="r"): + """Decorator to ensure clean opening and closing of files. + + Parameters + ---------- + path_arg : string or int + Name or index of the argument that is a path. + + mode : str + String for opening mode. + + Returns + ------- + _open_file : function + Function which cleanly executes the io. + + Examples + -------- + Decorate functions like this:: + + @open_file(0, "r") + def read_function(pathname): + pass + + + @open_file(1, "w") + def write_function(G, pathname): + pass + + + @open_file(1, "w") + def write_function(G, pathname="graph.dot"): + pass + + + @open_file("pathname", "w") + def write_function(G, pathname="graph.dot"): + pass + + + @open_file("path", "w+") + def another_function(arg, **kwargs): + path = kwargs["path"] + pass + + Notes + ----- + Note that this decorator solves the problem when a path argument is + specified as a string, but it does not handle the situation when the + function wants to accept a default of None (and then handle it). + + Here is an example of how to handle this case:: + + @open_file("path") + def some_function(arg1, arg2, path=None): + if path is None: + fobj = tempfile.NamedTemporaryFile(delete=False) + else: + # `path` could have been a string or file object or something + # similar. In any event, the decorator has given us a file object + # and it will close it for us, if it should. + fobj = path + + try: + fobj.write("blah") + finally: + if path is None: + fobj.close() + + Normally, we'd want to use "with" to ensure that fobj gets closed. + However, the decorator will make `path` a file object for us, + and using "with" would undesirably close that file object. + Instead, we use a try block, as shown above. + When we exit the function, fobj will be closed, if it should be, by the decorator. + """ + + def _open_file(path): + # Now we have the path_arg. There are two types of input to consider: + # 1) string representing a path that should be opened + # 2) an already opened file object + if isinstance(path, str): + ext = splitext(path)[1] + elif isinstance(path, Path): + # path is a pathlib reference to a filename + ext = path.suffix + path = str(path) + else: + # could be None, or a file handle, in which case the algorithm will deal with it + return path, lambda: None + + fobj = _dispatch_dict[ext](path, mode=mode) + return fobj, lambda: fobj.close() + + return argmap(_open_file, path_arg, try_finally=True) + + +def nodes_or_number(which_args): + """Decorator to allow number of nodes or container of nodes. + + With this decorator, the specified argument can be either a number or a container + of nodes. If it is a number, the nodes used are `range(n)`. + This allows `nx.complete_graph(50)` in place of `nx.complete_graph(list(range(50)))`. + And it also allows `nx.complete_graph(any_list_of_nodes)`. + + Parameters + ---------- + which_args : string or int or sequence of strings or ints + If string, the name of the argument to be treated. + If int, the index of the argument to be treated. + If more than one node argument is allowed, can be a list of locations. + + Returns + ------- + _nodes_or_numbers : function + Function which replaces int args with ranges. + + Examples + -------- + Decorate functions like this:: + + @nodes_or_number("nodes") + def empty_graph(nodes): + # nodes is converted to a list of nodes + + @nodes_or_number(0) + def empty_graph(nodes): + # nodes is converted to a list of nodes + + @nodes_or_number(["m1", "m2"]) + def grid_2d_graph(m1, m2, periodic=False): + # m1 and m2 are each converted to a list of nodes + + @nodes_or_number([0, 1]) + def grid_2d_graph(m1, m2, periodic=False): + # m1 and m2 are each converted to a list of nodes + + @nodes_or_number(1) + def full_rary_tree(r, n) + # presumably r is a number. It is not handled by this decorator. + # n is converted to a list of nodes + """ + + def _nodes_or_number(n): + try: + nodes = list(range(n)) + except TypeError: + nodes = tuple(n) + else: + if n < 0: + raise nx.NetworkXError(f"Negative number of nodes not valid: {n}") + return (n, nodes) + + try: + iter_wa = iter(which_args) + except TypeError: + iter_wa = (which_args,) + + return argmap(_nodes_or_number, *iter_wa) + + +def np_random_state(random_state_argument): + """Decorator to generate a numpy RandomState or Generator instance. + + The decorator processes the argument indicated by `random_state_argument` + using :func:`nx.utils.create_random_state`. + The argument value can be a seed (integer), or a `numpy.random.RandomState` + or `numpy.random.RandomState` instance or (`None` or `numpy.random`). + The latter two options use the global random number generator for `numpy.random`. + + The returned instance is a `numpy.random.RandomState` or `numpy.random.Generator`. + + Parameters + ---------- + random_state_argument : string or int + The name or index of the argument to be converted + to a `numpy.random.RandomState` instance. + + Returns + ------- + _random_state : function + Function whose random_state keyword argument is a RandomState instance. + + Examples + -------- + Decorate functions like this:: + + @np_random_state("seed") + def random_float(seed=None): + return seed.rand() + + + @np_random_state(0) + def random_float(rng=None): + return rng.rand() + + + @np_random_state(1) + def random_array(dims, random_state=1): + return random_state.rand(*dims) + + See Also + -------- + py_random_state + """ + return argmap(create_random_state, random_state_argument) + + +def py_random_state(random_state_argument): + """Decorator to generate a random.Random instance (or equiv). + + This decorator processes `random_state_argument` using + :func:`nx.utils.create_py_random_state`. + The input value can be a seed (integer), or a random number generator:: + + If int, return a random.Random instance set with seed=int. + If random.Random instance, return it. + If None or the `random` package, return the global random number + generator used by `random`. + If np.random package, or the default numpy RandomState instance, + return the default numpy random number generator wrapped in a + `PythonRandomViaNumpyBits` class. + If np.random.Generator instance, return it wrapped in a + `PythonRandomViaNumpyBits` class. + + # Legacy options + If np.random.RandomState instance, return it wrapped in a + `PythonRandomInterface` class. + If a `PythonRandomInterface` instance, return it + + Parameters + ---------- + random_state_argument : string or int + The name of the argument or the index of the argument in args that is + to be converted to the random.Random instance or numpy.random.RandomState + instance that mimics basic methods of random.Random. + + Returns + ------- + _random_state : function + Function whose random_state_argument is converted to a Random instance. + + Examples + -------- + Decorate functions like this:: + + @py_random_state("random_state") + def random_float(random_state=None): + return random_state.rand() + + + @py_random_state(0) + def random_float(rng=None): + return rng.rand() + + + @py_random_state(1) + def random_array(dims, seed=12345): + return seed.rand(*dims) + + See Also + -------- + np_random_state + """ + + return argmap(create_py_random_state, random_state_argument) + + +class argmap: + """A decorator to apply a map to arguments before calling the function + + This class provides a decorator that maps (transforms) arguments of the function + before the function is called. Thus for example, we have similar code + in many functions to determine whether an argument is the number of nodes + to be created, or a list of nodes to be handled. The decorator provides + the code to accept either -- transforming the indicated argument into a + list of nodes before the actual function is called. + + This decorator class allows us to process single or multiple arguments. + The arguments to be processed can be specified by string, naming the argument, + or by index, specifying the item in the args list. + + Parameters + ---------- + func : callable + The function to apply to arguments + + *args : iterable of (int, str or tuple) + A list of parameters, specified either as strings (their names), ints + (numerical indices) or tuples, which may contain ints, strings, and + (recursively) tuples. Each indicates which parameters the decorator + should map. Tuples indicate that the map function takes (and returns) + multiple parameters in the same order and nested structure as indicated + here. + + try_finally : bool (default: False) + When True, wrap the function call in a try-finally block with code + for the finally block created by `func`. This is used when the map + function constructs an object (like a file handle) that requires + post-processing (like closing). + + Note: try_finally decorators cannot be used to decorate generator + functions. + + Examples + -------- + Most of these examples use `@argmap(...)` to apply the decorator to + the function defined on the next line. + In the NetworkX codebase however, `argmap` is used within a function to + construct a decorator. That is, the decorator defines a mapping function + and then uses `argmap` to build and return a decorated function. + A simple example is a decorator that specifies which currency to report money. + The decorator (named `convert_to`) would be used like:: + + @convert_to("US_Dollars", "income") + def show_me_the_money(name, income): + print(f"{name} : {income}") + + And the code to create the decorator might be:: + + def convert_to(currency, which_arg): + def _convert(amount): + if amount.currency != currency: + amount = amount.to_currency(currency) + return amount + + return argmap(_convert, which_arg) + + Despite this common idiom for argmap, most of the following examples + use the `@argmap(...)` idiom to save space. + + Here's an example use of argmap to sum the elements of two of the functions + arguments. The decorated function:: + + @argmap(sum, "xlist", "zlist") + def foo(xlist, y, zlist): + return xlist - y + zlist + + is syntactic sugar for:: + + def foo(xlist, y, zlist): + x = sum(xlist) + z = sum(zlist) + return x - y + z + + and is equivalent to (using argument indexes):: + + @argmap(sum, "xlist", 2) + def foo(xlist, y, zlist): + return xlist - y + zlist + + or:: + + @argmap(sum, "zlist", 0) + def foo(xlist, y, zlist): + return xlist - y + zlist + + Transforming functions can be applied to multiple arguments, such as:: + + def swap(x, y): + return y, x + + # the 2-tuple tells argmap that the map `swap` has 2 inputs/outputs. + @argmap(swap, ("a", "b")): + def foo(a, b, c): + return a / b * c + + is equivalent to:: + + def foo(a, b, c): + a, b = swap(a, b) + return a / b * c + + More generally, the applied arguments can be nested tuples of strings or ints. + The syntax `@argmap(some_func, ("a", ("b", "c")))` would expect `some_func` to + accept 2 inputs with the second expected to be a 2-tuple. It should then return + 2 outputs with the second a 2-tuple. The returns values would replace input "a" + "b" and "c" respectively. Similarly for `@argmap(some_func, (0, ("b", 2)))`. + + Also, note that an index larger than the number of named parameters is allowed + for variadic functions. For example:: + + def double(a): + return 2 * a + + + @argmap(double, 3) + def overflow(a, *args): + return a, args + + + print(overflow(1, 2, 3, 4, 5, 6)) # output is 1, (2, 3, 8, 5, 6) + + **Try Finally** + + Additionally, this `argmap` class can be used to create a decorator that + initiates a try...finally block. The decorator must be written to return + both the transformed argument and a closing function. + This feature was included to enable the `open_file` decorator which might + need to close the file or not depending on whether it had to open that file. + This feature uses the keyword-only `try_finally` argument to `@argmap`. + + For example this map opens a file and then makes sure it is closed:: + + def open_file(fn): + f = open(fn) + return f, lambda: f.close() + + The decorator applies that to the function `foo`:: + + @argmap(open_file, "file", try_finally=True) + def foo(file): + print(file.read()) + + is syntactic sugar for:: + + def foo(file): + file, close_file = open_file(file) + try: + print(file.read()) + finally: + close_file() + + and is equivalent to (using indexes):: + + @argmap(open_file, 0, try_finally=True) + def foo(file): + print(file.read()) + + Here's an example of the try_finally feature used to create a decorator:: + + def my_closing_decorator(which_arg): + def _opener(path): + if path is None: + path = open(path) + fclose = path.close + else: + # assume `path` handles the closing + fclose = lambda: None + return path, fclose + + return argmap(_opener, which_arg, try_finally=True) + + which can then be used as:: + + @my_closing_decorator("file") + def fancy_reader(file=None): + # this code doesn't need to worry about closing the file + print(file.read()) + + Decorators with try_finally = True cannot be used with generator functions, + because the `finally` block is evaluated before the generator is exhausted:: + + @argmap(open_file, "file", try_finally=True) + def file_to_lines(file): + for line in file.readlines(): + yield line + + is equivalent to:: + + def file_to_lines_wrapped(file): + for line in file.readlines(): + yield line + + + def file_to_lines_wrapper(file): + try: + file = open_file(file) + return file_to_lines_wrapped(file) + finally: + file.close() + + which behaves similarly to:: + + def file_to_lines_whoops(file): + file = open_file(file) + file.close() + for line in file.readlines(): + yield line + + because the `finally` block of `file_to_lines_wrapper` is executed before + the caller has a chance to exhaust the iterator. + + Notes + ----- + An object of this class is callable and intended to be used when + defining a decorator. Generally, a decorator takes a function as input + and constructs a function as output. Specifically, an `argmap` object + returns the input function decorated/wrapped so that specified arguments + are mapped (transformed) to new values before the decorated function is called. + + As an overview, the argmap object returns a new function with all the + dunder values of the original function (like `__doc__`, `__name__`, etc). + Code for this decorated function is built based on the original function's + signature. It starts by mapping the input arguments to potentially new + values. Then it calls the decorated function with these new values in place + of the indicated arguments that have been mapped. The return value of the + original function is then returned. This new function is the function that + is actually called by the user. + + Three additional features are provided. + 1) The code is lazily compiled. That is, the new function is returned + as an object without the code compiled, but with all information + needed so it can be compiled upon it's first invocation. This saves + time on import at the cost of additional time on the first call of + the function. Subsequent calls are then just as fast as normal. + + 2) If the "try_finally" keyword-only argument is True, a try block + follows each mapped argument, matched on the other side of the wrapped + call, by a finally block closing that mapping. We expect func to return + a 2-tuple: the mapped value and a function to be called in the finally + clause. This feature was included so the `open_file` decorator could + provide a file handle to the decorated function and close the file handle + after the function call. It even keeps track of whether to close the file + handle or not based on whether it had to open the file or the input was + already open. So, the decorated function does not need to include any + code to open or close files. + + 3) The maps applied can process multiple arguments. For example, + you could swap two arguments using a mapping, or transform + them to their sum and their difference. This was included to allow + a decorator in the `quality.py` module that checks that an input + `partition` is a valid partition of the nodes of the input graph `G`. + In this example, the map has inputs `(G, partition)`. After checking + for a valid partition, the map either raises an exception or leaves + the inputs unchanged. Thus many functions that make this check can + use the decorator rather than copy the checking code into each function. + More complicated nested argument structures are described below. + + The remaining notes describe the code structure and methods for this + class in broad terms to aid in understanding how to use it. + + Instantiating an `argmap` object simply stores the mapping function and + the input identifiers of which arguments to map. The resulting decorator + is ready to use this map to decorate any function. Calling that object + (`argmap.__call__`, but usually done via `@my_decorator`) a lazily + compiled thin wrapper of the decorated function is constructed, + wrapped with the necessary function dunder attributes like `__doc__` + and `__name__`. That thinly wrapped function is returned as the + decorated function. When that decorated function is called, the thin + wrapper of code calls `argmap._lazy_compile` which compiles the decorated + function (using `argmap.compile`) and replaces the code of the thin + wrapper with the newly compiled code. This saves the compilation step + every import of networkx, at the cost of compiling upon the first call + to the decorated function. + + When the decorated function is compiled, the code is recursively assembled + using the `argmap.assemble` method. The recursive nature is needed in + case of nested decorators. The result of the assembly is a number of + useful objects. + + sig : the function signature of the original decorated function as + constructed by :func:`argmap.signature`. This is constructed + using `inspect.signature` but enhanced with attribute + strings `sig_def` and `sig_call`, and other information + specific to mapping arguments of this function. + This information is used to construct a string of code defining + the new decorated function. + + wrapped_name : a unique internally used name constructed by argmap + for the decorated function. + + functions : a dict of the functions used inside the code of this + decorated function, to be used as `globals` in `exec`. + This dict is recursively updated to allow for nested decorating. + + mapblock : code (as a list of strings) to map the incoming argument + values to their mapped values. + + finallys : code (as a list of strings) to provide the possibly nested + set of finally clauses if needed. + + mutable_args : a bool indicating whether the `sig.args` tuple should be + converted to a list so mutation can occur. + + After this recursive assembly process, the `argmap.compile` method + constructs code (as strings) to convert the tuple `sig.args` to a list + if needed. It joins the defining code with appropriate indents and + compiles the result. Finally, this code is evaluated and the original + wrapper's implementation is replaced with the compiled version (see + `argmap._lazy_compile` for more details). + + Other `argmap` methods include `_name` and `_count` which allow internally + generated names to be unique within a python session. + The methods `_flatten` and `_indent` process the nested lists of strings + into properly indented python code ready to be compiled. + + More complicated nested tuples of arguments also allowed though + usually not used. For the simple 2 argument case, the argmap + input ("a", "b") implies the mapping function will take 2 arguments + and return a 2-tuple of mapped values. A more complicated example + with argmap input `("a", ("b", "c"))` requires the mapping function + take 2 inputs, with the second being a 2-tuple. It then must output + the 3 mapped values in the same nested structure `(newa, (newb, newc))`. + This level of generality is not often needed, but was convenient + to implement when handling the multiple arguments. + + See Also + -------- + not_implemented_for + open_file + nodes_or_number + py_random_state + networkx.algorithms.community.quality.require_partition + + """ + + def __init__(self, func, *args, try_finally=False): + self._func = func + self._args = args + self._finally = try_finally + + @staticmethod + def _lazy_compile(func): + """Compile the source of a wrapped function + + Assemble and compile the decorated function, and intrusively replace its + code with the compiled version's. The thinly wrapped function becomes + the decorated function. + + Parameters + ---------- + func : callable + A function returned by argmap.__call__ which is in the process + of being called for the first time. + + Returns + ------- + func : callable + The same function, with a new __code__ object. + + Notes + ----- + It was observed in NetworkX issue #4732 [1] that the import time of + NetworkX was significantly bloated by the use of decorators: over half + of the import time was being spent decorating functions. This was + somewhat improved by a change made to the `decorator` library, at the + cost of a relatively heavy-weight call to `inspect.Signature.bind` + for each call to the decorated function. + + The workaround we arrived at is to do minimal work at the time of + decoration. When the decorated function is called for the first time, + we compile a function with the same function signature as the wrapped + function. The resulting decorated function is faster than one made by + the `decorator` library, so that the overhead of the first call is + 'paid off' after a small number of calls. + + References + ---------- + + [1] https://github.com/networkx/networkx/issues/4732 + + """ + real_func = func.__argmap__.compile(func.__wrapped__) + func.__code__ = real_func.__code__ + func.__globals__.update(real_func.__globals__) + func.__dict__.update(real_func.__dict__) + return func + + def __call__(self, f): + """Construct a lazily decorated wrapper of f. + + The decorated function will be compiled when it is called for the first time, + and it will replace its own __code__ object so subsequent calls are fast. + + Parameters + ---------- + f : callable + A function to be decorated. + + Returns + ------- + func : callable + The decorated function. + + See Also + -------- + argmap._lazy_compile + """ + + def func(*args, __wrapper=None, **kwargs): + return argmap._lazy_compile(__wrapper)(*args, **kwargs) + + # standard function-wrapping stuff + func.__name__ = f.__name__ + func.__doc__ = f.__doc__ + func.__defaults__ = f.__defaults__ + func.__kwdefaults__.update(f.__kwdefaults__ or {}) + func.__module__ = f.__module__ + func.__qualname__ = f.__qualname__ + func.__dict__.update(f.__dict__) + func.__wrapped__ = f + + # now that we've wrapped f, we may have picked up some __dict__ or + # __kwdefaults__ items that were set by a previous argmap. Thus, we set + # these values after those update() calls. + + # If we attempt to access func from within itself, that happens through + # a closure -- which trips an error when we replace func.__code__. The + # standard workaround for functions which can't see themselves is to use + # a Y-combinator, as we do here. + func.__kwdefaults__["_argmap__wrapper"] = func + + # this self-reference is here because functools.wraps preserves + # everything in __dict__, and we don't want to mistake a non-argmap + # wrapper for an argmap wrapper + func.__self__ = func + + # this is used to variously call self.assemble and self.compile + func.__argmap__ = self + + if hasattr(f, "__argmap__"): + func.__is_generator = f.__is_generator + else: + func.__is_generator = inspect.isgeneratorfunction(f) + + if self._finally and func.__is_generator: + raise nx.NetworkXError("argmap cannot decorate generators with try_finally") + + return func + + __count = 0 + + @classmethod + def _count(cls): + """Maintain a globally-unique identifier for function names and "file" names + + Note that this counter is a class method reporting a class variable + so the count is unique within a Python session. It could differ from + session to session for a specific decorator depending on the order + that the decorators are created. But that doesn't disrupt `argmap`. + + This is used in two places: to construct unique variable names + in the `_name` method and to construct unique fictitious filenames + in the `_compile` method. + + Returns + ------- + count : int + An integer unique to this Python session (simply counts from zero) + """ + cls.__count += 1 + return cls.__count + + _bad_chars = re.compile("[^a-zA-Z0-9_]") + + @classmethod + def _name(cls, f): + """Mangle the name of a function to be unique but somewhat human-readable + + The names are unique within a Python session and set using `_count`. + + Parameters + ---------- + f : str or object + + Returns + ------- + name : str + The mangled version of `f.__name__` (if `f.__name__` exists) or `f` + + """ + f = f.__name__ if hasattr(f, "__name__") else f + fname = re.sub(cls._bad_chars, "_", f) + return f"argmap_{fname}_{cls._count()}" + + def compile(self, f): + """Compile the decorated function. + + Called once for a given decorated function -- collects the code from all + argmap decorators in the stack, and compiles the decorated function. + + Much of the work done here uses the `assemble` method to allow recursive + treatment of multiple argmap decorators on a single decorated function. + That flattens the argmap decorators, collects the source code to construct + a single decorated function, then compiles/executes/returns that function. + + The source code for the decorated function is stored as an attribute + `_code` on the function object itself. + + Note that Python's `compile` function requires a filename, but this + code is constructed without a file, so a fictitious filename is used + to describe where the function comes from. The name is something like: + "argmap compilation 4". + + Parameters + ---------- + f : callable + The function to be decorated + + Returns + ------- + func : callable + The decorated file + + """ + sig, wrapped_name, functions, mapblock, finallys, mutable_args = self.assemble( + f + ) + + call = f"{sig.call_sig.format(wrapped_name)}#" + mut_args = f"{sig.args} = list({sig.args})" if mutable_args else "" + body = argmap._indent(sig.def_sig, mut_args, mapblock, call, finallys) + code = "\n".join(body) + + locl = {} + globl = dict(functions.values()) + filename = f"{self.__class__} compilation {self._count()}" + compiled = compile(code, filename, "exec") + exec(compiled, globl, locl) + func = locl[sig.name] + func._code = code + return func + + def assemble(self, f): + """Collects components of the source for the decorated function wrapping f. + + If `f` has multiple argmap decorators, we recursively assemble the stack of + decorators into a single flattened function. + + This method is part of the `compile` method's process yet separated + from that method to allow recursive processing. The outputs are + strings, dictionaries and lists that collect needed info to + flatten any nested argmap-decoration. + + Parameters + ---------- + f : callable + The function to be decorated. If f is argmapped, we assemble it. + + Returns + ------- + sig : argmap.Signature + The function signature as an `argmap.Signature` object. + wrapped_name : str + The mangled name used to represent the wrapped function in the code + being assembled. + functions : dict + A dictionary mapping id(g) -> (mangled_name(g), g) for functions g + referred to in the code being assembled. These need to be present + in the ``globals`` scope of ``exec`` when defining the decorated + function. + mapblock : list of lists and/or strings + Code that implements mapping of parameters including any try blocks + if needed. This code will precede the decorated function call. + finallys : list of lists and/or strings + Code that implements the finally blocks to post-process the + arguments (usually close any files if needed) after the + decorated function is called. + mutable_args : bool + True if the decorator needs to modify positional arguments + via their indices. The compile method then turns the argument + tuple into a list so that the arguments can be modified. + """ + + # first, we check if f is already argmapped -- if that's the case, + # build up the function recursively. + # > mapblock is generally a list of function calls of the sort + # arg = func(arg) + # in addition to some try-blocks if needed. + # > finallys is a recursive list of finally blocks of the sort + # finally: + # close_func_1() + # finally: + # close_func_2() + # > functions is a dict of functions used in the scope of our decorated + # function. It will be used to construct globals used in compilation. + # We make functions[id(f)] = name_of_f, f to ensure that a given + # function is stored and named exactly once even if called by + # nested decorators. + if hasattr(f, "__argmap__") and f.__self__ is f: + ( + sig, + wrapped_name, + functions, + mapblock, + finallys, + mutable_args, + ) = f.__argmap__.assemble(f.__wrapped__) + functions = dict(functions) # shallow-copy just in case + else: + sig = self.signature(f) + wrapped_name = self._name(f) + mapblock, finallys = [], [] + functions = {id(f): (wrapped_name, f)} + mutable_args = False + + if id(self._func) in functions: + fname, _ = functions[id(self._func)] + else: + fname, _ = functions[id(self._func)] = self._name(self._func), self._func + + # this is a bit complicated -- we can call functions with a variety of + # nested arguments, so long as their input and output are tuples with + # the same nested structure. e.g. ("a", "b") maps arguments a and b. + # A more complicated nesting like (0, (3, 4)) maps arguments 0, 3, 4 + # expecting the mapping to output new values in the same nested shape. + # The ability to argmap multiple arguments was necessary for + # the decorator `nx.algorithms.community.quality.require_partition`, and + # while we're not taking full advantage of the ability to handle + # multiply-nested tuples, it was convenient to implement this in + # generality because the recursive call to `get_name` is necessary in + # any case. + applied = set() + + def get_name(arg, first=True): + nonlocal mutable_args + if isinstance(arg, tuple): + name = ", ".join(get_name(x, False) for x in arg) + return name if first else f"({name})" + if arg in applied: + raise nx.NetworkXError(f"argument {arg} is specified multiple times") + applied.add(arg) + if arg in sig.names: + return sig.names[arg] + elif isinstance(arg, str): + if sig.kwargs is None: + raise nx.NetworkXError( + f"name {arg} is not a named parameter and this function doesn't have kwargs" + ) + return f"{sig.kwargs}[{arg!r}]" + else: + if sig.args is None: + raise nx.NetworkXError( + f"index {arg} not a parameter index and this function doesn't have args" + ) + mutable_args = True + return f"{sig.args}[{arg - sig.n_positional}]" + + if self._finally: + # here's where we handle try_finally decorators. Such a decorator + # returns a mapped argument and a function to be called in a + # finally block. This feature was required by the open_file + # decorator. The below generates the code + # + # name, final = func(name) #<--append to mapblock + # try: #<--append to mapblock + # ... more argmapping and try blocks + # return WRAPPED_FUNCTION(...) + # ... more finally blocks + # finally: #<--prepend to finallys + # final() #<--prepend to finallys + # + for a in self._args: + name = get_name(a) + final = self._name(name) + mapblock.append(f"{name}, {final} = {fname}({name})") + mapblock.append("try:") + finallys = ["finally:", f"{final}()#", "#", finallys] + else: + mapblock.extend( + f"{name} = {fname}({name})" for name in map(get_name, self._args) + ) + + return sig, wrapped_name, functions, mapblock, finallys, mutable_args + + @classmethod + def signature(cls, f): + r"""Construct a Signature object describing `f` + + Compute a Signature so that we can write a function wrapping f with + the same signature and call-type. + + Parameters + ---------- + f : callable + A function to be decorated + + Returns + ------- + sig : argmap.Signature + The Signature of f + + Notes + ----- + The Signature is a namedtuple with names: + + name : a unique version of the name of the decorated function + signature : the inspect.signature of the decorated function + def_sig : a string used as code to define the new function + call_sig : a string used as code to call the decorated function + names : a dict keyed by argument name and index to the argument's name + n_positional : the number of positional arguments in the signature + args : the name of the VAR_POSITIONAL argument if any, i.e. \*theseargs + kwargs : the name of the VAR_KEYWORDS argument if any, i.e. \*\*kwargs + + These named attributes of the signature are used in `assemble` and `compile` + to construct a string of source code for the decorated function. + + """ + sig = inspect.signature(f, follow_wrapped=False) + def_sig = [] + call_sig = [] + names = {} + + kind = None + args = None + kwargs = None + npos = 0 + for i, param in enumerate(sig.parameters.values()): + # parameters can be position-only, keyword-or-position, keyword-only + # in any combination, but only in the order as above. we do edge + # detection to add the appropriate punctuation + prev = kind + kind = param.kind + if prev == param.POSITIONAL_ONLY != kind: + # the last token was position-only, but this one isn't + def_sig.append("/") + if ( + param.VAR_POSITIONAL + != prev + != param.KEYWORD_ONLY + == kind + != param.VAR_POSITIONAL + ): + # param is the first keyword-only arg and isn't starred + def_sig.append("*") + + # star arguments as appropriate + if kind == param.VAR_POSITIONAL: + name = "*" + param.name + args = param.name + count = 0 + elif kind == param.VAR_KEYWORD: + name = "**" + param.name + kwargs = param.name + count = 0 + else: + names[i] = names[param.name] = param.name + name = param.name + count = 1 + + # assign to keyword-only args in the function call + if kind == param.KEYWORD_ONLY: + call_sig.append(f"{name} = {name}") + else: + npos += count + call_sig.append(name) + + def_sig.append(name) + + fname = cls._name(f) + def_sig = f"def {fname}({', '.join(def_sig)}):" + + call_sig = f"return {{}}({', '.join(call_sig)})" + + return cls.Signature(fname, sig, def_sig, call_sig, names, npos, args, kwargs) + + Signature = collections.namedtuple( + "Signature", + [ + "name", + "signature", + "def_sig", + "call_sig", + "names", + "n_positional", + "args", + "kwargs", + ], + ) + + @staticmethod + def _flatten(nestlist, visited): + """flattens a recursive list of lists that doesn't have cyclic references + + Parameters + ---------- + nestlist : iterable + A recursive list of objects to be flattened into a single iterable + + visited : set + A set of object ids which have been walked -- initialize with an + empty set + + Yields + ------ + Non-list objects contained in nestlist + + """ + for thing in nestlist: + if isinstance(thing, list): + if id(thing) in visited: + raise ValueError("A cycle was found in nestlist. Be a tree.") + else: + visited.add(id(thing)) + yield from argmap._flatten(thing, visited) + else: + yield thing + + _tabs = " " * 64 + + @staticmethod + def _indent(*lines): + """Indent list of code lines to make executable Python code + + Indents a tree-recursive list of strings, following the rule that one + space is added to the tab after a line that ends in a colon, and one is + removed after a line that ends in an hashmark. + + Parameters + ---------- + *lines : lists and/or strings + A recursive list of strings to be assembled into properly indented + code. + + Returns + ------- + code : str + + Examples + -------- + + argmap._indent(*["try:", "try:", "pass#", "finally:", "pass#", "#", + "finally:", "pass#"]) + + renders to + + '''try: + try: + pass# + finally: + pass# + # + finally: + pass#''' + """ + depth = 0 + for line in argmap._flatten(lines, set()): + yield f"{argmap._tabs[:depth]}{line}" + depth += (line[-1:] == ":") - (line[-1:] == "#") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/heaps.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/heaps.py new file mode 100644 index 0000000000000000000000000000000000000000..2d67dfd3ff381d51e2ece51aae7ea27ee3091acb --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/heaps.py @@ -0,0 +1,338 @@ +""" +Min-heaps. +""" + +from heapq import heappop, heappush +from itertools import count + +import networkx as nx + +__all__ = ["MinHeap", "PairingHeap", "BinaryHeap"] + + +class MinHeap: + """Base class for min-heaps. + + A MinHeap stores a collection of key-value pairs ordered by their values. + It supports querying the minimum pair, inserting a new pair, decreasing the + value in an existing pair and deleting the minimum pair. + """ + + class _Item: + """Used by subclassess to represent a key-value pair.""" + + __slots__ = ("key", "value") + + def __init__(self, key, value): + self.key = key + self.value = value + + def __repr__(self): + return repr((self.key, self.value)) + + def __init__(self): + """Initialize a new min-heap.""" + self._dict = {} + + def min(self): + """Query the minimum key-value pair. + + Returns + ------- + key, value : tuple + The key-value pair with the minimum value in the heap. + + Raises + ------ + NetworkXError + If the heap is empty. + """ + raise NotImplementedError + + def pop(self): + """Delete the minimum pair in the heap. + + Returns + ------- + key, value : tuple + The key-value pair with the minimum value in the heap. + + Raises + ------ + NetworkXError + If the heap is empty. + """ + raise NotImplementedError + + def get(self, key, default=None): + """Returns the value associated with a key. + + Parameters + ---------- + key : hashable object + The key to be looked up. + + default : object + Default value to return if the key is not present in the heap. + Default value: None. + + Returns + ------- + value : object. + The value associated with the key. + """ + raise NotImplementedError + + def insert(self, key, value, allow_increase=False): + """Insert a new key-value pair or modify the value in an existing + pair. + + Parameters + ---------- + key : hashable object + The key. + + value : object comparable with existing values. + The value. + + allow_increase : bool + Whether the value is allowed to increase. If False, attempts to + increase an existing value have no effect. Default value: False. + + Returns + ------- + decreased : bool + True if a pair is inserted or the existing value is decreased. + """ + raise NotImplementedError + + def __nonzero__(self): + """Returns whether the heap if empty.""" + return bool(self._dict) + + def __bool__(self): + """Returns whether the heap if empty.""" + return bool(self._dict) + + def __len__(self): + """Returns the number of key-value pairs in the heap.""" + return len(self._dict) + + def __contains__(self, key): + """Returns whether a key exists in the heap. + + Parameters + ---------- + key : any hashable object. + The key to be looked up. + """ + return key in self._dict + + +class PairingHeap(MinHeap): + """A pairing heap.""" + + class _Node(MinHeap._Item): + """A node in a pairing heap. + + A tree in a pairing heap is stored using the left-child, right-sibling + representation. + """ + + __slots__ = ("left", "next", "prev", "parent") + + def __init__(self, key, value): + super().__init__(key, value) + # The leftmost child. + self.left = None + # The next sibling. + self.next = None + # The previous sibling. + self.prev = None + # The parent. + self.parent = None + + def __init__(self): + """Initialize a pairing heap.""" + super().__init__() + self._root = None + + def min(self): + if self._root is None: + raise nx.NetworkXError("heap is empty.") + return (self._root.key, self._root.value) + + def pop(self): + if self._root is None: + raise nx.NetworkXError("heap is empty.") + min_node = self._root + self._root = self._merge_children(self._root) + del self._dict[min_node.key] + return (min_node.key, min_node.value) + + def get(self, key, default=None): + node = self._dict.get(key) + return node.value if node is not None else default + + def insert(self, key, value, allow_increase=False): + node = self._dict.get(key) + root = self._root + if node is not None: + if value < node.value: + node.value = value + if node is not root and value < node.parent.value: + self._cut(node) + self._root = self._link(root, node) + return True + elif allow_increase and value > node.value: + node.value = value + child = self._merge_children(node) + # Nonstandard step: Link the merged subtree with the root. See + # below for the standard step. + if child is not None: + self._root = self._link(self._root, child) + # Standard step: Perform a decrease followed by a pop as if the + # value were the smallest in the heap. Then insert the new + # value into the heap. + # if node is not root: + # self._cut(node) + # if child is not None: + # root = self._link(root, child) + # self._root = self._link(root, node) + # else: + # self._root = (self._link(node, child) + # if child is not None else node) + return False + else: + # Insert a new key. + node = self._Node(key, value) + self._dict[key] = node + self._root = self._link(root, node) if root is not None else node + return True + + def _link(self, root, other): + """Link two nodes, making the one with the smaller value the parent of + the other. + """ + if other.value < root.value: + root, other = other, root + next = root.left + other.next = next + if next is not None: + next.prev = other + other.prev = None + root.left = other + other.parent = root + return root + + def _merge_children(self, root): + """Merge the subtrees of the root using the standard two-pass method. + The resulting subtree is detached from the root. + """ + node = root.left + root.left = None + if node is not None: + link = self._link + # Pass 1: Merge pairs of consecutive subtrees from left to right. + # At the end of the pass, only the prev pointers of the resulting + # subtrees have meaningful values. The other pointers will be fixed + # in pass 2. + prev = None + while True: + next = node.next + if next is None: + node.prev = prev + break + next_next = next.next + node = link(node, next) + node.prev = prev + prev = node + if next_next is None: + break + node = next_next + # Pass 2: Successively merge the subtrees produced by pass 1 from + # right to left with the rightmost one. + prev = node.prev + while prev is not None: + prev_prev = prev.prev + node = link(prev, node) + prev = prev_prev + # Now node can become the new root. Its has no parent nor siblings. + node.prev = None + node.next = None + node.parent = None + return node + + def _cut(self, node): + """Cut a node from its parent.""" + prev = node.prev + next = node.next + if prev is not None: + prev.next = next + else: + node.parent.left = next + node.prev = None + if next is not None: + next.prev = prev + node.next = None + node.parent = None + + +class BinaryHeap(MinHeap): + """A binary heap.""" + + def __init__(self): + """Initialize a binary heap.""" + super().__init__() + self._heap = [] + self._count = count() + + def min(self): + dict = self._dict + if not dict: + raise nx.NetworkXError("heap is empty") + heap = self._heap + # Repeatedly remove stale key-value pairs until a up-to-date one is + # met. + while True: + value, _, key = heap[0] + if key in dict and value == dict[key]: + break + heappop(heap) + return (key, value) + + def pop(self): + dict = self._dict + if not dict: + raise nx.NetworkXError("heap is empty") + heap = self._heap + # Repeatedly remove stale key-value pairs until a up-to-date one is + # met. + while True: + value, _, key = heap[0] + heappop(heap) + if key in dict and value == dict[key]: + break + del dict[key] + return (key, value) + + def get(self, key, default=None): + return self._dict.get(key, default) + + def insert(self, key, value, allow_increase=False): + dict = self._dict + if key in dict: + old_value = dict[key] + if value < old_value or (allow_increase and value > old_value): + # Since there is no way to efficiently obtain the location of a + # key-value pair in the heap, insert a new pair even if ones + # with the same key may already be present. Deem the old ones + # as stale and skip them when the minimum pair is queried. + dict[key] = value + heappush(self._heap, (value, next(self._count), key)) + return value < old_value + return False + else: + dict[key] = value + heappush(self._heap, (value, next(self._count), key)) + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/mapped_queue.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/mapped_queue.py new file mode 100644 index 0000000000000000000000000000000000000000..0dcea368a93873fd72195fc8d388891c129942e0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/mapped_queue.py @@ -0,0 +1,297 @@ +"""Priority queue class with updatable priorities.""" + +import heapq + +__all__ = ["MappedQueue"] + + +class _HeapElement: + """This proxy class separates the heap element from its priority. + + The idea is that using a 2-tuple (priority, element) works + for sorting, but not for dict lookup because priorities are + often floating point values so round-off can mess up equality. + + So, we need inequalities to look at the priority (for sorting) + and equality (and hash) to look at the element to enable + updates to the priority. + + Unfortunately, this class can be tricky to work with if you forget that + `__lt__` compares the priority while `__eq__` compares the element. + In `greedy_modularity_communities()` the following code is + used to check that two _HeapElements differ in either element or priority: + + if d_oldmax != row_max or d_oldmax.priority != row_max.priority: + + If the priorities are the same, this implementation uses the element + as a tiebreaker. This provides compatibility with older systems that + use tuples to combine priority and elements. + """ + + __slots__ = ["priority", "element", "_hash"] + + def __init__(self, priority, element): + self.priority = priority + self.element = element + self._hash = hash(element) + + def __lt__(self, other): + try: + other_priority = other.priority + except AttributeError: + return self.priority < other + # assume comparing to another _HeapElement + if self.priority == other_priority: + try: + return self.element < other.element + except TypeError as err: + raise TypeError( + "Consider using a tuple, with a priority value that can be compared." + ) + return self.priority < other_priority + + def __gt__(self, other): + try: + other_priority = other.priority + except AttributeError: + return self.priority > other + # assume comparing to another _HeapElement + if self.priority == other_priority: + try: + return self.element > other.element + except TypeError as err: + raise TypeError( + "Consider using a tuple, with a priority value that can be compared." + ) + return self.priority > other_priority + + def __eq__(self, other): + try: + return self.element == other.element + except AttributeError: + return self.element == other + + def __hash__(self): + return self._hash + + def __getitem__(self, indx): + return self.priority if indx == 0 else self.element[indx - 1] + + def __iter__(self): + yield self.priority + try: + yield from self.element + except TypeError: + yield self.element + + def __repr__(self): + return f"_HeapElement({self.priority}, {self.element})" + + +class MappedQueue: + """The MappedQueue class implements a min-heap with removal and update-priority. + + The min heap uses heapq as well as custom written _siftup and _siftdown + methods to allow the heap positions to be tracked by an additional dict + keyed by element to position. The smallest element can be popped in O(1) time, + new elements can be pushed in O(log n) time, and any element can be removed + or updated in O(log n) time. The queue cannot contain duplicate elements + and an attempt to push an element already in the queue will have no effect. + + MappedQueue complements the heapq package from the python standard + library. While MappedQueue is designed for maximum compatibility with + heapq, it adds element removal, lookup, and priority update. + + Parameters + ---------- + data : dict or iterable + + Examples + -------- + + A `MappedQueue` can be created empty, or optionally, given a dictionary + of initial elements and priorities. The methods `push`, `pop`, + `remove`, and `update` operate on the queue. + + >>> colors_nm = {"red": 665, "blue": 470, "green": 550} + >>> q = MappedQueue(colors_nm) + >>> q.remove("red") + >>> q.update("green", "violet", 400) + >>> q.push("indigo", 425) + True + >>> [q.pop().element for i in range(len(q.heap))] + ['violet', 'indigo', 'blue'] + + A `MappedQueue` can also be initialized with a list or other iterable. The priority is assumed + to be the sort order of the items in the list. + + >>> q = MappedQueue([916, 50, 4609, 493, 237]) + >>> q.remove(493) + >>> q.update(237, 1117) + >>> [q.pop() for i in range(len(q.heap))] + [50, 916, 1117, 4609] + + An exception is raised if the elements are not comparable. + + >>> q = MappedQueue([100, "a"]) + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'int' and 'str' + + To avoid the exception, use a dictionary to assign priorities to the elements. + + >>> q = MappedQueue({100: 0, "a": 1}) + + References + ---------- + .. [1] Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2001). + Introduction to algorithms second edition. + .. [2] Knuth, D. E. (1997). The art of computer programming (Vol. 3). + Pearson Education. + """ + + def __init__(self, data=None): + """Priority queue class with updatable priorities.""" + if data is None: + self.heap = [] + elif isinstance(data, dict): + self.heap = [_HeapElement(v, k) for k, v in data.items()] + else: + self.heap = list(data) + self.position = {} + self._heapify() + + def _heapify(self): + """Restore heap invariant and recalculate map.""" + heapq.heapify(self.heap) + self.position = {elt: pos for pos, elt in enumerate(self.heap)} + if len(self.heap) != len(self.position): + raise AssertionError("Heap contains duplicate elements") + + def __len__(self): + return len(self.heap) + + def push(self, elt, priority=None): + """Add an element to the queue.""" + if priority is not None: + elt = _HeapElement(priority, elt) + # If element is already in queue, do nothing + if elt in self.position: + return False + # Add element to heap and dict + pos = len(self.heap) + self.heap.append(elt) + self.position[elt] = pos + # Restore invariant by sifting down + self._siftdown(0, pos) + return True + + def pop(self): + """Remove and return the smallest element in the queue.""" + # Remove smallest element + elt = self.heap[0] + del self.position[elt] + # If elt is last item, remove and return + if len(self.heap) == 1: + self.heap.pop() + return elt + # Replace root with last element + last = self.heap.pop() + self.heap[0] = last + self.position[last] = 0 + # Restore invariant by sifting up + self._siftup(0) + # Return smallest element + return elt + + def update(self, elt, new, priority=None): + """Replace an element in the queue with a new one.""" + if priority is not None: + new = _HeapElement(priority, new) + # Replace + pos = self.position[elt] + self.heap[pos] = new + del self.position[elt] + self.position[new] = pos + # Restore invariant by sifting up + self._siftup(pos) + + def remove(self, elt): + """Remove an element from the queue.""" + # Find and remove element + try: + pos = self.position[elt] + del self.position[elt] + except KeyError: + # Not in queue + raise + # If elt is last item, remove and return + if pos == len(self.heap) - 1: + self.heap.pop() + return + # Replace elt with last element + last = self.heap.pop() + self.heap[pos] = last + self.position[last] = pos + # Restore invariant by sifting up + self._siftup(pos) + + def _siftup(self, pos): + """Move smaller child up until hitting a leaf. + + Built to mimic code for heapq._siftup + only updating position dict too. + """ + heap, position = self.heap, self.position + end_pos = len(heap) + startpos = pos + newitem = heap[pos] + # Shift up the smaller child until hitting a leaf + child_pos = (pos << 1) + 1 # start with leftmost child position + while child_pos < end_pos: + # Set child_pos to index of smaller child. + child = heap[child_pos] + right_pos = child_pos + 1 + if right_pos < end_pos: + right = heap[right_pos] + if not child < right: + child = right + child_pos = right_pos + # Move the smaller child up. + heap[pos] = child + position[child] = pos + pos = child_pos + child_pos = (pos << 1) + 1 + # pos is a leaf position. Put newitem there, and bubble it up + # to its final resting place (by sifting its parents down). + while pos > 0: + parent_pos = (pos - 1) >> 1 + parent = heap[parent_pos] + if not newitem < parent: + break + heap[pos] = parent + position[parent] = pos + pos = parent_pos + heap[pos] = newitem + position[newitem] = pos + + def _siftdown(self, start_pos, pos): + """Restore invariant. keep swapping with parent until smaller. + + Built to mimic code for heapq._siftdown + only updating position dict too. + """ + heap, position = self.heap, self.position + newitem = heap[pos] + # Follow the path to the root, moving parents down until finding a place + # newitem fits. + while pos > start_pos: + parent_pos = (pos - 1) >> 1 + parent = heap[parent_pos] + if not newitem < parent: + break + heap[pos] = parent + position[parent] = pos + pos = parent_pos + heap[pos] = newitem + position[newitem] = pos diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/misc.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..f848113f4c5a7a2aeb09504e71f4e4b6da0061c3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/misc.py @@ -0,0 +1,703 @@ +""" +Miscellaneous Helpers for NetworkX. + +These are not imported into the base networkx namespace but +can be accessed, for example, as + +>>> import networkx as nx +>>> nx.utils.make_list_of_ints({1, 2, 3}) +[1, 2, 3] +>>> nx.utils.arbitrary_element({5, 1, 7}) # doctest: +SKIP +1 +""" + +import itertools +import random +import warnings +from collections import defaultdict +from collections.abc import Iterable, Iterator, Sized +from itertools import chain, tee, zip_longest + +import networkx as nx + +__all__ = [ + "flatten", + "make_list_of_ints", + "dict_to_numpy_array", + "arbitrary_element", + "pairwise", + "groups", + "create_random_state", + "create_py_random_state", + "PythonRandomInterface", + "PythonRandomViaNumpyBits", + "nodes_equal", + "edges_equal", + "graphs_equal", + "_clear_cache", +] + + +# some cookbook stuff +# used in deciding whether something is a bunch of nodes, edges, etc. +# see G.add_nodes and others in Graph Class in networkx/base.py + + +def flatten(obj, result=None): + """Return flattened version of (possibly nested) iterable object.""" + if not isinstance(obj, Iterable | Sized) or isinstance(obj, str): + return obj + if result is None: + result = [] + for item in obj: + if not isinstance(item, Iterable | Sized) or isinstance(item, str): + result.append(item) + else: + flatten(item, result) + return tuple(result) + + +def make_list_of_ints(sequence): + """Return list of ints from sequence of integral numbers. + + All elements of the sequence must satisfy int(element) == element + or a ValueError is raised. Sequence is iterated through once. + + If sequence is a list, the non-int values are replaced with ints. + So, no new list is created + """ + if not isinstance(sequence, list): + result = [] + for i in sequence: + errmsg = f"sequence is not all integers: {i}" + try: + ii = int(i) + except ValueError: + raise nx.NetworkXError(errmsg) from None + if ii != i: + raise nx.NetworkXError(errmsg) + result.append(ii) + return result + # original sequence is a list... in-place conversion to ints + for indx, i in enumerate(sequence): + errmsg = f"sequence is not all integers: {i}" + if isinstance(i, int): + continue + try: + ii = int(i) + except ValueError: + raise nx.NetworkXError(errmsg) from None + if ii != i: + raise nx.NetworkXError(errmsg) + sequence[indx] = ii + return sequence + + +def dict_to_numpy_array(d, mapping=None): + """Convert a dictionary of dictionaries to a numpy array + with optional mapping.""" + try: + return _dict_to_numpy_array2(d, mapping) + except (AttributeError, TypeError): + # AttributeError is when no mapping was provided and v.keys() fails. + # TypeError is when a mapping was provided and d[k1][k2] fails. + return _dict_to_numpy_array1(d, mapping) + + +def _dict_to_numpy_array2(d, mapping=None): + """Convert a dictionary of dictionaries to a 2d numpy array + with optional mapping. + + """ + import numpy as np + + if mapping is None: + s = set(d.keys()) + for k, v in d.items(): + s.update(v.keys()) + mapping = dict(zip(s, range(len(s)))) + n = len(mapping) + a = np.zeros((n, n)) + for k1, i in mapping.items(): + for k2, j in mapping.items(): + try: + a[i, j] = d[k1][k2] + except KeyError: + pass + return a + + +def _dict_to_numpy_array1(d, mapping=None): + """Convert a dictionary of numbers to a 1d numpy array with optional mapping.""" + import numpy as np + + if mapping is None: + s = set(d.keys()) + mapping = dict(zip(s, range(len(s)))) + n = len(mapping) + a = np.zeros(n) + for k1, i in mapping.items(): + i = mapping[k1] + a[i] = d[k1] + return a + + +def arbitrary_element(iterable): + """Returns an arbitrary element of `iterable` without removing it. + + This is most useful for "peeking" at an arbitrary element of a set, + but can be used for any list, dictionary, etc., as well. + + Parameters + ---------- + iterable : `abc.collections.Iterable` instance + Any object that implements ``__iter__``, e.g. set, dict, list, tuple, + etc. + + Returns + ------- + The object that results from ``next(iter(iterable))`` + + Raises + ------ + ValueError + If `iterable` is an iterator (because the current implementation of + this function would consume an element from the iterator). + + Examples + -------- + Arbitrary elements from common Iterable objects: + + >>> nx.utils.arbitrary_element([1, 2, 3]) # list + 1 + >>> nx.utils.arbitrary_element((1, 2, 3)) # tuple + 1 + >>> nx.utils.arbitrary_element({1, 2, 3}) # set + 1 + >>> d = {k: v for k, v in zip([1, 2, 3], [3, 2, 1])} + >>> nx.utils.arbitrary_element(d) # dict_keys + 1 + >>> nx.utils.arbitrary_element(d.values()) # dict values + 3 + + `str` is also an Iterable: + + >>> nx.utils.arbitrary_element("hello") + 'h' + + :exc:`ValueError` is raised if `iterable` is an iterator: + + >>> iterator = iter([1, 2, 3]) # Iterator, *not* Iterable + >>> nx.utils.arbitrary_element(iterator) + Traceback (most recent call last): + ... + ValueError: cannot return an arbitrary item from an iterator + + Notes + ----- + This function does not return a *random* element. If `iterable` is + ordered, sequential calls will return the same value:: + + >>> l = [1, 2, 3] + >>> nx.utils.arbitrary_element(l) + 1 + >>> nx.utils.arbitrary_element(l) + 1 + + """ + if isinstance(iterable, Iterator): + raise ValueError("cannot return an arbitrary item from an iterator") + # Another possible implementation is ``for x in iterable: return x``. + return next(iter(iterable)) + + +def pairwise(iterable, cyclic=False): + """Return successive overlapping pairs taken from an input iterable. + + Parameters + ---------- + iterable : iterable + An iterable from which to generate pairs. + + cyclic : bool, optional (default=False) + If `True`, a pair with the last and first items is included at the end. + + Returns + ------- + iterator + An iterator over successive overlapping pairs from the `iterable`. + + See Also + -------- + itertools.pairwise + + Examples + -------- + >>> list(nx.utils.pairwise([1, 2, 3, 4])) + [(1, 2), (2, 3), (3, 4)] + + >>> list(nx.utils.pairwise([1, 2, 3, 4], cyclic=True)) + [(1, 2), (2, 3), (3, 4), (4, 1)] + """ + if not cyclic: + return itertools.pairwise(iterable) + a, b = tee(iterable) + first = next(b, None) + return zip(a, chain(b, (first,))) + + +def groups(many_to_one): + """Converts a many-to-one mapping into a one-to-many mapping. + + `many_to_one` must be a dictionary whose keys and values are all + :term:`hashable`. + + The return value is a dictionary mapping values from `many_to_one` + to sets of keys from `many_to_one` that have that value. + + Examples + -------- + >>> from networkx.utils import groups + >>> many_to_one = {"a": 1, "b": 1, "c": 2, "d": 3, "e": 3} + >>> groups(many_to_one) # doctest: +SKIP + {1: {'a', 'b'}, 2: {'c'}, 3: {'e', 'd'}} + """ + one_to_many = defaultdict(set) + for v, k in many_to_one.items(): + one_to_many[k].add(v) + return dict(one_to_many) + + +def create_random_state(random_state=None): + """Returns a numpy.random.RandomState or numpy.random.Generator instance + depending on input. + + Parameters + ---------- + random_state : int or NumPy RandomState or Generator instance, optional (default=None) + If int, return a numpy.random.RandomState instance set with seed=int. + if `numpy.random.RandomState` instance, return it. + if `numpy.random.Generator` instance, return it. + if None or numpy.random, return the global random number generator used + by numpy.random. + """ + import numpy as np + + if random_state is None or random_state is np.random: + return np.random.mtrand._rand + if isinstance(random_state, np.random.RandomState): + return random_state + if isinstance(random_state, int): + return np.random.RandomState(random_state) + if isinstance(random_state, np.random.Generator): + return random_state + msg = ( + f"{random_state} cannot be used to create a numpy.random.RandomState or\n" + "numpy.random.Generator instance" + ) + raise ValueError(msg) + + +class PythonRandomViaNumpyBits(random.Random): + """Provide the random.random algorithms using a numpy.random bit generator + + The intent is to allow people to contribute code that uses Python's random + library, but still allow users to provide a single easily controlled random + bit-stream for all work with NetworkX. This implementation is based on helpful + comments and code from Robert Kern on NumPy's GitHub Issue #24458. + + This implementation supersedes that of `PythonRandomInterface` which rewrote + methods to account for subtle differences in API between `random` and + `numpy.random`. Instead this subclasses `random.Random` and overwrites + the methods `random`, `getrandbits`, `getstate`, `setstate` and `seed`. + It makes them use the rng values from an input numpy `RandomState` or `Generator`. + Those few methods allow the rest of the `random.Random` methods to provide + the API interface of `random.random` while using randomness generated by + a numpy generator. + """ + + def __init__(self, rng=None): + try: + import numpy as np + except ImportError: + msg = "numpy not found, only random.random available." + warnings.warn(msg, ImportWarning) + + if rng is None: + self._rng = np.random.mtrand._rand + else: + self._rng = rng + + # Not necessary, given our overriding of gauss() below, but it's + # in the superclass and nominally public, so initialize it here. + self.gauss_next = None + + def random(self): + """Get the next random number in the range 0.0 <= X < 1.0.""" + return self._rng.random() + + def getrandbits(self, k): + """getrandbits(k) -> x. Generates an int with k random bits.""" + if k < 0: + raise ValueError("number of bits must be non-negative") + numbytes = (k + 7) // 8 # bits / 8 and rounded up + x = int.from_bytes(self._rng.bytes(numbytes), "big") + return x >> (numbytes * 8 - k) # trim excess bits + + def getstate(self): + return self._rng.__getstate__() + + def setstate(self, state): + self._rng.__setstate__(state) + + def seed(self, *args, **kwds): + "Do nothing override method." + raise NotImplementedError("seed() not implemented in PythonRandomViaNumpyBits") + + +################################################################## +class PythonRandomInterface: + """PythonRandomInterface is included for backward compatibility + New code should use PythonRandomViaNumpyBits instead. + """ + + def __init__(self, rng=None): + try: + import numpy as np + except ImportError: + msg = "numpy not found, only random.random available." + warnings.warn(msg, ImportWarning) + + if rng is None: + self._rng = np.random.mtrand._rand + else: + self._rng = rng + + def random(self): + return self._rng.random() + + def uniform(self, a, b): + return a + (b - a) * self._rng.random() + + def randrange(self, a, b=None): + import numpy as np + + if b is None: + a, b = 0, a + if b > 9223372036854775807: # from np.iinfo(np.int64).max + tmp_rng = PythonRandomViaNumpyBits(self._rng) + return tmp_rng.randrange(a, b) + + if isinstance(self._rng, np.random.Generator): + return self._rng.integers(a, b) + return self._rng.randint(a, b) + + # NOTE: the numpy implementations of `choice` don't support strings, so + # this cannot be replaced with self._rng.choice + def choice(self, seq): + import numpy as np + + if isinstance(self._rng, np.random.Generator): + idx = self._rng.integers(0, len(seq)) + else: + idx = self._rng.randint(0, len(seq)) + return seq[idx] + + def gauss(self, mu, sigma): + return self._rng.normal(mu, sigma) + + def shuffle(self, seq): + return self._rng.shuffle(seq) + + # Some methods don't match API for numpy RandomState. + # Commented out versions are not used by NetworkX + + def sample(self, seq, k): + return self._rng.choice(list(seq), size=(k,), replace=False) + + def randint(self, a, b): + import numpy as np + + if b > 9223372036854775807: # from np.iinfo(np.int64).max + tmp_rng = PythonRandomViaNumpyBits(self._rng) + return tmp_rng.randint(a, b) + + if isinstance(self._rng, np.random.Generator): + return self._rng.integers(a, b + 1) + return self._rng.randint(a, b + 1) + + # exponential as expovariate with 1/argument, + def expovariate(self, scale): + return self._rng.exponential(1 / scale) + + # pareto as paretovariate with argument, + def paretovariate(self, shape): + return self._rng.pareto(shape) + + +# weibull as weibullvariate multiplied by beta, +# def weibullvariate(self, alpha, beta): +# return self._rng.weibull(alpha) * beta +# +# def triangular(self, low, high, mode): +# return self._rng.triangular(low, mode, high) +# +# def choices(self, seq, weights=None, cum_weights=None, k=1): +# return self._rng.choice(seq + + +def create_py_random_state(random_state=None): + """Returns a random.Random instance depending on input. + + Parameters + ---------- + random_state : int or random number generator or None (default=None) + - If int, return a `random.Random` instance set with seed=int. + - If `random.Random` instance, return it. + - If None or the `np.random` package, return the global random number + generator used by `np.random`. + - If an `np.random.Generator` instance, or the `np.random` package, or + the global numpy random number generator, then return it. + wrapped in a `PythonRandomViaNumpyBits` class. + - If a `PythonRandomViaNumpyBits` instance, return it. + - If a `PythonRandomInterface` instance, return it. + - If a `np.random.RandomState` instance and not the global numpy default, + return it wrapped in `PythonRandomInterface` for backward bit-stream + matching with legacy code. + + Notes + ----- + - A diagram intending to illustrate the relationships behind our support + for numpy random numbers is called + `NetworkX Numpy Random Numbers `_. + - More discussion about this support also appears in + `gh-6869#comment `_. + - Wrappers of numpy.random number generators allow them to mimic the Python random + number generation algorithms. For example, Python can create arbitrarily large + random ints, and the wrappers use Numpy bit-streams with CPython's random module + to choose arbitrarily large random integers too. + - We provide two wrapper classes: + `PythonRandomViaNumpyBits` is usually what you want and is always used for + `np.Generator` instances. But for users who need to recreate random numbers + produced in NetworkX 3.2 or earlier, we maintain the `PythonRandomInterface` + wrapper as well. We use it only used if passed a (non-default) `np.RandomState` + instance pre-initialized from a seed. Otherwise the newer wrapper is used. + """ + if random_state is None or random_state is random: + return random._inst + if isinstance(random_state, random.Random): + return random_state + if isinstance(random_state, int): + return random.Random(random_state) + + try: + import numpy as np + except ImportError: + pass + else: + if isinstance(random_state, PythonRandomInterface | PythonRandomViaNumpyBits): + return random_state + if isinstance(random_state, np.random.Generator): + return PythonRandomViaNumpyBits(random_state) + if random_state is np.random: + return PythonRandomViaNumpyBits(np.random.mtrand._rand) + + if isinstance(random_state, np.random.RandomState): + if random_state is np.random.mtrand._rand: + return PythonRandomViaNumpyBits(random_state) + # Only need older interface if specially constructed RandomState used + return PythonRandomInterface(random_state) + + msg = f"{random_state} cannot be used to generate a random.Random instance" + raise ValueError(msg) + + +def nodes_equal(nodes1, nodes2): + """Check if nodes are equal. + + Equality here means equal as Python objects. + Node data must match if included. + The order of nodes is not relevant. + + Parameters + ---------- + nodes1, nodes2 : iterables of nodes, or (node, datadict) tuples + + Returns + ------- + bool + True if nodes are equal, False otherwise. + """ + nlist1 = list(nodes1) + nlist2 = list(nodes2) + try: + d1 = dict(nlist1) + d2 = dict(nlist2) + except (ValueError, TypeError): + d1 = dict.fromkeys(nlist1) + d2 = dict.fromkeys(nlist2) + return d1 == d2 + + +def edges_equal(edges1, edges2, *, directed=False): + """Return whether edgelists are equal. + + Equality here means equal as Python objects. Edge data must match + if included. Ordering of edges in an edgelist is not relevant; + ordering of nodes in an edge is only relevant if ``directed == True``. + + Parameters + ---------- + edges1, edges2 : iterables of tuples + Each tuple can be + an edge tuple ``(u, v)``, or + an edge tuple with data `dict` s ``(u, v, d)``, or + an edge tuple with keys and data `dict` s ``(u, v, k, d)``. + + directed : bool, optional (default=False) + If `True`, edgelists are treated as coming from directed + graphs. + + Returns + ------- + bool + `True` if edgelists are equal, `False` otherwise. + + Examples + -------- + >>> G1 = nx.complete_graph(3) + >>> G2 = nx.cycle_graph(3) + >>> edges_equal(G1.edges, G2.edges) + True + + Edge order is not taken into account: + + >>> G1 = nx.Graph([(0, 1), (1, 2)]) + >>> G2 = nx.Graph([(1, 2), (0, 1)]) + >>> edges_equal(G1.edges, G2.edges) + True + + The `directed` parameter controls whether edges are treated as + coming from directed graphs. + + >>> DG1 = nx.DiGraph([(0, 1)]) + >>> DG2 = nx.DiGraph([(1, 0)]) + >>> edges_equal(DG1.edges, DG2.edges, directed=False) # Not recommended. + True + >>> edges_equal(DG1.edges, DG2.edges, directed=True) + False + + This function is meant to be used on edgelists (i.e. the output of a + ``G.edges()`` call), and can give unexpected results on unprocessed + lists of edges: + + >>> l1 = [(0, 1)] + >>> l2 = [(0, 1), (1, 0)] + >>> edges_equal(l1, l2) # Not recommended. + False + >>> G1 = nx.Graph(l1) + >>> G2 = nx.Graph(l2) + >>> edges_equal(G1.edges, G2.edges) + True + >>> DG1 = nx.DiGraph(l1) + >>> DG2 = nx.DiGraph(l2) + >>> edges_equal(DG1.edges, DG2.edges, directed=True) + False + """ + d1 = defaultdict(list) + d2 = defaultdict(list) + + for e1, e2 in zip_longest(edges1, edges2, fillvalue=None): + if e1 is None or e2 is None: + return False # One is longer. + for e, d in [(e1, d1), (e2, d2)]: + u, v, *data = e + d[u, v].append(data) + if not directed: + d[v, u].append(data) + + # Can check one direction because lengths are the same. + return all(d1[e].count(data) == d2[e].count(data) for e in d1 for data in d1[e]) + + +def graphs_equal(graph1, graph2): + """Check if graphs are equal. + + Equality here means equal as Python objects (not isomorphism). + Node, edge and graph data must match. + + Parameters + ---------- + graph1, graph2 : graph + + Returns + ------- + bool + True if graphs are equal, False otherwise. + """ + return ( + graph1.adj == graph2.adj + and graph1.nodes == graph2.nodes + and graph1.graph == graph2.graph + ) + + +def _clear_cache(G): + """Clear the cache of a graph (currently stores converted graphs). + + Caching is controlled via ``nx.config.cache_converted_graphs`` configuration. + """ + if cache := getattr(G, "__networkx_cache__", None): + cache.clear() + + +def check_create_using(create_using, *, directed=None, multigraph=None, default=None): + """Assert that create_using has good properties + + This checks for desired directedness and multi-edge properties. + It returns `create_using` unless that is `None` when it returns + the optionally specified default value. + + Parameters + ---------- + create_using : None, graph class or instance + The input value of create_using for a function. + directed : None or bool + Whether to check `create_using.is_directed() == directed`. + If None, do not assert directedness. + multigraph : None or bool + Whether to check `create_using.is_multigraph() == multigraph`. + If None, do not assert multi-edge property. + default : None or graph class + The graph class to return if create_using is None. + + Returns + ------- + create_using : graph class or instance + The provided graph class or instance, or if None, the `default` value. + + Raises + ------ + NetworkXError + When `create_using` doesn't match the properties specified by `directed` + or `multigraph` parameters. + """ + if default is None: + default = nx.Graph + G = create_using if create_using is not None else default + + G_directed = G.is_directed(None) if isinstance(G, type) else G.is_directed() + G_multigraph = G.is_multigraph(None) if isinstance(G, type) else G.is_multigraph() + + if directed is not None: + if directed and not G_directed: + raise nx.NetworkXError("create_using must be directed") + if not directed and G_directed: + raise nx.NetworkXError("create_using must not be directed") + + if multigraph is not None: + if multigraph and not G_multigraph: + raise nx.NetworkXError("create_using must be a multi-graph") + if not multigraph and G_multigraph: + raise nx.NetworkXError("create_using must not be a multi-graph") + return G diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/random_sequence.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/random_sequence.py new file mode 100644 index 0000000000000000000000000000000000000000..f4513034d15034f0783982b8361d2290d30bd1ac --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/random_sequence.py @@ -0,0 +1,198 @@ +""" +Utilities for generating random numbers, random sequences, and +random selections. +""" + +import networkx as nx +from networkx.utils import py_random_state + +__all__ = [ + "powerlaw_sequence", + "is_valid_tree_degree_sequence", + "zipf_rv", + "cumulative_distribution", + "discrete_sequence", + "random_weighted_sample", + "weighted_choice", +] + + +# The same helpers for choosing random sequences from distributions +# uses Python's random module +# https://docs.python.org/3/library/random.html + + +@py_random_state(2) +def powerlaw_sequence(n, exponent=2.0, seed=None): + """ + Return sample sequence of length n from a power law distribution. + """ + return [seed.paretovariate(exponent - 1) for i in range(n)] + + +def is_valid_tree_degree_sequence(degree_sequence): + """Check if a degree sequence is valid for a tree. + + Two conditions must be met for a degree sequence to be valid for a tree: + + 1. The number of nodes must be one more than the number of edges. + 2. The degree sequence must be trivial or have only strictly positive + node degrees. + + Parameters + ---------- + degree_sequence : iterable + Iterable of node degrees. + + Returns + ------- + bool + Whether the degree sequence is valid for a tree. + str + Reason for invalidity, or dummy string if valid. + """ + seq = list(degree_sequence) + number_of_nodes = len(seq) + twice_number_of_edges = sum(seq) + + if 2 * number_of_nodes - twice_number_of_edges != 2: + return False, "tree must have one more node than number of edges" + elif seq != [0] and any(d <= 0 for d in seq): + return False, "nontrivial tree must have strictly positive node degrees" + return True, "" + + +@py_random_state(2) +def zipf_rv(alpha, xmin=1, seed=None): + r"""Returns a random value chosen from the Zipf distribution. + + The return value is an integer drawn from the probability distribution + + .. math:: + + p(x)=\frac{x^{-\alpha}}{\zeta(\alpha, x_{\min})}, + + where $\zeta(\alpha, x_{\min})$ is the Hurwitz zeta function. + + Parameters + ---------- + alpha : float + Exponent value of the distribution + xmin : int + Minimum value + seed : integer, random_state, or None (default) + Indicator of random number generation state. + See :ref:`Randomness`. + + Returns + ------- + x : int + Random value from Zipf distribution + + Raises + ------ + ValueError: + If xmin < 1 or + If alpha <= 1 + + Notes + ----- + The rejection algorithm generates random values for a the power-law + distribution in uniformly bounded expected time dependent on + parameters. See [1]_ for details on its operation. + + Examples + -------- + >>> nx.utils.zipf_rv(alpha=2, xmin=3, seed=42) + 8 + + References + ---------- + .. [1] Luc Devroye, Non-Uniform Random Variate Generation, + Springer-Verlag, New York, 1986. + """ + if xmin < 1: + raise ValueError("xmin < 1") + if alpha <= 1: + raise ValueError("a <= 1.0") + a1 = alpha - 1.0 + b = 2**a1 + while True: + u = 1.0 - seed.random() # u in (0,1] + v = seed.random() # v in [0,1) + x = int(xmin * u ** -(1.0 / a1)) + t = (1.0 + (1.0 / x)) ** a1 + if v * x * (t - 1.0) / (b - 1.0) <= t / b: + break + return x + + +def cumulative_distribution(distribution): + """Returns normalized cumulative distribution from discrete distribution.""" + + cdf = [0.0] + cumulative = 0.0 + for element in distribution: + cumulative += element + cdf.append(cumulative) + return [element / cumulative for element in cdf] + + +@py_random_state(3) +def discrete_sequence(n, distribution=None, cdistribution=None, seed=None): + """ + Return sample sequence of length n from a given discrete distribution + or discrete cumulative distribution. + + One of the following must be specified. + + distribution = histogram of values, will be normalized + + cdistribution = normalized discrete cumulative distribution + + """ + import bisect + + if cdistribution is not None: + cdf = cdistribution + elif distribution is not None: + cdf = cumulative_distribution(distribution) + else: + raise nx.NetworkXError( + "discrete_sequence: distribution or cdistribution missing" + ) + + # get a uniform random number + inputseq = [seed.random() for i in range(n)] + + # choose from CDF + seq = [bisect.bisect_left(cdf, s) - 1 for s in inputseq] + return seq + + +@py_random_state(2) +def random_weighted_sample(mapping, k, seed=None): + """Returns k items without replacement from a weighted sample. + + The input is a dictionary of items with weights as values. + """ + if k > len(mapping): + raise ValueError("sample larger than population") + sample = set() + while len(sample) < k: + sample.add(weighted_choice(mapping, seed)) + return list(sample) + + +@py_random_state(1) +def weighted_choice(mapping, seed=None): + """Returns a single element from a weighted sample. + + The input is a dictionary of items with weights as values. + """ + # use roulette method + rnd = seed.random() * sum(mapping.values()) + for k, w in mapping.items(): + rnd -= w + if rnd < 0: + return k diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/rcm.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/rcm.py new file mode 100644 index 0000000000000000000000000000000000000000..7465c50d5af49095e421c509e36b33f2476ae157 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/rcm.py @@ -0,0 +1,159 @@ +""" +Cuthill-McKee ordering of graph nodes to produce sparse matrices +""" + +from collections import deque +from operator import itemgetter + +import networkx as nx + +from ..utils import arbitrary_element + +__all__ = ["cuthill_mckee_ordering", "reverse_cuthill_mckee_ordering"] + + +def cuthill_mckee_ordering(G, heuristic=None): + """Generate an ordering (permutation) of the graph nodes to make + a sparse matrix. + + Uses the Cuthill-McKee heuristic (based on breadth-first search) [1]_. + + Parameters + ---------- + G : graph + A NetworkX graph + + heuristic : function, optional + Function to choose starting node for RCM algorithm. If None + a node from a pseudo-peripheral pair is used. A user-defined function + can be supplied that takes a graph object and returns a single node. + + Returns + ------- + nodes : generator + Generator of nodes in Cuthill-McKee ordering. + + Examples + -------- + >>> from networkx.utils import cuthill_mckee_ordering + >>> G = nx.path_graph(4) + >>> rcm = list(cuthill_mckee_ordering(G)) + >>> A = nx.adjacency_matrix(G, nodelist=rcm) + + Smallest degree node as heuristic function: + + >>> def smallest_degree(G): + ... return min(G, key=G.degree) + >>> rcm = list(cuthill_mckee_ordering(G, heuristic=smallest_degree)) + + + See Also + -------- + reverse_cuthill_mckee_ordering + + Notes + ----- + The optimal solution the bandwidth reduction is NP-complete [2]_. + + + References + ---------- + .. [1] E. Cuthill and J. McKee. + Reducing the bandwidth of sparse symmetric matrices, + In Proc. 24th Nat. Conf. ACM, pages 157-172, 1969. + http://doi.acm.org/10.1145/800195.805928 + .. [2] Steven S. Skiena. 1997. The Algorithm Design Manual. + Springer-Verlag New York, Inc., New York, NY, USA. + """ + for c in nx.connected_components(G): + yield from connected_cuthill_mckee_ordering(G.subgraph(c), heuristic) + + +def reverse_cuthill_mckee_ordering(G, heuristic=None): + """Generate an ordering (permutation) of the graph nodes to make + a sparse matrix. + + Uses the reverse Cuthill-McKee heuristic (based on breadth-first search) + [1]_. + + Parameters + ---------- + G : graph + A NetworkX graph + + heuristic : function, optional + Function to choose starting node for RCM algorithm. If None + a node from a pseudo-peripheral pair is used. A user-defined function + can be supplied that takes a graph object and returns a single node. + + Returns + ------- + nodes : generator + Generator of nodes in reverse Cuthill-McKee ordering. + + Examples + -------- + >>> from networkx.utils import reverse_cuthill_mckee_ordering + >>> G = nx.path_graph(4) + >>> rcm = list(reverse_cuthill_mckee_ordering(G)) + >>> A = nx.adjacency_matrix(G, nodelist=rcm) + + Smallest degree node as heuristic function: + + >>> def smallest_degree(G): + ... return min(G, key=G.degree) + >>> rcm = list(reverse_cuthill_mckee_ordering(G, heuristic=smallest_degree)) + + + See Also + -------- + cuthill_mckee_ordering + + Notes + ----- + The optimal solution the bandwidth reduction is NP-complete [2]_. + + References + ---------- + .. [1] E. Cuthill and J. McKee. + Reducing the bandwidth of sparse symmetric matrices, + In Proc. 24th Nat. Conf. ACM, pages 157-72, 1969. + http://doi.acm.org/10.1145/800195.805928 + .. [2] Steven S. Skiena. 1997. The Algorithm Design Manual. + Springer-Verlag New York, Inc., New York, NY, USA. + """ + return reversed(list(cuthill_mckee_ordering(G, heuristic=heuristic))) + + +def connected_cuthill_mckee_ordering(G, heuristic=None): + # the cuthill mckee algorithm for connected graphs + if heuristic is None: + start = pseudo_peripheral_node(G) + else: + start = heuristic(G) + visited = {start} + queue = deque([start]) + while queue: + parent = queue.popleft() + yield parent + nd = sorted(G.degree(set(G[parent]) - visited), key=itemgetter(1)) + children = [n for n, d in nd] + visited.update(children) + queue.extend(children) + + +def pseudo_peripheral_node(G): + # helper for cuthill-mckee to find a node in a "pseudo peripheral pair" + # to use as good starting node + u = arbitrary_element(G) + lp = 0 + v = u + while True: + spl = nx.shortest_path_length(G, v) + l = max(spl.values()) + if l <= lp: + break + lp = l + farthest = (n for n, dist in spl.items() if dist == l) + v, deg = min(G.degree(farthest), key=itemgetter(1)) + return v diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b05ea39e8cb576202bcfe726c2f9ff3a91800938 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test__init.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test__init.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c7cafb38eaa80232934d50555960b69e1c5bf4c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test__init.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_backends.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_backends.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36d79691f0930f90eca6b456ebfbf2fc920a8e90 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_backends.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_config.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d18db3a80a0e445704c6d20611e781e0512cfac Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_config.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_decorators.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_decorators.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40f3410bbb6eec2bc97368d7c0041ef52376c0c7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_decorators.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_heaps.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_heaps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5a1860dee4659b30634fd52ec2e1eb5d347ed3b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_heaps.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_mapped_queue.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_mapped_queue.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4491e368abe52383016a969f17e81c2839c9c9a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_mapped_queue.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_misc.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_misc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fcc831808eb499a9c02606df985446c4f92fb18 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_misc.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_random_sequence.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_random_sequence.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a60ac06071f7ee1e62f39dfb3f8aaebfeb545758 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_random_sequence.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_rcm.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_rcm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70c6ca5a1f7352161b902d57cf1804543287f8fc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_rcm.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_unionfind.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_unionfind.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8103d0fb1622567768f4276b1a230a4bab782249 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/__pycache__/test_unionfind.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test__init.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test__init.py new file mode 100644 index 0000000000000000000000000000000000000000..ecbcce36df7cd37781dd45879f63f7d6f55e5567 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test__init.py @@ -0,0 +1,11 @@ +import pytest + + +def test_utils_namespace(): + """Ensure objects are not unintentionally exposed in utils namespace.""" + with pytest.raises(ImportError): + from networkx.utils import nx + with pytest.raises(ImportError): + from networkx.utils import sys + with pytest.raises(ImportError): + from networkx.utils import defaultdict, deque diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_backends.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_backends.py new file mode 100644 index 0000000000000000000000000000000000000000..5b82f596546732a304677793bcc5ad82142d70d5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_backends.py @@ -0,0 +1,225 @@ +import pickle + +import pytest + +import networkx as nx + +sp = pytest.importorskip("scipy") +pytest.importorskip("numpy") + + +@nx._dispatchable(implemented_by_nx=False) +def _stub_func(G): + raise NotImplementedError("_stub_func is a stub") + + +def test_dispatch_kwds_vs_args(): + G = nx.path_graph(4) + nx.pagerank(G) + nx.pagerank(G=G) + with pytest.raises(TypeError): + nx.pagerank() + + +def test_pickle(): + count = 0 + for name, func in nx.utils.backends._registered_algorithms.items(): + pickled = pickle.dumps(func.__wrapped__) + assert pickle.loads(pickled) is func.__wrapped__ + try: + # Some functions can't be pickled, but it's not b/c of _dispatchable + pickled = pickle.dumps(func) + except pickle.PicklingError: + continue + assert pickle.loads(pickled) is func + count += 1 + assert count > 0 + assert pickle.loads(pickle.dumps(nx.inverse_line_graph)) is nx.inverse_line_graph + + +@pytest.mark.skipif( + "not nx.config.backend_priority.algos " + "or nx.config.backend_priority.algos[0] != 'nx_loopback'" +) +def test_graph_converter_needs_backend(): + # When testing, `nx.from_scipy_sparse_array` will *always* call the backend + # implementation if it's implemented. If `backend=` isn't given, then the result + # will be converted back to NetworkX via `convert_to_nx`. + # If not testing, then calling `nx.from_scipy_sparse_array` w/o `backend=` will + # always call the original version. `backend=` is *required* to call the backend. + from networkx.classes.tests.dispatch_interface import ( + LoopbackBackendInterface, + LoopbackGraph, + ) + + A = sp.sparse.coo_array([[0, 3, 2], [3, 0, 1], [2, 1, 0]]) + + side_effects = [] + + def from_scipy_sparse_array(self, *args, **kwargs): + side_effects.append(1) # Just to prove this was called + return self.convert_from_nx( + self.__getattr__("from_scipy_sparse_array")(*args, **kwargs), + preserve_edge_attrs=True, + preserve_node_attrs=True, + preserve_graph_attrs=True, + ) + + @staticmethod + def convert_to_nx(obj, *, name=None): + if type(obj) is nx.Graph: + return obj + return nx.Graph(obj) + + # *This mutates LoopbackBackendInterface!* + orig_convert_to_nx = LoopbackBackendInterface.convert_to_nx + LoopbackBackendInterface.convert_to_nx = convert_to_nx + LoopbackBackendInterface.from_scipy_sparse_array = from_scipy_sparse_array + + try: + assert side_effects == [] + assert type(nx.from_scipy_sparse_array(A)) is nx.Graph + assert side_effects == [1] + assert ( + type(nx.from_scipy_sparse_array(A, backend="nx_loopback")) is LoopbackGraph + ) + assert side_effects == [1, 1] + # backend="networkx" is default implementation + assert type(nx.from_scipy_sparse_array(A, backend="networkx")) is nx.Graph + assert side_effects == [1, 1] + finally: + LoopbackBackendInterface.convert_to_nx = staticmethod(orig_convert_to_nx) + del LoopbackBackendInterface.from_scipy_sparse_array + with pytest.raises(ImportError, match="backend is not installed"): + nx.from_scipy_sparse_array(A, backend="bad-backend-name") + + +@pytest.mark.skipif( + "not nx.config.backend_priority.algos " + "or nx.config.backend_priority.algos[0] != 'nx_loopback'" +) +def test_networkx_backend(): + """Test using `backend="networkx"` in a dispatchable function.""" + # (Implementing this test is harder than it should be) + from networkx.classes.tests.dispatch_interface import ( + LoopbackBackendInterface, + LoopbackGraph, + ) + + G = LoopbackGraph() + G.add_edges_from([(0, 1), (1, 2), (1, 3), (2, 4)]) + + @staticmethod + def convert_to_nx(obj, *, name=None): + if isinstance(obj, LoopbackGraph): + new_graph = nx.Graph() + new_graph.__dict__.update(obj.__dict__) + return new_graph + return obj + + # *This mutates LoopbackBackendInterface!* + # This uses the same trick as in the previous test. + orig_convert_to_nx = LoopbackBackendInterface.convert_to_nx + LoopbackBackendInterface.convert_to_nx = convert_to_nx + try: + G2 = nx.ego_graph(G, 0, backend="networkx") + assert type(G2) is nx.Graph + finally: + LoopbackBackendInterface.convert_to_nx = staticmethod(orig_convert_to_nx) + + +def test_dispatchable_are_functions(): + assert type(nx.pagerank) is type(nx.pagerank.orig_func) + + +@pytest.mark.skipif("not nx.utils.backends.backends") +def test_mixing_backend_graphs(): + from networkx.classes.tests import dispatch_interface + + G = nx.Graph() + G.add_edge(1, 2) + G.add_edge(2, 3) + H = nx.Graph() + H.add_edge(2, 3) + rv = nx.intersection(G, H) + assert set(nx.intersection(G, H)) == {2, 3} + G2 = dispatch_interface.convert(G) + H2 = dispatch_interface.convert(H) + if "nx_loopback" in nx.config.backend_priority: + # Auto-convert + assert set(nx.intersection(G2, H)) == {2, 3} + assert set(nx.intersection(G, H2)) == {2, 3} + elif not nx.config.backend_priority and "nx_loopback" not in nx.config.backends: + # G2 and H2 are backend objects for a backend that is not registered! + with pytest.raises(ImportError, match="backend is not installed"): + nx.intersection(G2, H) + with pytest.raises(ImportError, match="backend is not installed"): + nx.intersection(G, H2) + # It would be nice to test passing graphs from *different* backends, + # but we are not set up to do this yet. + + +def test_bad_backend_name(): + """Using `backend=` raises with unknown backend even if there are no backends.""" + with pytest.raises( + ImportError, match="'this_backend_does_not_exist' backend is not installed" + ): + nx.null_graph(backend="this_backend_does_not_exist") + + +def test_not_implemented_by_nx(): + assert "networkx" in nx.pagerank.backends + assert "networkx" not in _stub_func.backends + + if "nx_loopback" in nx.config.backends: + from networkx.classes.tests.dispatch_interface import LoopbackBackendInterface + + def stub_func_implementation(G): + return True + + LoopbackBackendInterface._stub_func = staticmethod(stub_func_implementation) + try: + assert _stub_func(nx.Graph()) is True + finally: + del LoopbackBackendInterface._stub_func + + with pytest.raises(NotImplementedError): + _stub_func(nx.Graph()) + + +@pytest.mark.skipif( + "not nx.config.backend_priority.algos " + "or nx.config.backend_priority.algos[0] != 'nx_loopback'" +) +def test_dispatch_graph_new(): + from networkx.classes.tests.dispatch_interface import LoopbackGraph + + G = nx.Graph() + assert not isinstance(G, LoopbackGraph) + + # `backend=` argument that gets passed to __init__ is ignored. + # Best practice is that it should not be in the `.graph` dict. + G = nx.Graph(backend="networkx") + assert type(G) is nx.Graph + assert "backend" not in G.graph + + G = nx.Graph(backend="nx_loopback") + assert isinstance(G, LoopbackGraph) + assert "backend" not in G.graph + + # Args are passed + G1 = nx.Graph([(0, 1), (1, 2)]) + assert not isinstance(G1, LoopbackGraph) + G2 = nx.Graph([(0, 1), (1, 2)], backend="nx_loopback") + assert isinstance(G2, LoopbackGraph) + assert nx.utils.misc.graphs_equal(G1, G2) + + # Test config for automatic usage + with nx.config.backend_priority(classes=["nx_loopback"]): + G = nx.Graph() + assert isinstance(G, LoopbackGraph) + # LoopbackDiGraph __new__ is not implemented + G = nx.DiGraph() + assert not isinstance(G, LoopbackGraph) + G = nx.Graph() + assert not isinstance(G, LoopbackGraph) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_config.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_config.py new file mode 100644 index 0000000000000000000000000000000000000000..d4fe902bceeafe98ccf073613a7de61bf0110b57 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_config.py @@ -0,0 +1,263 @@ +import collections +import pickle + +import pytest + +import networkx as nx +from networkx.utils.configs import BackendPriorities, Config + + +# Define this at module level so we can test pickling +class ExampleConfig(Config): + """Example configuration.""" + + x: int + y: str + + def _on_setattr(self, key, value): + if key == "x" and value <= 0: + raise ValueError("x must be positive") + if key == "y" and not isinstance(value, str): + raise TypeError("y must be a str") + return value + + +class EmptyConfig(Config): + pass + + +@pytest.mark.parametrize("cfg", [EmptyConfig(), Config()]) +def test_config_empty(cfg): + assert dir(cfg) == [] + with pytest.raises(AttributeError): + cfg.x = 1 + with pytest.raises(KeyError): + cfg["x"] = 1 + with pytest.raises(AttributeError): + cfg.x + with pytest.raises(KeyError): + cfg["x"] + assert len(cfg) == 0 + assert "x" not in cfg + assert cfg == cfg + assert cfg.get("x", 2) == 2 + assert set(cfg.keys()) == set() + assert set(cfg.values()) == set() + assert set(cfg.items()) == set() + cfg2 = pickle.loads(pickle.dumps(cfg)) + assert cfg == cfg2 + assert isinstance(cfg, collections.abc.Collection) + assert isinstance(cfg, collections.abc.Mapping) + + +def test_config_subclass(): + with pytest.raises(TypeError, match="missing 2 required keyword-only"): + ExampleConfig() + with pytest.raises(ValueError, match="x must be positive"): + ExampleConfig(x=0, y="foo") + with pytest.raises(TypeError, match="unexpected keyword"): + ExampleConfig(x=1, y="foo", z="bad config") + with pytest.raises(TypeError, match="unexpected keyword"): + EmptyConfig(z="bad config") + cfg = ExampleConfig(x=1, y="foo") + assert cfg.x == 1 + assert cfg["x"] == 1 + assert cfg["y"] == "foo" + assert cfg.y == "foo" + assert "x" in cfg + assert "y" in cfg + assert "z" not in cfg + assert len(cfg) == 2 + assert set(iter(cfg)) == {"x", "y"} + assert set(cfg.keys()) == {"x", "y"} + assert set(cfg.values()) == {1, "foo"} + assert set(cfg.items()) == {("x", 1), ("y", "foo")} + assert dir(cfg) == ["x", "y"] + cfg.x = 2 + cfg["y"] = "bar" + assert cfg["x"] == 2 + assert cfg.y == "bar" + with pytest.raises(TypeError, match="can't be deleted"): + del cfg.x + with pytest.raises(TypeError, match="can't be deleted"): + del cfg["y"] + assert cfg.x == 2 + assert cfg == cfg + assert cfg == ExampleConfig(x=2, y="bar") + assert cfg != ExampleConfig(x=3, y="baz") + assert cfg != Config(x=2, y="bar") + with pytest.raises(TypeError, match="y must be a str"): + cfg["y"] = 5 + with pytest.raises(ValueError, match="x must be positive"): + cfg.x = -5 + assert cfg.get("x", 10) == 2 + with pytest.raises(AttributeError): + cfg.z = 5 + with pytest.raises(KeyError): + cfg["z"] = 5 + with pytest.raises(AttributeError): + cfg.z + with pytest.raises(KeyError): + cfg["z"] + cfg2 = pickle.loads(pickle.dumps(cfg)) + assert cfg == cfg2 + assert cfg.__doc__ == "Example configuration." + assert cfg2.__doc__ == "Example configuration." + + +def test_config_defaults(): + class DefaultConfig(Config): + x: int = 0 + y: int + + cfg = DefaultConfig(y=1) + assert cfg.x == 0 + cfg = DefaultConfig(x=2, y=1) + assert cfg.x == 2 + + +def test_nxconfig(): + assert isinstance(nx.config.backend_priority, BackendPriorities) + assert isinstance(nx.config.backend_priority.algos, list) + assert isinstance(nx.config.backends, Config) + with pytest.raises(TypeError, match="must be a list of backend names"): + nx.config.backend_priority.algos = "nx_loopback" + with pytest.raises(ValueError, match="Unknown backend when setting"): + nx.config.backend_priority.algos = ["this_almost_certainly_is_not_a_backend"] + with pytest.raises(TypeError, match="must be a Config of backend configs"): + nx.config.backends = {} + with pytest.raises(TypeError, match="must be a Config of backend configs"): + nx.config.backends = Config(plausible_backend_name={}) + with pytest.raises(ValueError, match="Unknown backend when setting"): + nx.config.backends = Config(this_almost_certainly_is_not_a_backend=Config()) + with pytest.raises(TypeError, match="must be True or False"): + nx.config.cache_converted_graphs = "bad value" + with pytest.raises(TypeError, match="must be a set of "): + nx.config.warnings_to_ignore = 7 + with pytest.raises(ValueError, match="Unknown warning "): + nx.config.warnings_to_ignore = {"bad value"} + + prev = nx.config.backend_priority + try: + nx.config.backend_priority = ["networkx"] + assert isinstance(nx.config.backend_priority, BackendPriorities) + assert nx.config.backend_priority.algos == ["networkx"] + finally: + nx.config.backend_priority = prev + + +def test_nxconfig_context(): + # We do some special handling so that `nx.config.backend_priority = val` + # actually does `nx.config.backend_priority.algos = val`. + orig = nx.config.backend_priority.algos + val = [] if orig else ["networkx"] + assert orig != val + assert nx.config.backend_priority.algos != val + with nx.config(backend_priority=val): + assert nx.config.backend_priority.algos == val + assert nx.config.backend_priority.algos == orig + with nx.config.backend_priority(algos=val): + assert nx.config.backend_priority.algos == val + assert nx.config.backend_priority.algos == orig + bad = ["bad-backend"] + with pytest.raises(ValueError, match="Unknown backend"): + nx.config.backend_priority = bad + with pytest.raises(ValueError, match="Unknown backend"): + with nx.config(backend_priority=bad): + pass + with pytest.raises(ValueError, match="Unknown backend"): + with nx.config.backend_priority(algos=bad): + pass + + +def test_not_strict(): + class FlexibleConfig(Config, strict=False): + x: int + + cfg = FlexibleConfig(x=1) + assert "_strict" not in cfg + assert len(cfg) == 1 + assert list(cfg) == ["x"] + assert list(cfg.keys()) == ["x"] + assert list(cfg.values()) == [1] + assert list(cfg.items()) == [("x", 1)] + assert cfg.x == 1 + assert cfg["x"] == 1 + assert "x" in cfg + assert hasattr(cfg, "x") + assert "FlexibleConfig(x=1)" in repr(cfg) + assert cfg == FlexibleConfig(x=1) + del cfg.x + assert "FlexibleConfig()" in repr(cfg) + assert len(cfg) == 0 + assert not hasattr(cfg, "x") + assert "x" not in cfg + assert not hasattr(cfg, "y") + assert "y" not in cfg + cfg.y = 2 + assert len(cfg) == 1 + assert list(cfg) == ["y"] + assert list(cfg.keys()) == ["y"] + assert list(cfg.values()) == [2] + assert list(cfg.items()) == [("y", 2)] + assert cfg.y == 2 + assert cfg["y"] == 2 + assert hasattr(cfg, "y") + assert "y" in cfg + del cfg["y"] + assert len(cfg) == 0 + assert list(cfg) == [] + with pytest.raises(AttributeError, match="y"): + del cfg.y + with pytest.raises(KeyError, match="y"): + del cfg["y"] + with pytest.raises(TypeError, match="missing 1 required keyword-only"): + FlexibleConfig() + # Be strict when first creating the config object + with pytest.raises(TypeError, match="unexpected keyword argument 'y'"): + FlexibleConfig(x=1, y=2) + + class FlexibleConfigWithDefault(Config, strict=False): + x: int = 0 + + assert FlexibleConfigWithDefault().x == 0 + assert FlexibleConfigWithDefault(x=1)["x"] == 1 + + +def test_context(): + cfg = Config(x=1) + with cfg(x=2) as c: + assert c.x == 2 + c.x = 3 + assert cfg.x == 3 + assert cfg.x == 1 + + with cfg(x=2) as c: + assert c == cfg + assert cfg.x == 2 + with cfg(x=3) as c2: + assert c2 == cfg + assert cfg.x == 3 + with pytest.raises(RuntimeError, match="context manager without"): + with cfg as c3: # Forgot to call `cfg(...)` + pass + assert cfg.x == 3 + assert cfg.x == 2 + assert cfg.x == 1 + + c = cfg(x=4) # Not yet as context (not recommended, but possible) + assert c == cfg + assert cfg.x == 4 + # Cheat by looking at internal data; context stack should only grow with __enter__ + assert cfg._prev is not None + assert cfg._context_stack == [] + with c: + assert c == cfg + assert cfg.x == 4 + assert cfg.x == 1 + # Cheat again; there was no preceding `cfg(...)` call this time + assert cfg._prev is None + with pytest.raises(RuntimeError, match="context manager without"): + with cfg: + pass + assert cfg.x == 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_decorators.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..0a4aeabfe0b016bec362eac628489f6f4244cc59 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_decorators.py @@ -0,0 +1,510 @@ +import os +import pathlib +import random +import tempfile + +import pytest + +import networkx as nx +from networkx.utils.decorators import ( + argmap, + not_implemented_for, + np_random_state, + open_file, + py_random_state, +) +from networkx.utils.misc import PythonRandomInterface, PythonRandomViaNumpyBits + + +def test_not_implemented_decorator(): + @not_implemented_for("directed") + def test_d(G): + pass + + test_d(nx.Graph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_d(nx.DiGraph()) + + @not_implemented_for("undirected") + def test_u(G): + pass + + test_u(nx.DiGraph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_u(nx.Graph()) + + @not_implemented_for("multigraph") + def test_m(G): + pass + + test_m(nx.Graph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_m(nx.MultiGraph()) + + @not_implemented_for("graph") + def test_g(G): + pass + + test_g(nx.MultiGraph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_g(nx.Graph()) + + # not MultiDiGraph (multiple arguments => AND) + @not_implemented_for("directed", "multigraph") + def test_not_md(G): + pass + + test_not_md(nx.Graph()) + test_not_md(nx.DiGraph()) + test_not_md(nx.MultiGraph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_not_md(nx.MultiDiGraph()) + + # Graph only (multiple decorators => OR) + @not_implemented_for("directed") + @not_implemented_for("multigraph") + def test_graph_only(G): + pass + + test_graph_only(nx.Graph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_graph_only(nx.DiGraph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_graph_only(nx.MultiGraph()) + with pytest.raises(nx.NetworkXNotImplemented): + test_graph_only(nx.MultiDiGraph()) + + with pytest.raises(ValueError): + not_implemented_for("directed", "undirected") + + with pytest.raises(ValueError): + not_implemented_for("multigraph", "graph") + + +def test_not_implemented_decorator_key(): + with pytest.raises(KeyError): + + @not_implemented_for("foo") + def test1(G): + pass + + test1(nx.Graph()) + + +def test_not_implemented_decorator_raise(): + with pytest.raises(nx.NetworkXNotImplemented): + + @not_implemented_for("graph") + def test1(G): + pass + + test1(nx.Graph()) + + +class TestOpenFileDecorator: + def setup_method(self): + self.text = ["Blah... ", "BLAH ", "BLAH!!!!"] + self.fobj = tempfile.NamedTemporaryFile("wb+", delete=False) + self.name = self.fobj.name + + def teardown_method(self): + self.fobj.close() + os.unlink(self.name) + + def write(self, path): + for text in self.text: + path.write(text.encode("ascii")) + + @open_file(1, "r") + def read(self, path): + return path.readlines()[0] + + @staticmethod + @open_file(0, "wb") + def writer_arg0(path): + path.write(b"demo") + + @open_file(1, "wb+") + def writer_arg1(self, path): + self.write(path) + + @open_file(2, "wb") + def writer_arg2default(self, x, path=None): + if path is None: + with tempfile.NamedTemporaryFile("wb+") as fh: + self.write(fh) + else: + self.write(path) + + @open_file(4, "wb") + def writer_arg4default(self, x, y, other="hello", path=None, **kwargs): + if path is None: + with tempfile.NamedTemporaryFile("wb+") as fh: + self.write(fh) + else: + self.write(path) + + @open_file("path", "wb") + def writer_kwarg(self, **kwargs): + path = kwargs.get("path", None) + if path is None: + with tempfile.NamedTemporaryFile("wb+") as fh: + self.write(fh) + else: + self.write(path) + + def test_writer_arg0_str(self): + self.writer_arg0(self.name) + + def test_writer_arg0_fobj(self): + self.writer_arg0(self.fobj) + + def test_writer_arg0_pathlib(self): + self.writer_arg0(pathlib.Path(self.name)) + + def test_writer_arg1_str(self): + self.writer_arg1(self.name) + assert self.read(self.name) == "".join(self.text) + + def test_writer_arg1_fobj(self): + self.writer_arg1(self.fobj) + assert not self.fobj.closed + self.fobj.close() + assert self.read(self.name) == "".join(self.text) + + def test_writer_arg2default_str(self): + self.writer_arg2default(0, path=None) + self.writer_arg2default(0, path=self.name) + assert self.read(self.name) == "".join(self.text) + + def test_writer_arg2default_fobj(self): + self.writer_arg2default(0, path=self.fobj) + assert not self.fobj.closed + self.fobj.close() + assert self.read(self.name) == "".join(self.text) + + def test_writer_arg2default_fobj_path_none(self): + self.writer_arg2default(0, path=None) + + def test_writer_arg4default_fobj(self): + self.writer_arg4default(0, 1, dog="dog", other="other") + self.writer_arg4default(0, 1, dog="dog", other="other", path=self.name) + assert self.read(self.name) == "".join(self.text) + + def test_writer_kwarg_str(self): + self.writer_kwarg(path=self.name) + assert self.read(self.name) == "".join(self.text) + + def test_writer_kwarg_fobj(self): + self.writer_kwarg(path=self.fobj) + self.fobj.close() + assert self.read(self.name) == "".join(self.text) + + def test_writer_kwarg_path_none(self): + self.writer_kwarg(path=None) + + +class TestRandomState: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + + @np_random_state(1) + def instantiate_np_random_state(self, random_state): + allowed = (np.random.RandomState, np.random.Generator) + assert isinstance(random_state, allowed) + return random_state.random() + + @py_random_state(1) + def instantiate_py_random_state(self, random_state): + allowed = (random.Random, PythonRandomInterface, PythonRandomViaNumpyBits) + assert isinstance(random_state, allowed) + return random_state.random() + + def test_random_state_None(self): + np.random.seed(42) + rv = np.random.random() + np.random.seed(42) + assert rv == self.instantiate_np_random_state(None) + + random.seed(42) + rv = random.random() + random.seed(42) + assert rv == self.instantiate_py_random_state(None) + + def test_random_state_np_random(self): + np.random.seed(42) + rv = np.random.random() + np.random.seed(42) + assert rv == self.instantiate_np_random_state(np.random) + np.random.seed(42) + assert rv == self.instantiate_py_random_state(np.random) + + def test_random_state_int(self): + np.random.seed(42) + np_rv = np.random.random() + random.seed(42) + py_rv = random.random() + + np.random.seed(42) + seed = 1 + rval = self.instantiate_np_random_state(seed) + rval_expected = np.random.RandomState(seed).rand() + assert rval == rval_expected + # test that global seed wasn't changed in function + assert np_rv == np.random.random() + + random.seed(42) + rval = self.instantiate_py_random_state(seed) + rval_expected = random.Random(seed).random() + assert rval == rval_expected + # test that global seed wasn't changed in function + assert py_rv == random.random() + + def test_random_state_np_random_Generator(self): + np.random.seed(42) + np_rv = np.random.random() + np.random.seed(42) + seed = 1 + + rng = np.random.default_rng(seed) + rval = self.instantiate_np_random_state(rng) + rval_expected = np.random.default_rng(seed).random() + assert rval == rval_expected + + rval = self.instantiate_py_random_state(rng) + rval_expected = np.random.default_rng(seed).random(size=2)[1] + assert rval == rval_expected + # test that global seed wasn't changed in function + assert np_rv == np.random.random() + + def test_random_state_np_random_RandomState(self): + np.random.seed(42) + np_rv = np.random.random() + np.random.seed(42) + seed = 1 + + rng = np.random.RandomState(seed) + rval = self.instantiate_np_random_state(rng) + rval_expected = np.random.RandomState(seed).random() + assert rval == rval_expected + + rval = self.instantiate_py_random_state(rng) + rval_expected = np.random.RandomState(seed).random(size=2)[1] + assert rval == rval_expected + # test that global seed wasn't changed in function + assert np_rv == np.random.random() + + def test_random_state_py_random(self): + seed = 1 + rng = random.Random(seed) + rv = self.instantiate_py_random_state(rng) + assert rv == random.Random(seed).random() + + pytest.raises(ValueError, self.instantiate_np_random_state, rng) + + +def test_random_state_string_arg_index(): + with pytest.raises(nx.NetworkXError): + + @np_random_state("a") + def make_random_state(rs): + pass + + rstate = make_random_state(1) + + +def test_py_random_state_string_arg_index(): + with pytest.raises(nx.NetworkXError): + + @py_random_state("a") + def make_random_state(rs): + pass + + rstate = make_random_state(1) + + +def test_random_state_invalid_arg_index(): + with pytest.raises(nx.NetworkXError): + + @np_random_state(2) + def make_random_state(rs): + pass + + rstate = make_random_state(1) + + +def test_py_random_state_invalid_arg_index(): + with pytest.raises(nx.NetworkXError): + + @py_random_state(2) + def make_random_state(rs): + pass + + rstate = make_random_state(1) + + +class TestArgmap: + class ArgmapError(RuntimeError): + pass + + def test_trivial_function(self): + def do_not_call(x): + raise ArgmapError("do not call this function") + + @argmap(do_not_call) + def trivial_argmap(): + return 1 + + assert trivial_argmap() == 1 + + def test_trivial_iterator(self): + def do_not_call(x): + raise ArgmapError("do not call this function") + + @argmap(do_not_call) + def trivial_argmap(): + yield from (1, 2, 3) + + assert tuple(trivial_argmap()) == (1, 2, 3) + + def test_contextmanager(self): + container = [] + + def contextmanager(x): + nonlocal container + return x, lambda: container.append(x) + + @argmap(contextmanager, 0, 1, 2, try_finally=True) + def foo(x, y, z): + return x, y, z + + x, y, z = foo("a", "b", "c") + + # context exits are called in reverse + assert container == ["c", "b", "a"] + + def test_tryfinally_generator(self): + container = [] + + def singleton(x): + return (x,) + + with pytest.raises(nx.NetworkXError): + + @argmap(singleton, 0, 1, 2, try_finally=True) + def foo(x, y, z): + yield from (x, y, z) + + @argmap(singleton, 0, 1, 2) + def foo(x, y, z): + return x + y + z + + q = foo("a", "b", "c") + + assert q == ("a", "b", "c") + + def test_actual_vararg(self): + @argmap(lambda x: -x, 4) + def foo(x, y, *args): + return (x, y) + tuple(args) + + assert foo(1, 2, 3, 4, 5, 6) == (1, 2, 3, 4, -5, 6) + + def test_signature_destroying_intermediate_decorator(self): + def add_one_to_first_bad_decorator(f): + """Bad because it doesn't wrap the f signature (clobbers it)""" + + def decorated(a, *args, **kwargs): + return f(a + 1, *args, **kwargs) + + return decorated + + add_two_to_second = argmap(lambda b: b + 2, 1) + + @add_two_to_second + @add_one_to_first_bad_decorator + def add_one_and_two(a, b): + return a, b + + assert add_one_and_two(5, 5) == (6, 7) + + def test_actual_kwarg(self): + @argmap(lambda x: -x, "arg") + def foo(*, arg): + return arg + + assert foo(arg=3) == -3 + + def test_nested_tuple(self): + def xform(x, y): + u, v = y + return x + u + v, (x + u, x + v) + + # we're testing args and kwargs here, too + @argmap(xform, (0, ("t", 2))) + def foo(a, *args, **kwargs): + return a, args, kwargs + + a, args, kwargs = foo(1, 2, 3, t=4) + + assert a == 1 + 4 + 3 + assert args == (2, 1 + 3) + assert kwargs == {"t": 1 + 4} + + def test_flatten(self): + assert tuple(argmap._flatten([[[[[], []], [], []], [], [], []]], set())) == () + + rlist = ["a", ["b", "c"], [["d"], "e"], "f"] + assert "".join(argmap._flatten(rlist, set())) == "abcdef" + + def test_indent(self): + code = "\n".join( + argmap._indent( + *[ + "try:", + "try:", + "pass#", + "finally:", + "pass#", + "#", + "finally:", + "pass#", + ] + ) + ) + assert ( + code + == """try: + try: + pass# + finally: + pass# + # +finally: + pass#""" + ) + + def test_immediate_raise(self): + @not_implemented_for("directed") + def yield_nodes(G): + yield from G + + G = nx.Graph([(1, 2)]) + D = nx.DiGraph() + + # test first call (argmap is compiled and executed) + with pytest.raises(nx.NetworkXNotImplemented): + node_iter = yield_nodes(D) + + # test second call (argmap is only executed) + with pytest.raises(nx.NetworkXNotImplemented): + node_iter = yield_nodes(D) + + # ensure that generators still make generators + node_iter = yield_nodes(G) + next(node_iter) + next(node_iter) + with pytest.raises(StopIteration): + next(node_iter) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_heaps.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_heaps.py new file mode 100644 index 0000000000000000000000000000000000000000..5ea3871638688ed466b72bf3c99c977913a503dc --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_heaps.py @@ -0,0 +1,131 @@ +import pytest + +import networkx as nx +from networkx.utils import BinaryHeap, PairingHeap + + +class X: + def __eq__(self, other): + raise self is other + + def __ne__(self, other): + raise self is not other + + def __lt__(self, other): + raise TypeError("cannot compare") + + def __le__(self, other): + raise TypeError("cannot compare") + + def __ge__(self, other): + raise TypeError("cannot compare") + + def __gt__(self, other): + raise TypeError("cannot compare") + + def __hash__(self): + return hash(id(self)) + + +x = X() + + +data = [ # min should not invent an element. + ("min", nx.NetworkXError), + # Popping an empty heap should fail. + ("pop", nx.NetworkXError), + # Getting nonexisting elements should return None. + ("get", 0, None), + ("get", x, None), + ("get", None, None), + # Inserting a new key should succeed. + ("insert", x, 1, True), + ("get", x, 1), + ("min", (x, 1)), + # min should not pop the top element. + ("min", (x, 1)), + # Inserting a new key of different type should succeed. + ("insert", 1, -2.0, True), + # int and float values should interop. + ("min", (1, -2.0)), + # pop removes minimum-valued element. + ("insert", 3, -(10**100), True), + ("insert", 4, 5, True), + ("pop", (3, -(10**100))), + ("pop", (1, -2.0)), + # Decrease-insert should succeed. + ("insert", 4, -50, True), + ("insert", 4, -60, False, True), + # Decrease-insert should not create duplicate keys. + ("pop", (4, -60)), + ("pop", (x, 1)), + # Popping all elements should empty the heap. + ("min", nx.NetworkXError), + ("pop", nx.NetworkXError), + # Non-value-changing insert should fail. + ("insert", x, 0, True), + ("insert", x, 0, False, False), + ("min", (x, 0)), + ("insert", x, 0, True, False), + ("min", (x, 0)), + # Failed insert should not create duplicate keys. + ("pop", (x, 0)), + ("pop", nx.NetworkXError), + # Increase-insert should succeed when allowed. + ("insert", None, 0, True), + ("insert", 2, -1, True), + ("min", (2, -1)), + ("insert", 2, 1, True, False), + ("min", (None, 0)), + # Increase-insert should fail when disallowed. + ("insert", None, 2, False, False), + ("min", (None, 0)), + # Failed increase-insert should not create duplicate keys. + ("pop", (None, 0)), + ("pop", (2, 1)), + ("min", nx.NetworkXError), + ("pop", nx.NetworkXError), +] + + +def _test_heap_class(cls, *args, **kwargs): + heap = cls(*args, **kwargs) + # Basic behavioral test + for op in data: + if op[-1] is not nx.NetworkXError: + assert op[-1] == getattr(heap, op[0])(*op[1:-1]) + else: + pytest.raises(op[-1], getattr(heap, op[0]), *op[1:-1]) + # Coverage test. + for i in range(99, -1, -1): + assert heap.insert(i, i) + for i in range(50): + assert heap.pop() == (i, i) + for i in range(100): + assert heap.insert(i, i) == (i < 50) + for i in range(100): + assert not heap.insert(i, i + 1) + for i in range(50): + assert heap.pop() == (i, i) + for i in range(100): + assert heap.insert(i, i + 1) == (i < 50) + for i in range(49): + assert heap.pop() == (i, i + 1) + assert sorted([heap.pop(), heap.pop()]) == [(49, 50), (50, 50)] + for i in range(51, 100): + assert not heap.insert(i, i + 1, True) + for i in range(51, 70): + assert heap.pop() == (i, i + 1) + for i in range(100): + assert heap.insert(i, i) + for i in range(100): + assert heap.pop() == (i, i) + pytest.raises(nx.NetworkXError, heap.pop) + + +def test_PairingHeap(): + _test_heap_class(PairingHeap) + + +def test_BinaryHeap(): + _test_heap_class(BinaryHeap) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_mapped_queue.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_mapped_queue.py new file mode 100644 index 0000000000000000000000000000000000000000..ca9b7e42072f5aebbf4b794302d06f21f5d8e17c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_mapped_queue.py @@ -0,0 +1,268 @@ +import pytest + +from networkx.utils.mapped_queue import MappedQueue, _HeapElement + + +def test_HeapElement_gtlt(): + bar = _HeapElement(1.1, "a") + foo = _HeapElement(1, "b") + assert foo < bar + assert bar > foo + assert foo < 1.1 + assert 1 < bar + + +def test_HeapElement_gtlt_tied_priority(): + bar = _HeapElement(1, "a") + foo = _HeapElement(1, "b") + assert foo > bar + assert bar < foo + + +def test_HeapElement_eq(): + bar = _HeapElement(1.1, "a") + foo = _HeapElement(1, "a") + assert foo == bar + assert bar == foo + assert foo == "a" + + +def test_HeapElement_iter(): + foo = _HeapElement(1, "a") + bar = _HeapElement(1.1, (3, 2, 1)) + assert list(foo) == [1, "a"] + assert list(bar) == [1.1, 3, 2, 1] + + +def test_HeapElement_getitem(): + foo = _HeapElement(1, "a") + bar = _HeapElement(1.1, (3, 2, 1)) + assert foo[1] == "a" + assert foo[0] == 1 + assert bar[0] == 1.1 + assert bar[2] == 2 + assert bar[3] == 1 + pytest.raises(IndexError, bar.__getitem__, 4) + pytest.raises(IndexError, foo.__getitem__, 2) + + +class TestMappedQueue: + def setup_method(self): + pass + + def _check_map(self, q): + assert q.position == {elt: pos for pos, elt in enumerate(q.heap)} + + def _make_mapped_queue(self, h): + q = MappedQueue() + q.heap = h + q.position = {elt: pos for pos, elt in enumerate(h)} + return q + + def test_heapify(self): + h = [5, 4, 3, 2, 1, 0] + q = self._make_mapped_queue(h) + q._heapify() + self._check_map(q) + + def test_init(self): + h = [5, 4, 3, 2, 1, 0] + q = MappedQueue(h) + self._check_map(q) + + def test_incomparable(self): + h = [5, 4, "a", 2, 1, 0] + pytest.raises(TypeError, MappedQueue, h) + + def test_len(self): + h = [5, 4, 3, 2, 1, 0] + q = MappedQueue(h) + self._check_map(q) + assert len(q) == 6 + + def test_siftup_leaf(self): + h = [2] + h_sifted = [2] + q = self._make_mapped_queue(h) + q._siftup(0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftup_one_child(self): + h = [2, 0] + h_sifted = [0, 2] + q = self._make_mapped_queue(h) + q._siftup(0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftup_left_child(self): + h = [2, 0, 1] + h_sifted = [0, 2, 1] + q = self._make_mapped_queue(h) + q._siftup(0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftup_right_child(self): + h = [2, 1, 0] + h_sifted = [0, 1, 2] + q = self._make_mapped_queue(h) + q._siftup(0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftup_multiple(self): + h = [0, 1, 2, 4, 3, 5, 6] + h_sifted = [0, 1, 2, 4, 3, 5, 6] + q = self._make_mapped_queue(h) + q._siftup(0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftdown_leaf(self): + h = [2] + h_sifted = [2] + q = self._make_mapped_queue(h) + q._siftdown(0, 0) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftdown_single(self): + h = [1, 0] + h_sifted = [0, 1] + q = self._make_mapped_queue(h) + q._siftdown(0, len(h) - 1) + assert q.heap == h_sifted + self._check_map(q) + + def test_siftdown_multiple(self): + h = [1, 2, 3, 4, 5, 6, 7, 0] + h_sifted = [0, 1, 3, 2, 5, 6, 7, 4] + q = self._make_mapped_queue(h) + q._siftdown(0, len(h) - 1) + assert q.heap == h_sifted + self._check_map(q) + + def test_push(self): + to_push = [6, 1, 4, 3, 2, 5, 0] + h_sifted = [0, 2, 1, 6, 3, 5, 4] + q = MappedQueue() + for elt in to_push: + q.push(elt) + assert q.heap == h_sifted + self._check_map(q) + + def test_push_duplicate(self): + to_push = [2, 1, 0] + h_sifted = [0, 2, 1] + q = MappedQueue() + for elt in to_push: + inserted = q.push(elt) + assert inserted + assert q.heap == h_sifted + self._check_map(q) + inserted = q.push(1) + assert not inserted + + def test_pop(self): + h = [3, 4, 6, 0, 1, 2, 5] + h_sorted = sorted(h) + q = self._make_mapped_queue(h) + q._heapify() + popped = [q.pop() for _ in range(len(h))] + assert popped == h_sorted + self._check_map(q) + + def test_remove_leaf(self): + h = [0, 2, 1, 6, 3, 5, 4] + h_removed = [0, 2, 1, 6, 4, 5] + q = self._make_mapped_queue(h) + removed = q.remove(3) + assert q.heap == h_removed + + def test_remove_root(self): + h = [0, 2, 1, 6, 3, 5, 4] + h_removed = [1, 2, 4, 6, 3, 5] + q = self._make_mapped_queue(h) + removed = q.remove(0) + assert q.heap == h_removed + + def test_update_leaf(self): + h = [0, 20, 10, 60, 30, 50, 40] + h_updated = [0, 15, 10, 60, 20, 50, 40] + q = self._make_mapped_queue(h) + removed = q.update(30, 15) + assert q.heap == h_updated + + def test_update_root(self): + h = [0, 20, 10, 60, 30, 50, 40] + h_updated = [10, 20, 35, 60, 30, 50, 40] + q = self._make_mapped_queue(h) + removed = q.update(0, 35) + assert q.heap == h_updated + + +class TestMappedDict(TestMappedQueue): + def _make_mapped_queue(self, h): + priority_dict = {elt: elt for elt in h} + return MappedQueue(priority_dict) + + def test_init(self): + d = {5: 0, 4: 1, "a": 2, 2: 3, 1: 4} + q = MappedQueue(d) + assert q.position == d + + def test_ties(self): + d = {5: 0, 4: 1, 3: 2, 2: 3, 1: 4} + q = MappedQueue(d) + assert q.position == {elt: pos for pos, elt in enumerate(q.heap)} + + def test_pop(self): + d = {5: 0, 4: 1, 3: 2, 2: 3, 1: 4} + q = MappedQueue(d) + assert q.pop() == _HeapElement(0, 5) + assert q.position == {elt: pos for pos, elt in enumerate(q.heap)} + + def test_empty_pop(self): + q = MappedQueue() + pytest.raises(IndexError, q.pop) + + def test_incomparable_ties(self): + d = {5: 0, 4: 0, "a": 0, 2: 0, 1: 0} + pytest.raises(TypeError, MappedQueue, d) + + def test_push(self): + to_push = [6, 1, 4, 3, 2, 5, 0] + h_sifted = [0, 2, 1, 6, 3, 5, 4] + q = MappedQueue() + for elt in to_push: + q.push(elt, priority=elt) + assert q.heap == h_sifted + self._check_map(q) + + def test_push_duplicate(self): + to_push = [2, 1, 0] + h_sifted = [0, 2, 1] + q = MappedQueue() + for elt in to_push: + inserted = q.push(elt, priority=elt) + assert inserted + assert q.heap == h_sifted + self._check_map(q) + inserted = q.push(1, priority=1) + assert not inserted + + def test_update_leaf(self): + h = [0, 20, 10, 60, 30, 50, 40] + h_updated = [0, 15, 10, 60, 20, 50, 40] + q = self._make_mapped_queue(h) + removed = q.update(30, 15, priority=15) + assert q.heap == h_updated + + def test_update_root(self): + h = [0, 20, 10, 60, 30, 50, 40] + h_updated = [10, 20, 35, 60, 30, 50, 40] + q = self._make_mapped_queue(h) + removed = q.update(0, 35, priority=35) + assert q.heap == h_updated diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_misc.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_misc.py new file mode 100644 index 0000000000000000000000000000000000000000..e1a874955125f84e651be24c09a31c7efa97b82c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_misc.py @@ -0,0 +1,393 @@ +import random +from copy import copy + +import pytest + +import networkx as nx +from networkx.utils import ( + PythonRandomInterface, + PythonRandomViaNumpyBits, + arbitrary_element, + create_py_random_state, + create_random_state, + dict_to_numpy_array, + discrete_sequence, + edges_equal, + flatten, + groups, + make_list_of_ints, + pairwise, + powerlaw_sequence, +) +from networkx.utils.misc import _dict_to_numpy_array1, _dict_to_numpy_array2 + +nested_depth = ( + 1, + 2, + (3, 4, ((5, 6, (7,), (8, (9, 10), 11), (12, 13, (14, 15)), 16), 17), 18, 19), + 20, +) + +nested_set = { + (1, 2, 3, 4), + (5, 6, 7, 8, 9), + (10, 11, (12, 13, 14), (15, 16, 17, 18)), + 19, + 20, +} + +nested_mixed = [ + 1, + (2, 3, {4, (5, 6), 7}, [8, 9]), + {10: "foo", 11: "bar", (12, 13): "baz"}, + {(14, 15): "qwe", 16: "asd"}, + (17, (18, "19"), 20), +] + + +@pytest.mark.parametrize("result", [None, [], ["existing"], ["existing1", "existing2"]]) +@pytest.mark.parametrize("nested", [nested_depth, nested_mixed, nested_set]) +def test_flatten(nested, result): + if result is None: + val = flatten(nested, result) + assert len(val) == 20 + else: + _result = copy(result) # because pytest passes parameters as is + nexisting = len(_result) + val = flatten(nested, _result) + assert len(val) == len(_result) == 20 + nexisting + + assert issubclass(type(val), tuple) + + +def test_make_list_of_ints(): + mylist = [1, 2, 3.0, 42, -2] + assert make_list_of_ints(mylist) is mylist + assert make_list_of_ints(mylist) == mylist + assert isinstance(make_list_of_ints(mylist)[2], int) + pytest.raises(nx.NetworkXError, make_list_of_ints, [1, 2, 3, "kermit"]) + pytest.raises(nx.NetworkXError, make_list_of_ints, [1, 2, 3.1]) + + +def test_random_number_distribution(): + # smoke test only + z = powerlaw_sequence(20, exponent=2.5) + z = discrete_sequence(20, distribution=[0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3]) + + +class TestNumpyArray: + @classmethod + def setup_class(cls): + global np + np = pytest.importorskip("numpy") + + def test_numpy_to_list_of_ints(self): + a = np.array([1, 2, 3], dtype=np.int64) + b = np.array([1.0, 2, 3]) + c = np.array([1.1, 2, 3]) + assert isinstance(make_list_of_ints(a), list) + assert make_list_of_ints(b) == list(b) + B = make_list_of_ints(b) + assert isinstance(B[0], int) + pytest.raises(nx.NetworkXError, make_list_of_ints, c) + + def test__dict_to_numpy_array1(self): + d = {"a": 1, "b": 2} + a = _dict_to_numpy_array1(d, mapping={"a": 0, "b": 1}) + np.testing.assert_allclose(a, np.array([1, 2])) + a = _dict_to_numpy_array1(d, mapping={"b": 0, "a": 1}) + np.testing.assert_allclose(a, np.array([2, 1])) + + a = _dict_to_numpy_array1(d) + np.testing.assert_allclose(a.sum(), 3) + + def test__dict_to_numpy_array2(self): + d = {"a": {"a": 1, "b": 2}, "b": {"a": 10, "b": 20}} + + mapping = {"a": 1, "b": 0} + a = _dict_to_numpy_array2(d, mapping=mapping) + np.testing.assert_allclose(a, np.array([[20, 10], [2, 1]])) + + a = _dict_to_numpy_array2(d) + np.testing.assert_allclose(a.sum(), 33) + + def test_dict_to_numpy_array_a(self): + d = {"a": {"a": 1, "b": 2}, "b": {"a": 10, "b": 20}} + + mapping = {"a": 0, "b": 1} + a = dict_to_numpy_array(d, mapping=mapping) + np.testing.assert_allclose(a, np.array([[1, 2], [10, 20]])) + + mapping = {"a": 1, "b": 0} + a = dict_to_numpy_array(d, mapping=mapping) + np.testing.assert_allclose(a, np.array([[20, 10], [2, 1]])) + + a = _dict_to_numpy_array2(d) + np.testing.assert_allclose(a.sum(), 33) + + def test_dict_to_numpy_array_b(self): + d = {"a": 1, "b": 2} + + mapping = {"a": 0, "b": 1} + a = dict_to_numpy_array(d, mapping=mapping) + np.testing.assert_allclose(a, np.array([1, 2])) + + a = _dict_to_numpy_array1(d) + np.testing.assert_allclose(a.sum(), 3) + + +def test_pairwise(): + nodes = range(4) + node_pairs = [(0, 1), (1, 2), (2, 3)] + node_pairs_cycle = node_pairs + [(3, 0)] + assert list(pairwise(nodes)) == node_pairs + assert list(pairwise(iter(nodes))) == node_pairs + assert list(pairwise(nodes, cyclic=True)) == node_pairs_cycle + empty_iter = iter(()) + assert list(pairwise(empty_iter)) == [] + empty_iter = iter(()) + assert list(pairwise(empty_iter, cyclic=True)) == [] + + +def test_groups(): + many_to_one = dict(zip("abcde", [0, 0, 1, 1, 2])) + actual = groups(many_to_one) + expected = {0: {"a", "b"}, 1: {"c", "d"}, 2: {"e"}} + assert actual == expected + assert {} == groups({}) + + +def test_create_random_state(): + np = pytest.importorskip("numpy") + rs = np.random.RandomState + + assert isinstance(create_random_state(1), rs) + assert isinstance(create_random_state(None), rs) + assert isinstance(create_random_state(np.random), rs) + assert isinstance(create_random_state(rs(1)), rs) + # Support for numpy.random.Generator + rng = np.random.default_rng() + assert isinstance(create_random_state(rng), np.random.Generator) + pytest.raises(ValueError, create_random_state, "a") + + assert np.all(rs(1).rand(10) == create_random_state(1).rand(10)) + + +def test_create_py_random_state(): + pyrs = random.Random + + assert isinstance(create_py_random_state(1), pyrs) + assert isinstance(create_py_random_state(None), pyrs) + assert isinstance(create_py_random_state(pyrs(1)), pyrs) + pytest.raises(ValueError, create_py_random_state, "a") + + np = pytest.importorskip("numpy") + + rs = np.random.RandomState + rng = np.random.default_rng(1000) + rng_explicit = np.random.Generator(np.random.SFC64()) + old_nprs = PythonRandomInterface + nprs = PythonRandomViaNumpyBits + assert isinstance(create_py_random_state(np.random), nprs) + assert isinstance(create_py_random_state(rs(1)), old_nprs) + assert isinstance(create_py_random_state(rng), nprs) + assert isinstance(create_py_random_state(rng_explicit), nprs) + # test default rng input + old_nprs_instance = old_nprs() + nprs_instance = nprs() + assert isinstance(old_nprs_instance, old_nprs) + assert isinstance(nprs_instance, nprs) + assert create_py_random_state(old_nprs_instance) == old_nprs_instance + assert create_py_random_state(nprs_instance) == nprs_instance + + # VeryLargeIntegers Smoke test (they raise error for np.random) + int64max = 9223372036854775807 # from np.iinfo(np.int64).max + for r in (rng, rs(1)): + prs = create_py_random_state(r) + prs.randrange(3, int64max + 5) + prs.randint(3, int64max + 5) + + +def test_PythonRandomInterface_RandomState(): + np = pytest.importorskip("numpy") + + seed = 42 + rs = np.random.RandomState + rng = PythonRandomInterface(rs(seed)) + rs42 = rs(seed) + + # make sure these functions are same as expected outcome + assert rng.randrange(3, 5) == rs42.randint(3, 5) + assert rng.randrange(2) == rs42.randint(0, 2) + assert rng.uniform(1, 10) == rs42.uniform(1, 10) + assert rng.choice([1, 2, 3]) == rs42.choice([1, 2, 3]) + assert rng.gauss(0, 1) == rs42.normal(0, 1) + assert rng.expovariate(1.5) == rs42.exponential(1 / 1.5) + assert rng.paretovariate(2) == rs42.pareto(2) + assert np.all(rng.shuffle([1, 2, 3]) == rs42.shuffle([1, 2, 3])) + assert np.all( + rng.sample([1, 2, 3], 2) == rs42.choice([1, 2, 3], (2,), replace=False) + ) + assert np.all( + [rng.randint(3, 5) for _ in range(100)] + == [rs42.randint(3, 6) for _ in range(100)] + ) + assert rng.random() == rs42.random_sample() + + +def test_PythonRandomInterface_Generator(): + np = pytest.importorskip("numpy") + + seed = 42 + rng = np.random.default_rng(seed) + pri = PythonRandomInterface(np.random.default_rng(seed)) + + # make sure these functions are same as expected outcome + assert pri.randrange(3, 5) == rng.integers(3, 5) + assert pri.randrange(2) == rng.integers(0, 2) + assert pri.uniform(1, 10) == rng.uniform(1, 10) + assert pri.choice([1, 2, 3]) == rng.choice([1, 2, 3]) + assert pri.gauss(0, 1) == rng.normal(0, 1) + assert pri.expovariate(1.5) == rng.exponential(1 / 1.5) + assert pri.paretovariate(2) == rng.pareto(2) + assert np.all(pri.shuffle([1, 2, 3]) == rng.shuffle([1, 2, 3])) + assert np.all( + pri.sample([1, 2, 3], 2) == rng.choice([1, 2, 3], (2,), replace=False) + ) + assert np.all( + [pri.randint(3, 5) for _ in range(100)] + == [rng.integers(3, 6) for _ in range(100)] + ) + assert pri.random() == rng.random() + + +@pytest.mark.parametrize( + ("iterable_type", "expected"), ((list, 1), (tuple, 1), (str, "["), (set, 1)) +) +def test_arbitrary_element(iterable_type, expected): + iterable = iterable_type([1, 2, 3]) + assert arbitrary_element(iterable) == expected + + +@pytest.mark.parametrize( + "iterator", + ((i for i in range(3)), iter([1, 2, 3])), # generator +) +def test_arbitrary_element_raises(iterator): + """Value error is raised when input is an iterator.""" + with pytest.raises(ValueError, match="from an iterator"): + arbitrary_element(iterator) + + +@pytest.mark.parametrize("n", [5, 10, 20]) +@pytest.mark.parametrize("gen", [nx.complete_graph, nx.path_graph, nx.cycle_graph]) +@pytest.mark.parametrize("create_using", [nx.Graph, nx.DiGraph]) +def test_edges_equal(n, gen, create_using): + """Test whether edges_equal properly compares edges without attribute data.""" + G = gen(n, create_using=create_using) + H = gen(n, create_using=create_using) + assert edges_equal(G.edges(), H.edges(), directed=G.is_directed()) + assert edges_equal(H.edges(), G.edges(), directed=H.is_directed()) + + H.remove_edge(0, 1) + assert edges_equal(H.edges(), H.edges(), directed=H.is_directed()) + assert not edges_equal(G.edges(), H.edges(), directed=G.is_directed()) + assert not edges_equal(H.edges(), G.edges(), directed=H.is_directed()) + + +@pytest.mark.parametrize("n", [5, 10, 20]) +@pytest.mark.parametrize("gen", [nx.complete_graph, nx.path_graph, nx.cycle_graph]) +@pytest.mark.parametrize("create_using", [nx.MultiGraph, nx.MultiDiGraph]) +def test_edges_equal_multiedge(n, gen, create_using): + """Test whether ``edges_equal`` properly compares edges in multigraphs.""" + G = gen(n, create_using=create_using) + H = gen(n, create_using=create_using) + + G_edges = list(G.edges()) + G.add_edges_from(G_edges) + H.add_edges_from(G_edges) + assert edges_equal(G.edges(), H.edges(), directed=G.is_directed()) + + H.remove_edge(0, 1) + assert edges_equal(H.edges(), H.edges(), directed=H.is_directed()) + assert not edges_equal(G.edges(), H.edges(), directed=G.is_directed()) + + +@pytest.mark.parametrize("n", [5, 10, 20]) +@pytest.mark.parametrize("gen", [nx.complete_graph, nx.path_graph, nx.cycle_graph]) +@pytest.mark.parametrize("weight", [1, 2, 3]) +def test_edges_equal_weighted(n, gen, weight): + """Test whether ``edges_equal`` properly compares edges with weight data.""" + G = gen(n) + H = gen(n) + + G_edges = list(G.edges()) + G.add_weighted_edges_from((*e, weight) for e in G_edges) + assert edges_equal(G.edges(), G.edges()) + + H.add_weighted_edges_from((*e, weight + 1) for e in G_edges) + assert edges_equal(H.edges(), H.edges()) + assert not edges_equal(G.edges(data=True), H.edges(data=True)) + + +def test_edges_equal_data(): + """Test whether ``edges_equal`` properly compares edges with attribute dictionaries.""" + G = nx.path_graph(3) + H = nx.path_graph(3) + I = nx.path_graph(3, create_using=nx.MultiGraph) + + attrs = {(0, 1): {"attr1": 20, "attr2": "nothing"}, (1, 2): {"attr2": 3}} + nx.set_edge_attributes(G, attrs) + assert edges_equal(G.edges(data=True), G.edges(data=True)) + assert not edges_equal(G.edges(data=True), G.edges()) + + nx.set_edge_attributes(H, attrs) + assert edges_equal(G.edges(), H.edges()) + assert edges_equal(G.edges(data=True), H.edges(data=True)) + + H[0][1]["attr2"] = "something" + assert edges_equal(G.edges(), H.edges()) + assert not edges_equal(G.edges(data=True), H.edges(data=True)) + + +def test_edges_equal_multigraph_data(): + """Test whether ``edges_equal`` properly compares edges with attribute dictionaries in ``MultiGraphs``.""" + G = nx.path_graph(3, create_using=nx.MultiGraph) + I = nx.path_graph(3, create_using=nx.MultiGraph) + + G.add_edge(0, 1, 0, attr1="blue") + G.add_edge(1, 2, 1, attr2="green") + I.add_edge(0, 1, 0, attr1="blue") + I.add_edge(0, 1, 1, attr2="green") + assert edges_equal(G.edges(data=True), G.edges(data=True)) + assert not edges_equal(G.edges(), I.edges()) + assert not edges_equal(G.edges(data=True), I.edges(data=True)) + assert not edges_equal(G.edges(keys=True), I.edges(keys=True)) + assert not edges_equal(G.edges(keys=True, data=True), I.edges(keys=True, data=True)) + + +def test_edges_equal_directed(): + """Test whether ``edges_equal`` properly compares directed edges.""" + G = nx.DiGraph([(0, 1)]) + I = nx.DiGraph([(1, 0)]) + + assert edges_equal(G.edges(), I.edges(), directed=False) + assert not edges_equal(G.edges(), I.edges(), directed=True) + + +def test_edges_equal_directed_data(): + """Test whether ``edges_equal`` properly compares directed edges with attribute dictionaries.""" + G = nx.DiGraph() + I = nx.DiGraph() + + G.add_edge(0, 1, attr1="blue") + I.add_edge(0, 1, attr1="blue") + assert edges_equal(G.edges(data=True), G.edges(data=True), directed=True) + I.add_edge(1, 2, attr2="green") + assert not edges_equal(G.edges(data=True), I.edges(data=True), directed=True) + G.add_edge(1, 2, attr2="green") + assert edges_equal(G.edges(data=True), I.edges(data=True), directed=True) + G.remove_edge(1, 2) + G.add_edge(2, 1, attr2="green") + assert edges_equal(G.edges(data=True), I.edges(data=True), directed=False) + assert not edges_equal(G.edges(data=True), I.edges(data=True), directed=True) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_random_sequence.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_random_sequence.py new file mode 100644 index 0000000000000000000000000000000000000000..edc28f07461f138c38a0acbbe1affbb966c1efef --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_random_sequence.py @@ -0,0 +1,53 @@ +import pytest + +import networkx as nx + + +def test_degree_sequences(): + seq = nx.utils.powerlaw_sequence(10, seed=1) + seq = nx.utils.powerlaw_sequence(10) + assert len(seq) == 10 + + +@pytest.mark.parametrize( + ("deg_seq", "valid", "reason"), + [ + ([], False, "must have one more node"), + ([0], True, ""), + ([2], False, "must have one more node"), + ([2, 0], False, "must have strictly positive"), + ([3, 1, 1, 1], True, ""), + ], +) +def test_valid_degree_sequence(deg_seq, valid, reason): + v, r = nx.utils.is_valid_tree_degree_sequence(deg_seq) + assert v == valid + assert reason in r + + +def test_zipf_rv(): + r = nx.utils.zipf_rv(2.3, xmin=2, seed=1) + r = nx.utils.zipf_rv(2.3, 2, 1) + r = nx.utils.zipf_rv(2.3) + assert type(r), int + pytest.raises(ValueError, nx.utils.zipf_rv, 0.5) + pytest.raises(ValueError, nx.utils.zipf_rv, 2, xmin=0) + + +def test_random_weighted_sample(): + mapping = {"a": 10, "b": 20} + s = nx.utils.random_weighted_sample(mapping, 2, seed=1) + s = nx.utils.random_weighted_sample(mapping, 2) + assert sorted(s) == sorted(mapping.keys()) + pytest.raises(ValueError, nx.utils.random_weighted_sample, mapping, 3) + + +def test_random_weighted_choice(): + mapping = {"a": 10, "b": 0} + c = nx.utils.weighted_choice(mapping, seed=1) + c = nx.utils.weighted_choice(mapping) + assert c == "a" + + +def test_random_sequence_low_precision(): + assert nx.utils.cumulative_distribution([0.1] * 100)[-1] == 1.0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_rcm.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_rcm.py new file mode 100644 index 0000000000000000000000000000000000000000..88702b3635dfa173f27eb283bc769d0930918e62 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_rcm.py @@ -0,0 +1,63 @@ +import networkx as nx +from networkx.utils import reverse_cuthill_mckee_ordering + + +def test_reverse_cuthill_mckee(): + # example graph from + # http://www.boost.org/doc/libs/1_37_0/libs/graph/example/cuthill_mckee_ordering.cpp + G = nx.Graph( + [ + (0, 3), + (0, 5), + (1, 2), + (1, 4), + (1, 6), + (1, 9), + (2, 3), + (2, 4), + (3, 5), + (3, 8), + (4, 6), + (5, 6), + (5, 7), + (6, 7), + ] + ) + rcm = list(reverse_cuthill_mckee_ordering(G)) + assert rcm in [[0, 8, 5, 7, 3, 6, 2, 4, 1, 9], [0, 8, 5, 7, 3, 6, 4, 2, 1, 9]] + + +def test_rcm_alternate_heuristic(): + # example from + G = nx.Graph( + [ + (0, 0), + (0, 4), + (1, 1), + (1, 2), + (1, 5), + (1, 7), + (2, 2), + (2, 4), + (3, 3), + (3, 6), + (4, 4), + (5, 5), + (5, 7), + (6, 6), + (7, 7), + ] + ) + + answers = [ + [6, 3, 5, 7, 1, 2, 4, 0], + [6, 3, 7, 5, 1, 2, 4, 0], + [7, 5, 1, 2, 4, 0, 6, 3], + ] + + def smallest_degree(G): + deg, node = min((d, n) for n, d in G.degree()) + return node + + rcm = list(reverse_cuthill_mckee_ordering(G, heuristic=smallest_degree)) + assert rcm in answers diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_unionfind.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_unionfind.py new file mode 100644 index 0000000000000000000000000000000000000000..2d30580fc942e3715f2a6a25125bad9f9e1e74b6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/tests/test_unionfind.py @@ -0,0 +1,55 @@ +import networkx as nx + + +def test_unionfind(): + # Fixed by: 2cddd5958689bdecdcd89b91ac9aaf6ce0e4f6b8 + # Previously (in 2.x), the UnionFind class could handle mixed types. + # But in Python 3.x, this causes a TypeError such as: + # TypeError: unorderable types: str() > int() + # + # Now we just make sure that no exception is raised. + x = nx.utils.UnionFind() + x.union(0, "a") + + +def test_subtree_union(): + # See https://github.com/networkx/networkx/pull/3224 + # (35db1b551ee65780794a357794f521d8768d5049). + # Test if subtree unions hare handled correctly by to_sets(). + uf = nx.utils.UnionFind() + uf.union(1, 2) + uf.union(3, 4) + uf.union(4, 5) + uf.union(1, 5) + assert list(uf.to_sets()) == [{1, 2, 3, 4, 5}] + + +def test_unionfind_weights(): + # Tests if weights are computed correctly with unions of many elements + uf = nx.utils.UnionFind() + uf.union(1, 4, 7) + uf.union(2, 5, 8) + uf.union(3, 6, 9) + uf.union(1, 2, 3, 4, 5, 6, 7, 8, 9) + assert uf.weights[uf[1]] == 9 + + +def test_unbalanced_merge_weights(): + # Tests if the largest set's root is used as the new root when merging + uf = nx.utils.UnionFind() + uf.union(1, 2, 3) + uf.union(4, 5, 6, 7, 8, 9) + assert uf.weights[uf[1]] == 3 + assert uf.weights[uf[4]] == 6 + largest_root = uf[4] + uf.union(1, 4) + assert uf[1] == largest_root + assert uf.weights[largest_root] == 9 + + +def test_empty_union(): + # Tests if a null-union does nothing. + uf = nx.utils.UnionFind((0, 1)) + uf.union() + assert uf[0] == 0 + assert uf[1] == 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/union_find.py b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/union_find.py new file mode 100644 index 0000000000000000000000000000000000000000..2a07129f5427cd8a3caf30095efee125bc3d853b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/networkx/utils/union_find.py @@ -0,0 +1,106 @@ +""" +Union-find data structure. +""" + +from networkx.utils import groups + + +class UnionFind: + """Union-find data structure. + + Each unionFind instance X maintains a family of disjoint sets of + hashable objects, supporting the following two methods: + + - X[item] returns a name for the set containing the given item. + Each set is named by an arbitrarily-chosen one of its members; as + long as the set remains unchanged it will keep the same name. If + the item is not yet part of a set in X, a new singleton set is + created for it. + + - X.union(item1, item2, ...) merges the sets containing each item + into a single larger set. If any item is not yet part of a set + in X, it is added to X as one of the members of the merged set. + + Union-find data structure. Based on Josiah Carlson's code, + https://code.activestate.com/recipes/215912/ + with significant additional changes by D. Eppstein. + http://www.ics.uci.edu/~eppstein/PADS/UnionFind.py + + """ + + def __init__(self, elements=None): + """Create a new empty union-find structure. + + If *elements* is an iterable, this structure will be initialized + with the discrete partition on the given set of elements. + + """ + if elements is None: + elements = () + self.parents = {} + self.weights = {} + for x in elements: + self.weights[x] = 1 + self.parents[x] = x + + def __getitem__(self, object): + """Find and return the name of the set containing the object.""" + + # check for previously unknown object + if object not in self.parents: + self.parents[object] = object + self.weights[object] = 1 + return object + + # find path of objects leading to the root + path = [] + root = self.parents[object] + while root != object: + path.append(object) + object = root + root = self.parents[object] + + # compress the path and return + for ancestor in path: + self.parents[ancestor] = root + return root + + def __iter__(self): + """Iterate through all items ever found or unioned by this structure.""" + return iter(self.parents) + + def to_sets(self): + """Iterates over the sets stored in this structure. + + For example:: + + >>> partition = UnionFind("xyz") + >>> sorted(map(sorted, partition.to_sets())) + [['x'], ['y'], ['z']] + >>> partition.union("x", "y") + >>> sorted(map(sorted, partition.to_sets())) + [['x', 'y'], ['z']] + + """ + # Ensure fully pruned paths + for x in self.parents: + _ = self[x] # Evaluated for side-effect only + + yield from groups(self.parents).values() + + def union(self, *objects): + """Find the sets containing the objects and merge them all.""" + # Find the heaviest root according to its weight. + roots = iter( + sorted( + {self[x] for x in objects}, key=lambda r: self.weights[r], reverse=True + ) + ) + try: + root = next(roots) + except StopIteration: + return + + for r in roots: + self.weights[root] += self.weights[r] + self.parents[r] = root diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/nvidia_curand-10.4.0.35.dist-info/licenses/License.txt b/URSA/.venv_ursa/lib/python3.12/site-packages/nvidia_curand-10.4.0.35.dist-info/licenses/License.txt new file mode 100644 index 0000000000000000000000000000000000000000..b491c70e0aef319022ded661e111ddbd45b8a17f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/nvidia_curand-10.4.0.35.dist-info/licenses/License.txt @@ -0,0 +1,1568 @@ +End User License Agreement +-------------------------- + + +Preface +------- + +The Software License Agreement in Chapter 1 and the Supplement +in Chapter 2 contain license terms and conditions that govern +the use of NVIDIA software. By accepting this agreement, you +agree to comply with all the terms and conditions applicable +to the product(s) included herein. + + +NVIDIA Driver + + +Description + +This package contains the operating system driver and +fundamental system software components for NVIDIA GPUs. + + +NVIDIA CUDA Toolkit + + +Description + +The NVIDIA CUDA Toolkit provides command-line and graphical +tools for building, debugging and optimizing the performance +of applications accelerated by NVIDIA GPUs, runtime and math +libraries, and documentation including programming guides, +user manuals, and API references. + + +Default Install Location of CUDA Toolkit + +Windows platform: + +%ProgramFiles%\NVIDIA GPU Computing Toolkit\CUDA\v#.# + +Linux platform: + +/usr/local/cuda-#.# + +Mac platform: + +/Developer/NVIDIA/CUDA-#.# + + +NVIDIA CUDA Samples + + +Description + +This package includes over 100+ CUDA examples that demonstrate +various CUDA programming principles, and efficient CUDA +implementation of algorithms in specific application domains. + + +Default Install Location of CUDA Samples + +Windows platform: + +%ProgramData%\NVIDIA Corporation\CUDA Samples\v#.# + +Linux platform: + +/usr/local/cuda-#.#/samples + +and + +$HOME/NVIDIA_CUDA-#.#_Samples + +Mac platform: + +/Developer/NVIDIA/CUDA-#.#/samples + + +NVIDIA Nsight Visual Studio Edition (Windows only) + + +Description + +NVIDIA Nsight Development Platform, Visual Studio Edition is a +development environment integrated into Microsoft Visual +Studio that provides tools for debugging, profiling, analyzing +and optimizing your GPU computing and graphics applications. + + +Default Install Location of Nsight Visual Studio Edition + +Windows platform: + +%ProgramFiles(x86)%\NVIDIA Corporation\Nsight Visual Studio Edition #.# + + +1. License Agreement for NVIDIA Software Development Kits +--------------------------------------------------------- + + +Release Date: July 26, 2018 +--------------------------- + + +Important NoticeRead before downloading, installing, +copying or using the licensed software: +------------------------------------------------------- + +This license agreement, including exhibits attached +("Agreement”) is a legal agreement between you and NVIDIA +Corporation ("NVIDIA") and governs your use of a NVIDIA +software development kit (“SDK”). + +Each SDK has its own set of software and materials, but here +is a description of the types of items that may be included in +a SDK: source code, header files, APIs, data sets and assets +(examples include images, textures, models, scenes, videos, +native API input/output files), binary software, sample code, +libraries, utility programs, programming code and +documentation. + +This Agreement can be accepted only by an adult of legal age +of majority in the country in which the SDK is used. + +If you are entering into this Agreement on behalf of a company +or other legal entity, you represent that you have the legal +authority to bind the entity to this Agreement, in which case +“you” will mean the entity you represent. + +If you don’t have the required age or authority to accept +this Agreement, or if you don’t accept all the terms and +conditions of this Agreement, do not download, install or use +the SDK. + +You agree to use the SDK only for purposes that are permitted +by (a) this Agreement, and (b) any applicable law, regulation +or generally accepted practices or guidelines in the relevant +jurisdictions. + + +1.1. License + + +1.1.1. License Grant + +Subject to the terms of this Agreement, NVIDIA hereby grants +you a non-exclusive, non-transferable license, without the +right to sublicense (except as expressly provided in this +Agreement) to: + + 1. Install and use the SDK, + + 2. Modify and create derivative works of sample source code + delivered in the SDK, and + + 3. Distribute those portions of the SDK that are identified + in this Agreement as distributable, as incorporated in + object code format into a software application that meets + the distribution requirements indicated in this Agreement. + + +1.1.2. Distribution Requirements + +These are the distribution requirements for you to exercise +the distribution grant: + + 1. Your application must have material additional + functionality, beyond the included portions of the SDK. + + 2. The distributable portions of the SDK shall only be + accessed by your application. + + 3. The following notice shall be included in modifications + and derivative works of sample source code distributed: + “This software contains source code provided by NVIDIA + Corporation.” + + 4. Unless a developer tool is identified in this Agreement + as distributable, it is delivered for your internal use + only. + + 5. The terms under which you distribute your application + must be consistent with the terms of this Agreement, + including (without limitation) terms relating to the + license grant and license restrictions and protection of + NVIDIA’s intellectual property rights. Additionally, you + agree that you will protect the privacy, security and + legal rights of your application users. + + 6. You agree to notify NVIDIA in writing of any known or + suspected distribution or use of the SDK not in compliance + with the requirements of this Agreement, and to enforce + the terms of your agreements with respect to distributed + SDK. + + +1.1.3. Authorized Users + +You may allow employees and contractors of your entity or of +your subsidiary(ies) to access and use the SDK from your +secure network to perform work on your behalf. + +If you are an academic institution you may allow users +enrolled or employed by the academic institution to access and +use the SDK from your secure network. + +You are responsible for the compliance with the terms of this +Agreement by your authorized users. If you become aware that +your authorized users didn’t follow the terms of this +Agreement, you agree to take reasonable steps to resolve the +non-compliance and prevent new occurrences. + + +1.1.4. Pre-Release SDK + +The SDK versions identified as alpha, beta, preview or +otherwise as pre-release, may not be fully functional, may +contain errors or design flaws, and may have reduced or +different security, privacy, accessibility, availability, and +reliability standards relative to commercial versions of +NVIDIA software and materials. Use of a pre-release SDK may +result in unexpected results, loss of data, project delays or +other unpredictable damage or loss. + +You may use a pre-release SDK at your own risk, understanding +that pre-release SDKs are not intended for use in production +or business-critical systems. + +NVIDIA may choose not to make available a commercial version +of any pre-release SDK. NVIDIA may also choose to abandon +development and terminate the availability of a pre-release +SDK at any time without liability. + + +1.1.5. Updates + +NVIDIA may, at its option, make available patches, workarounds +or other updates to this SDK. Unless the updates are provided +with their separate governing terms, they are deemed part of +the SDK licensed to you as provided in this Agreement. You +agree that the form and content of the SDK that NVIDIA +provides may change without prior notice to you. While NVIDIA +generally maintains compatibility between versions, NVIDIA may +in some cases make changes that introduce incompatibilities in +future versions of the SDK. + + +1.1.6. Third Party Licenses + +The SDK may come bundled with, or otherwise include or be +distributed with, third party software licensed by a NVIDIA +supplier and/or open source software provided under an open +source license. Use of third party software is subject to the +third-party license terms, or in the absence of third party +terms, the terms of this Agreement. Copyright to third party +software is held by the copyright holders indicated in the +third-party software or license. + + +1.1.7. Reservation of Rights + +NVIDIA reserves all rights, title, and interest in and to the +SDK, not expressly granted to you under this Agreement. + + +1.2. Limitations + +The following license limitations apply to your use of the +SDK: + + 1. You may not reverse engineer, decompile or disassemble, + or remove copyright or other proprietary notices from any + portion of the SDK or copies of the SDK. + + 2. Except as expressly provided in this Agreement, you may + not copy, sell, rent, sublicense, transfer, distribute, + modify, or create derivative works of any portion of the + SDK. For clarity, you may not distribute or sublicense the + SDK as a stand-alone product. + + 3. Unless you have an agreement with NVIDIA for this + purpose, you may not indicate that an application created + with the SDK is sponsored or endorsed by NVIDIA. + + 4. You may not bypass, disable, or circumvent any + encryption, security, digital rights management or + authentication mechanism in the SDK. + + 5. You may not use the SDK in any manner that would cause it + to become subject to an open source software license. As + examples, licenses that require as a condition of use, + modification, and/or distribution that the SDK be: + + a. Disclosed or distributed in source code form; + + b. Licensed for the purpose of making derivative works; + or + + c. Redistributable at no charge. + + 6. Unless you have an agreement with NVIDIA for this + purpose, you may not use the SDK with any system or + application where the use or failure of the system or + application can reasonably be expected to threaten or + result in personal injury, death, or catastrophic loss. + Examples include use in avionics, navigation, military, + medical, life support or other life critical applications. + NVIDIA does not design, test or manufacture the SDK for + these critical uses and NVIDIA shall not be liable to you + or any third party, in whole or in part, for any claims or + damages arising from such uses. + + 7. You agree to defend, indemnify and hold harmless NVIDIA + and its affiliates, and their respective employees, + contractors, agents, officers and directors, from and + against any and all claims, damages, obligations, losses, + liabilities, costs or debt, fines, restitutions and + expenses (including but not limited to attorney’s fees + and costs incident to establishing the right of + indemnification) arising out of or related to your use of + the SDK outside of the scope of this Agreement, or not in + compliance with its terms. + + +1.3. Ownership + + 1. NVIDIA or its licensors hold all rights, title and + interest in and to the SDK and its modifications and + derivative works, including their respective intellectual + property rights, subject to your rights described in this + section. This SDK may include software and materials from + NVIDIA’s licensors, and these licensors are intended + third party beneficiaries that may enforce this Agreement + with respect to their intellectual property rights. + + 2. You hold all rights, title and interest in and to your + applications and your derivative works of the sample + source code delivered in the SDK, including their + respective intellectual property rights, subject to + NVIDIA’s rights described in this section. + + 3. You may, but don’t have to, provide to NVIDIA + suggestions, feature requests or other feedback regarding + the SDK, including possible enhancements or modifications + to the SDK. For any feedback that you voluntarily provide, + you hereby grant NVIDIA and its affiliates a perpetual, + non-exclusive, worldwide, irrevocable license to use, + reproduce, modify, license, sublicense (through multiple + tiers of sublicensees), and distribute (through multiple + tiers of distributors) it without the payment of any + royalties or fees to you. NVIDIA will use feedback at its + choice. NVIDIA is constantly looking for ways to improve + its products, so you may send feedback to NVIDIA through + the developer portal at https://developer.nvidia.com. + + +1.4. No Warranties + +THE SDK IS PROVIDED BY NVIDIA “AS IS” AND “WITH ALL +FAULTS.” TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND +ITS AFFILIATES EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND +OR NATURE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, +BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THE +ABSENCE OF ANY DEFECTS THEREIN, WHETHER LATENT OR PATENT. NO +WARRANTY IS MADE ON THE BASIS OF TRADE USAGE, COURSE OF +DEALING OR COURSE OF TRADE. + + +1.5. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, NVIDIA AND ITS +AFFILIATES SHALL NOT BE LIABLE FOR ANY SPECIAL, INCIDENTAL, +PUNITIVE OR CONSEQUENTIAL DAMAGES, OR ANY LOST PROFITS, LOSS +OF USE, LOSS OF DATA OR LOSS OF GOODWILL, OR THE COSTS OF +PROCURING SUBSTITUTE PRODUCTS, ARISING OUT OF OR IN CONNECTION +WITH THIS AGREEMENT OR THE USE OR PERFORMANCE OF THE SDK, +WHETHER SUCH LIABILITY ARISES FROM ANY CLAIM BASED UPON BREACH +OF CONTRACT, BREACH OF WARRANTY, TORT (INCLUDING NEGLIGENCE), +PRODUCT LIABILITY OR ANY OTHER CAUSE OF ACTION OR THEORY OF +LIABILITY. IN NO EVENT WILL NVIDIA’S AND ITS AFFILIATES +TOTAL CUMULATIVE LIABILITY UNDER OR ARISING OUT OF THIS +AGREEMENT EXCEED US$10.00. THE NATURE OF THE LIABILITY OR THE +NUMBER OF CLAIMS OR SUITS SHALL NOT ENLARGE OR EXTEND THIS +LIMIT. + +These exclusions and limitations of liability shall apply +regardless if NVIDIA or its affiliates have been advised of +the possibility of such damages, and regardless of whether a +remedy fails its essential purpose. These exclusions and +limitations of liability form an essential basis of the +bargain between the parties, and, absent any of these +exclusions or limitations of liability, the provisions of this +Agreement, including, without limitation, the economic terms, +would be substantially different. + + +1.6. Termination + + 1. This Agreement will continue to apply until terminated by + either you or NVIDIA as described below. + + 2. If you want to terminate this Agreement, you may do so by + stopping to use the SDK. + + 3. NVIDIA may, at any time, terminate this Agreement if: + + a. (i) you fail to comply with any term of this + Agreement and the non-compliance is not fixed within + thirty (30) days following notice from NVIDIA (or + immediately if you violate NVIDIA’s intellectual + property rights); + + b. (ii) you commence or participate in any legal + proceeding against NVIDIA with respect to the SDK; or + + c. (iii) NVIDIA decides to no longer provide the SDK in + a country or, in NVIDIA’s sole discretion, the + continued use of it is no longer commercially viable. + + 4. Upon any termination of this Agreement, you agree to + promptly discontinue use of the SDK and destroy all copies + in your possession or control. Your prior distributions in + accordance with this Agreement are not affected by the + termination of this Agreement. Upon written request, you + will certify in writing that you have complied with your + commitments under this section. Upon any termination of + this Agreement all provisions survive except for the + license grant provisions. + + +1.7. General + +If you wish to assign this Agreement or your rights and +obligations, including by merger, consolidation, dissolution +or operation of law, contact NVIDIA to ask for permission. Any +attempted assignment not approved by NVIDIA in writing shall +be void and of no effect. NVIDIA may assign, delegate or +transfer this Agreement and its rights and obligations, and if +to a non-affiliate you will be notified. + +You agree to cooperate with NVIDIA and provide reasonably +requested information to verify your compliance with this +Agreement. + +This Agreement will be governed in all respects by the laws of +the United States and of the State of Delaware as those laws +are applied to contracts entered into and performed entirely +within Delaware by Delaware residents, without regard to the +conflicts of laws principles. The United Nations Convention on +Contracts for the International Sale of Goods is specifically +disclaimed. You agree to all terms of this Agreement in the +English language. + +The state or federal courts residing in Santa Clara County, +California shall have exclusive jurisdiction over any dispute +or claim arising out of this Agreement. Notwithstanding this, +you agree that NVIDIA shall still be allowed to apply for +injunctive remedies or an equivalent type of urgent legal +relief in any jurisdiction. + +If any court of competent jurisdiction determines that any +provision of this Agreement is illegal, invalid or +unenforceable, such provision will be construed as limited to +the extent necessary to be consistent with and fully +enforceable under the law and the remaining provisions will +remain in full force and effect. Unless otherwise specified, +remedies are cumulative. + +Each party acknowledges and agrees that the other is an +independent contractor in the performance of this Agreement. + +The SDK has been developed entirely at private expense and is +“commercial items” consisting of “commercial computer +software” and “commercial computer software +documentation” provided with RESTRICTED RIGHTS. Use, +duplication or disclosure by the U.S. Government or a U.S. +Government subcontractor is subject to the restrictions in +this Agreement pursuant to DFARS 227.7202-3(a) or as set forth +in subparagraphs (c)(1) and (2) of the Commercial Computer +Software - Restricted Rights clause at FAR 52.227-19, as +applicable. Contractor/manufacturer is NVIDIA, 2788 San Tomas +Expressway, Santa Clara, CA 95051. + +The SDK is subject to United States export laws and +regulations. You agree that you will not ship, transfer or +export the SDK into any country, or use the SDK in any manner, +prohibited by the United States Bureau of Industry and +Security or economic sanctions regulations administered by the +U.S. Department of Treasury’s Office of Foreign Assets +Control (OFAC), or any applicable export laws, restrictions or +regulations. These laws include restrictions on destinations, +end users and end use. By accepting this Agreement, you +confirm that you are not a resident or citizen of any country +currently embargoed by the U.S. and that you are not otherwise +prohibited from receiving the SDK. + +Any notice delivered by NVIDIA to you under this Agreement +will be delivered via mail, email or fax. You agree that any +notices that NVIDIA sends you electronically will satisfy any +legal communication requirements. Please direct your legal +notices or other correspondence to NVIDIA Corporation, 2788 +San Tomas Expressway, Santa Clara, California 95051, United +States of America, Attention: Legal Department. + +This Agreement and any exhibits incorporated into this +Agreement constitute the entire agreement of the parties with +respect to the subject matter of this Agreement and supersede +all prior negotiations or documentation exchanged between the +parties relating to this SDK license. Any additional and/or +conflicting terms on documents issued by you are null, void, +and invalid. Any amendment or waiver under this Agreement +shall be in writing and signed by representatives of both +parties. + + +2. CUDA Toolkit Supplement to Software License Agreement for +NVIDIA Software Development Kits +------------------------------------------------------------ + + +Release date: August 16, 2018 +----------------------------- + +The terms in this supplement govern your use of the NVIDIA +CUDA Toolkit SDK under the terms of your license agreement +(“Agreement”) as modified by this supplement. Capitalized +terms used but not defined below have the meaning assigned to +them in the Agreement. + +This supplement is an exhibit to the Agreement and is +incorporated as an integral part of the Agreement. In the +event of conflict between the terms in this supplement and the +terms in the Agreement, the terms in this supplement govern. + + +2.1. License Scope + +The SDK is licensed for you to develop applications only for +use in systems with NVIDIA GPUs. + + +2.2. Distribution + +The portions of the SDK that are distributable under the +Agreement are listed in Attachment A. + + +2.3. Operating Systems + +Those portions of the SDK designed exclusively for use on the +Linux or FreeBSD operating systems, or other operating systems +derived from the source code to these operating systems, may +be copied and redistributed for use in accordance with this +Agreement, provided that the object code files are not +modified in any way (except for unzipping of compressed +files). + + +2.4. Audio and Video Encoders and Decoders + +You acknowledge and agree that it is your sole responsibility +to obtain any additional third-party licenses required to +make, have made, use, have used, sell, import, and offer for +sale your products or services that include or incorporate any +third-party software and content relating to audio and/or +video encoders and decoders from, including but not limited +to, Microsoft, Thomson, Fraunhofer IIS, Sisvel S.p.A., +MPEG-LA, and Coding Technologies. NVIDIA does not grant to you +under this Agreement any necessary patent or other rights with +respect to any audio and/or video encoders and decoders. + + +2.5. Licensing + +If the distribution terms in this Agreement are not suitable +for your organization, or for any questions regarding this +Agreement, please contact NVIDIA at +nvidia-compute-license-questions@nvidia.com. + + +2.6. Attachment A + +The following portions of the SDK are distributable under the +Agreement: + +Component + +CUDA Runtime + +Windows + +cudart.dll, cudart_static.lib, cudadevrt.lib + +Mac OSX + +libcudart.dylib, libcudart_static.a, libcudadevrt.a + +Linux + +libcudart.so, libcudart_static.a, libcudadevrt.a + +Android + +libcudart.so, libcudart_static.a, libcudadevrt.a + +Component + +CUDA FFT Library + +Windows + +cufft.dll, cufftw.dll, cufft.lib, cufftw.lib + +Mac OSX + +libcufft.dylib, libcufft_static.a, libcufftw.dylib, +libcufftw_static.a + +Linux + +libcufft.so, libcufft_static.a, libcufftw.so, +libcufftw_static.a + +Android + +libcufft.so, libcufft_static.a, libcufftw.so, +libcufftw_static.a + +Component + +CUDA BLAS Library + +Windows + +cublas.dll, cublasLt.dll + +Mac OSX + +libcublas.dylib, libcublasLt.dylib, libcublas_static.a, +libcublasLt_static.a + +Linux + +libcublas.so, libcublasLt.so, libcublas_static.a, +libcublasLt_static.a + +Android + +libcublas.so, libcublasLt.so, libcublas_static.a, +libcublasLt_static.a + +Component + +NVIDIA "Drop-in" BLAS Library + +Windows + +nvblas.dll + +Mac OSX + +libnvblas.dylib + +Linux + +libnvblas.so + +Component + +CUDA Sparse Matrix Library + +Windows + +cusparse.dll, cusparse.lib + +Mac OSX + +libcusparse.dylib, libcusparse_static.a + +Linux + +libcusparse.so, libcusparse_static.a + +Android + +libcusparse.so, libcusparse_static.a + +Component + +CUDA Linear Solver Library + +Windows + +cusolver.dll, cusolver.lib + +Mac OSX + +libcusolver.dylib, libcusolver_static.a + +Linux + +libcusolver.so, libcusolver_static.a + +Android + +libcusolver.so, libcusolver_static.a + +Component + +CUDA Random Number Generation Library + +Windows + +curand.dll, curand.lib + +Mac OSX + +libcurand.dylib, libcurand_static.a + +Linux + +libcurand.so, libcurand_static.a + +Android + +libcurand.so, libcurand_static.a + +Component + +CUDA Accelerated Graph Library + +Component + +NVIDIA Performance Primitives Library + +Windows + +nppc.dll, nppc.lib, nppial.dll, nppial.lib, nppicc.dll, +nppicc.lib, nppicom.dll, nppicom.lib, nppidei.dll, +nppidei.lib, nppif.dll, nppif.lib, nppig.dll, nppig.lib, +nppim.dll, nppim.lib, nppist.dll, nppist.lib, nppisu.dll, +nppisu.lib, nppitc.dll, nppitc.lib, npps.dll, npps.lib + +Mac OSX + +libnppc.dylib, libnppc_static.a, libnppial.dylib, +libnppial_static.a, libnppicc.dylib, libnppicc_static.a, +libnppicom.dylib, libnppicom_static.a, libnppidei.dylib, +libnppidei_static.a, libnppif.dylib, libnppif_static.a, +libnppig.dylib, libnppig_static.a, libnppim.dylib, +libnppisu_static.a, libnppitc.dylib, libnppitc_static.a, +libnpps.dylib, libnpps_static.a + +Linux + +libnppc.so, libnppc_static.a, libnppial.so, +libnppial_static.a, libnppicc.so, libnppicc_static.a, +libnppicom.so, libnppicom_static.a, libnppidei.so, +libnppidei_static.a, libnppif.so, libnppif_static.a +libnppig.so, libnppig_static.a, libnppim.so, +libnppim_static.a, libnppist.so, libnppist_static.a, +libnppisu.so, libnppisu_static.a, libnppitc.so +libnppitc_static.a, libnpps.so, libnpps_static.a + +Android + +libnppc.so, libnppc_static.a, libnppial.so, +libnppial_static.a, libnppicc.so, libnppicc_static.a, +libnppicom.so, libnppicom_static.a, libnppidei.so, +libnppidei_static.a, libnppif.so, libnppif_static.a +libnppig.so, libnppig_static.a, libnppim.so, +libnppim_static.a, libnppist.so, libnppist_static.a, +libnppisu.so, libnppisu_static.a, libnppitc.so +libnppitc_static.a, libnpps.so, libnpps_static.a + +Component + +NVIDIA JPEG Library + +Linux + +libnvjpeg.so, libnvjpeg_static.a + +Component + +Internal common library required for statically linking to +cuBLAS, cuSPARSE, cuFFT, cuRAND, nvJPEG and NPP + +Mac OSX + +libculibos.a + +Linux + +libculibos.a + +Component + +NVIDIA Runtime Compilation Library and Header + +All + +nvrtc.h + +Windows + +nvrtc.dll, nvrtc-builtins.dll + +Mac OSX + +libnvrtc.dylib, libnvrtc-builtins.dylib + +Linux + +libnvrtc.so, libnvrtc-builtins.so + +Component + +NVIDIA Optimizing Compiler Library + +Windows + +nvvm.dll + +Mac OSX + +libnvvm.dylib + +Linux + +libnvvm.so + +Component + +NVIDIA Common Device Math Functions Library + +Windows + +libdevice.10.bc + +Mac OSX + +libdevice.10.bc + +Linux + +libdevice.10.bc + +Component + +CUDA Occupancy Calculation Header Library + +All + +cuda_occupancy.h + +Component + +CUDA Half Precision Headers + +All + +cuda_fp16.h, cuda_fp16.hpp + +Component + +CUDA Profiling Tools Interface (CUPTI) Library + +Windows + +cupti.dll + +Mac OSX + +libcupti.dylib + +Linux + +libcupti.so + +Component + +NVIDIA Tools Extension Library + +Windows + +nvToolsExt.dll, nvToolsExt.lib + +Mac OSX + +libnvToolsExt.dylib + +Linux + +libnvToolsExt.so + +Component + +NVIDIA CUDA Driver Libraries + +Linux + +libcuda.so, libnvidia-fatbinaryloader.so, +libnvidia-ptxjitcompiler.so + +The NVIDIA CUDA Driver Libraries are only distributable in +applications that meet this criteria: + + 1. The application was developed starting from a NVIDIA CUDA + container obtained from Docker Hub or the NVIDIA GPU + Cloud, and + + 2. The resulting application is packaged as a Docker + container and distributed to users on Docker Hub or the + NVIDIA GPU Cloud only. + + +2.7. Attachment B + + +Additional Licensing Obligations + +The following third party components included in the SOFTWARE +are licensed to Licensee pursuant to the following terms and +conditions: + + 1. Licensee's use of the GDB third party component is + subject to the terms and conditions of GNU GPL v3: + + This product includes copyrighted third-party software licensed + under the terms of the GNU General Public License v3 ("GPL v3"). + All third-party software packages are copyright by their respective + authors. GPL v3 terms and conditions are hereby incorporated into + the Agreement by this reference: http://www.gnu.org/licenses/gpl.txt + + Consistent with these licensing requirements, the software + listed below is provided under the terms of the specified + open source software licenses. To obtain source code for + software provided under licenses that require + redistribution of source code, including the GNU General + Public License (GPL) and GNU Lesser General Public License + (LGPL), contact oss-requests@nvidia.com. This offer is + valid for a period of three (3) years from the date of the + distribution of this product by NVIDIA CORPORATION. + + Component License + CUDA-GDB GPL v3 + + 2. Licensee represents and warrants that any and all third + party licensing and/or royalty payment obligations in + connection with Licensee's use of the H.264 video codecs + are solely the responsibility of Licensee. + + 3. Licensee's use of the Thrust library is subject to the + terms and conditions of the Apache License Version 2.0. + All third-party software packages are copyright by their + respective authors. Apache License Version 2.0 terms and + conditions are hereby incorporated into the Agreement by + this reference. + http://www.apache.org/licenses/LICENSE-2.0.html + + In addition, Licensee acknowledges the following notice: + Thrust includes source code from the Boost Iterator, + Tuple, System, and Random Number libraries. + + Boost Software License - Version 1.0 - August 17th, 2003 + . . . . + + Permission is hereby granted, free of charge, to any person or + organization obtaining a copy of the software and accompanying + documentation covered by this license (the "Software") to use, + reproduce, display, distribute, execute, and transmit the Software, + and to prepare derivative works of the Software, and to permit + third-parties to whom the Software is furnished to do so, all + subject to the following: + + The copyright notices in the Software and this entire statement, + including the above license grant, this restriction and the following + disclaimer, must be included in all copies of the Software, in whole + or in part, and all derivative works of the Software, unless such + copies or derivative works are solely in the form of machine-executable + object code generated by a source language processor. + + 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, TITLE AND + NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR + OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + 4. Licensee's use of the LLVM third party component is + subject to the following terms and conditions: + + ====================================================== + LLVM Release License + ====================================================== + University of Illinois/NCSA + Open Source License + + Copyright (c) 2003-2010 University of Illinois at Urbana-Champaign. + All rights reserved. + + Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal with 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: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at Urbana- + Champaign, nor the names of its contributors may be used to endorse or + promote products derived from this Software without specific prior + written permission. + + 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 CONTRIBUTORS 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 WITH THE SOFTWARE. + + 5. Licensee's use (e.g. nvprof) of the PCRE third party + component is subject to the following terms and + conditions: + + ------------ + PCRE LICENCE + ------------ + PCRE is a library of functions to support regular expressions whose syntax + and semantics are as close as possible to those of the Perl 5 language. + Release 8 of PCRE is distributed under the terms of the "BSD" licence, as + specified below. The documentation for PCRE, supplied in the "doc" + directory, is distributed under the same terms as the software itself. The + basic library functions are written in C and are freestanding. Also + included in the distribution is a set of C++ wrapper functions, and a just- + in-time compiler that can be used to optimize pattern matching. These are + both optional features that can be omitted when the library is built. + + THE BASIC LIBRARY FUNCTIONS + --------------------------- + Written by: Philip Hazel + Email local part: ph10 + Email domain: cam.ac.uk + University of Cambridge Computing Service, + Cambridge, England. + Copyright (c) 1997-2012 University of Cambridge + All rights reserved. + + PCRE JUST-IN-TIME COMPILATION SUPPORT + ------------------------------------- + Written by: Zoltan Herczeg + Email local part: hzmester + Emain domain: freemail.hu + Copyright(c) 2010-2012 Zoltan Herczeg + All rights reserved. + + STACK-LESS JUST-IN-TIME COMPILER + -------------------------------- + Written by: Zoltan Herczeg + Email local part: hzmester + Emain domain: freemail.hu + Copyright(c) 2009-2012 Zoltan Herczeg + All rights reserved. + + THE C++ WRAPPER FUNCTIONS + ------------------------- + Contributed by: Google Inc. + Copyright (c) 2007-2012, Google Inc. + All rights reserved. + + THE "BSD" LICENCE + ----------------- + 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 University of Cambridge nor the name of Google + Inc. nor the names of their 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 OWNER 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. + + 6. Some of the cuBLAS library routines were written by or + derived from code written by Vasily Volkov and are subject + to the Modified Berkeley Software Distribution License as + follows: + + Copyright (c) 2007-2009, Regents of the University of California + + 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 University of California, Berkeley 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 AUTHOR "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 AUTHOR 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. + + 7. Some of the cuBLAS library routines were written by or + derived from code written by Davide Barbieri and are + subject to the Modified Berkeley Software Distribution + License as follows: + + Copyright (c) 2008-2009 Davide Barbieri @ University of Rome Tor Vergata. + + 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. + * The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR "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 AUTHOR 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. + + 8. Some of the cuBLAS library routines were derived from + code developed by the University of Tennessee and are + subject to the Modified Berkeley Software Distribution + License as follows: + + Copyright (c) 2010 The University of Tennessee. + + 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 listed in this license in the documentation and/or + other materials provided with the distribution. + * Neither the name of the copyright holders 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 + OWNER 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. + + 9. Some of the cuBLAS library routines were written by or + derived from code written by Jonathan Hogg and are subject + to the Modified Berkeley Software Distribution License as + follows: + + Copyright (c) 2012, The Science and Technology Facilities Council (STFC). + + 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 STFC 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 STFC 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. + + 10. Some of the cuBLAS library routines were written by or + derived from code written by Ahmad M. Abdelfattah, David + Keyes, and Hatem Ltaief, and are subject to the Apache + License, Version 2.0, as follows: + + -- (C) Copyright 2013 King Abdullah University of Science and Technology + Authors: + Ahmad Abdelfattah (ahmad.ahmad@kaust.edu.sa) + David Keyes (david.keyes@kaust.edu.sa) + Hatem Ltaief (hatem.ltaief@kaust.edu.sa) + + 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 King Abdullah University of Science and + Technology 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 + HOLDERS 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 + + 11. Some of the cuSPARSE library routines were written by or + derived from code written by Li-Wen Chang and are subject + to the NCSA Open Source License as follows: + + Copyright (c) 2012, University of Illinois. + + All rights reserved. + + Developed by: IMPACT Group, University of Illinois, http://impact.crhc.illinois.edu + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal with 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: + * 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 + disclaimers in the documentation and/or other materials provided + with the distribution. + * Neither the names of IMPACT Group, University of Illinois, nor + the names of its contributors may be used to endorse or promote + products derived from this Software without specific prior + written permission. + + 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 CONTRIBUTORS 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 WITH THE + SOFTWARE. + + 12. Some of the cuRAND library routines were written by or + derived from code written by Mutsuo Saito and Makoto + Matsumoto and are subject to the following license: + + Copyright (c) 2009, 2010 Mutsuo Saito, Makoto Matsumoto and Hiroshima + University. All rights reserved. + + Copyright (c) 2011 Mutsuo Saito, Makoto Matsumoto, Hiroshima + University and University of Tokyo. 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 Hiroshima University 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 + OWNER 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. + + 13. Some of the cuRAND library routines were derived from + code developed by D. E. Shaw Research and are subject to + the following license: + + Copyright 2010-2011, D. E. Shaw Research. + + 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 D. E. Shaw Research 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 + OWNER 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. + + 14. Some of the Math library routines were written by or + derived from code developed by Norbert Juffa and are + subject to the following license: + + Copyright (c) 2015-2017, Norbert Juffa + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. 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. + + 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. + + 15. Licensee's use of the lz4 third party component is + subject to the following terms and conditions: + + Copyright (C) 2011-2013, Yann Collet. + BSD 2-Clause License (http://www.opensource.org/licenses/bsd-license.php) + + 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. + + 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 + OWNER 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. + + 16. The NPP library uses code from the Boost Math Toolkit, + and is subject to the following license: + + Boost Software License - Version 1.0 - August 17th, 2003 + . . . . + + Permission is hereby granted, free of charge, to any person or + organization obtaining a copy of the software and accompanying + documentation covered by this license (the "Software") to use, + reproduce, display, distribute, execute, and transmit the Software, + and to prepare derivative works of the Software, and to permit + third-parties to whom the Software is furnished to do so, all + subject to the following: + + The copyright notices in the Software and this entire statement, + including the above license grant, this restriction and the following + disclaimer, must be included in all copies of the Software, in whole + or in part, and all derivative works of the Software, unless such + copies or derivative works are solely in the form of machine-executable + object code generated by a source language processor. + + 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, TITLE AND + NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR + OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + 17. Portions of the Nsight Eclipse Edition is subject to the + following license: + + The Eclipse Foundation makes available all content in this plug-in + ("Content"). Unless otherwise indicated below, the Content is provided + to you under the terms and conditions of the Eclipse Public License + Version 1.0 ("EPL"). A copy of the EPL is available at http:// + www.eclipse.org/legal/epl-v10.html. For purposes of the EPL, "Program" + will mean the Content. + + If you did not receive this Content directly from the Eclipse + Foundation, the Content is being redistributed by another party + ("Redistributor") and different terms and conditions may apply to your + use of any object code in the Content. Check the Redistributor's + license that was provided with the Content. If no such license exists, + contact the Redistributor. Unless otherwise indicated below, the terms + and conditions of the EPL still apply to any source code in the + Content and such source code may be obtained at http://www.eclipse.org. + + 18. Some of the cuBLAS library routines uses code from + OpenAI, which is subject to the following license: + + License URL + https://github.com/openai/openai-gemm/blob/master/LICENSE + + License Text + The MIT License + + Copyright (c) 2016 OpenAI (http://openai.com), 2016 Google Inc. + + 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. + + 19. Licensee's use of the Visual Studio Setup Configuration + Samples is subject to the following license: + + The MIT License (MIT) + Copyright (C) Microsoft Corporation. All rights reserved. + + 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. + + 20. Licensee's use of linmath.h header for CPU functions for + GL vector/matrix operations from lunarG is subject to the + Apache License Version 2.0. + + 21. The DX12-CUDA sample uses the d3dx12.h header, which is + subject to the MIT license . + +----------------- diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fa82c503576848157b9aa97e27c8f9de4ae21c3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2238396a73fb0d4a1f80cb3538c9451f77eabb21 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_common.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e717573adc75f4e0020b8a4549a4156ae672d5b4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psaix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a81977fe0d972628a885b4ee0e8d2bd7fc3e852 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psbsd.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pslinux.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pslinux.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba6655c3b2d1c2d12e9269162ae80612551815a0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pslinux.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5c8102fe734b4a14b7efd0decfa439518b795c2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psosx.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psposix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psposix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2859380448484fd0b4c152fc250077ec6ab1de8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_psposix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d218cb81141c46491c44b54e1654f8833836aa0c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pssunos.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pswindows.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pswindows.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5e054f7dc845bb77ada39c631a6cf7d81eac1a4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/__pycache__/_pswindows.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5d4b3abb7965010010f1810b6007bc912e1887bf --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__init__.py @@ -0,0 +1,2025 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Test utilities.""" + + +import atexit +import contextlib +import ctypes +import enum +import errno +import functools +import gc +import importlib +import ipaddress +import os +import platform +import random +import re +import select +import shlex +import shutil +import signal +import socket +import stat +import subprocess +import sys +import tempfile +import textwrap +import threading +import time +import unittest +import warnings +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_STREAM + + +try: + import pytest +except ImportError: + pytest = None + +import psutil +from psutil import AIX +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import bytes2human +from psutil._common import debug +from psutil._common import memoize +from psutil._common import print_color +from psutil._common import supports_ipv6 + + +if POSIX: + from psutil._psposix import wait_pid + + +# fmt: off +__all__ = [ + # constants + 'DEVNULL', 'GLOBAL_TIMEOUT', 'TOLERANCE_SYS_MEM', 'NO_RETRIES', + 'PYPY', 'PYTHON_EXE', 'PYTHON_EXE_ENV', 'ROOT_DIR', 'SCRIPTS_DIR', + 'TESTFN_PREFIX', 'UNICODE_SUFFIX', 'INVALID_UNICODE_SUFFIX', + 'CI_TESTING', 'VALID_PROC_STATUSES', 'TOLERANCE_DISK_USAGE', 'IS_64BIT', + "HAS_CPU_AFFINITY", "HAS_CPU_FREQ", "HAS_ENVIRON", "HAS_PROC_IO_COUNTERS", + "HAS_IONICE", "HAS_MEMORY_MAPS", "HAS_PROC_CPU_NUM", "HAS_RLIMIT", + "HAS_SENSORS_BATTERY", "HAS_BATTERY", "HAS_SENSORS_FANS", + "HAS_SENSORS_TEMPERATURES", "HAS_NET_CONNECTIONS_UNIX", "MACOS_11PLUS", + "MACOS_12PLUS", "COVERAGE", 'AARCH64', "PYTEST_PARALLEL", + # subprocesses + 'pyrun', 'terminate', 'reap_children', 'spawn_testproc', 'spawn_zombie', + 'spawn_children_pair', + # threads + 'ThreadTask', + # test utils + 'unittest', 'skip_on_access_denied', 'skip_on_not_implemented', + 'retry_on_failure', 'TestMemoryLeak', 'PsutilTestCase', + 'process_namespace', 'system_namespace', 'print_sysinfo', + 'is_win_secure_system_proc', 'fake_pytest', + # fs utils + 'chdir', 'safe_rmpath', 'create_py_exe', 'create_c_exe', 'get_testfn', + # os + 'get_winver', 'kernel_version', + # sync primitives + 'call_until', 'wait_for_pid', 'wait_for_file', + # network + 'check_net_address', 'filter_proc_net_connections', + 'get_free_port', 'bind_socket', 'bind_unix_socket', 'tcp_socketpair', + 'unix_socketpair', 'create_sockets', + # compat + 'reload_module', 'import_module_by_path', + # others + 'warn', 'copyload_shared_lib', 'is_namedtuple', +] +# fmt: on + + +# =================================================================== +# --- constants +# =================================================================== + +# --- platforms + +PYPY = '__pypy__' in sys.builtin_module_names +# whether we're running this test suite on a Continuous Integration service +GITHUB_ACTIONS = 'GITHUB_ACTIONS' in os.environ or 'CIBUILDWHEEL' in os.environ +CI_TESTING = GITHUB_ACTIONS +COVERAGE = 'COVERAGE_RUN' in os.environ +PYTEST_PARALLEL = "PYTEST_XDIST_WORKER" in os.environ # `make test-parallel` +# are we a 64 bit process? +IS_64BIT = sys.maxsize > 2**32 +AARCH64 = platform.machine() == "aarch64" + + +@memoize +def macos_version(): + version_str = platform.mac_ver()[0] + version = tuple(map(int, version_str.split(".")[:2])) + if version == (10, 16): + # When built against an older macOS SDK, Python will report + # macOS 10.16 instead of the real version. + version_str = subprocess.check_output( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + env={"SYSTEM_VERSION_COMPAT": "0"}, + universal_newlines=True, + ) + version = tuple(map(int, version_str.split(".")[:2])) + return version + + +if MACOS: + MACOS_11PLUS = macos_version() > (10, 15) + MACOS_12PLUS = macos_version() >= (12, 0) +else: + MACOS_11PLUS = False + MACOS_12PLUS = False + + +# --- configurable defaults + +# how many times retry_on_failure() decorator will retry +NO_RETRIES = 10 +# bytes tolerance for system-wide related tests +TOLERANCE_SYS_MEM = 5 * 1024 * 1024 # 5MB +TOLERANCE_DISK_USAGE = 10 * 1024 * 1024 # 10MB +# the timeout used in functions which have to wait +GLOBAL_TIMEOUT = 5 +# be more tolerant if we're on CI in order to avoid false positives +if CI_TESTING: + NO_RETRIES *= 3 + GLOBAL_TIMEOUT *= 3 + TOLERANCE_SYS_MEM *= 4 + TOLERANCE_DISK_USAGE *= 3 + +# --- file names + +# Disambiguate TESTFN for parallel testing. +if os.name == 'java': + # Jython disallows @ in module names + TESTFN_PREFIX = f"$psutil-{os.getpid()}-" +else: + TESTFN_PREFIX = f"@psutil-{os.getpid()}-" +UNICODE_SUFFIX = "-ƒőő" +# An invalid unicode string. +INVALID_UNICODE_SUFFIX = b"f\xc0\x80".decode('utf8', 'surrogateescape') +ASCII_FS = sys.getfilesystemencoding().lower() in {"ascii", "us-ascii"} + +# --- paths + +ROOT_DIR = os.path.realpath( + os.path.join(os.path.dirname(__file__), '..', '..') +) +SCRIPTS_DIR = os.environ.get( + "PSUTIL_SCRIPTS_DIR", os.path.join(ROOT_DIR, 'scripts') +) +HERE = os.path.realpath(os.path.dirname(__file__)) + +# --- support + +HAS_CPU_AFFINITY = hasattr(psutil.Process, "cpu_affinity") +HAS_CPU_FREQ = hasattr(psutil, "cpu_freq") +HAS_ENVIRON = hasattr(psutil.Process, "environ") +HAS_GETLOADAVG = hasattr(psutil, "getloadavg") +HAS_IONICE = hasattr(psutil.Process, "ionice") +HAS_MEMORY_MAPS = hasattr(psutil.Process, "memory_maps") +HAS_NET_CONNECTIONS_UNIX = POSIX and not SUNOS +HAS_NET_IO_COUNTERS = hasattr(psutil, "net_io_counters") +HAS_PROC_CPU_NUM = hasattr(psutil.Process, "cpu_num") +HAS_PROC_IO_COUNTERS = hasattr(psutil.Process, "io_counters") +HAS_RLIMIT = hasattr(psutil.Process, "rlimit") +HAS_SENSORS_BATTERY = hasattr(psutil, "sensors_battery") +try: + HAS_BATTERY = HAS_SENSORS_BATTERY and bool(psutil.sensors_battery()) +except Exception: # noqa: BLE001 + HAS_BATTERY = False +HAS_SENSORS_FANS = hasattr(psutil, "sensors_fans") +HAS_SENSORS_TEMPERATURES = hasattr(psutil, "sensors_temperatures") +HAS_THREADS = hasattr(psutil.Process, "threads") +SKIP_SYSCONS = (MACOS or AIX) and os.getuid() != 0 + +# --- misc + + +def _get_py_exe(): + def attempt(exe): + try: + subprocess.check_call( + [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError: + return None + else: + return exe + + env = os.environ.copy() + + # On Windows, starting with python 3.7, virtual environments use a + # venv launcher startup process. This does not play well when + # counting spawned processes, or when relying on the PID of the + # spawned process to do some checks, e.g. connections check per PID. + # Let's use the base python in this case. + base = getattr(sys, "_base_executable", None) + if WINDOWS and sys.version_info >= (3, 7) and base is not None: + # We need to set __PYVENV_LAUNCHER__ to sys.executable for the + # base python executable to know about the environment. + env["__PYVENV_LAUNCHER__"] = sys.executable + return base, env + elif GITHUB_ACTIONS: + return sys.executable, env + elif MACOS: + exe = ( + attempt(sys.executable) + or attempt(os.path.realpath(sys.executable)) + or attempt( + shutil.which("python{}.{}".format(*sys.version_info[:2])) + ) + or attempt(psutil.Process().exe()) + ) + if not exe: + raise ValueError("can't find python exe real abspath") + return exe, env + else: + exe = os.path.realpath(sys.executable) + assert os.path.exists(exe), exe + return exe, env + + +PYTHON_EXE, PYTHON_EXE_ENV = _get_py_exe() +DEVNULL = open(os.devnull, 'r+') # noqa: SIM115 +atexit.register(DEVNULL.close) + +VALID_PROC_STATUSES = [ + getattr(psutil, x) for x in dir(psutil) if x.startswith('STATUS_') +] +AF_UNIX = getattr(socket, "AF_UNIX", object()) + +_subprocesses_started = set() +_pids_started = set() + + +# =================================================================== +# --- threads +# =================================================================== + + +class ThreadTask(threading.Thread): + """A thread task which does nothing expect staying alive.""" + + def __init__(self): + super().__init__() + self._running = False + self._interval = 0.001 + self._flag = threading.Event() + + def __repr__(self): + name = self.__class__.__name__ + return f"<{name} running={self._running} at {id(self):#x}>" + + def __enter__(self): + self.start() + return self + + def __exit__(self, *args, **kwargs): + self.stop() + + def start(self): + """Start thread and keep it running until an explicit + stop() request. Polls for shutdown every 'timeout' seconds. + """ + if self._running: + raise ValueError("already started") + threading.Thread.start(self) + self._flag.wait() + + def run(self): + self._running = True + self._flag.set() + while self._running: + time.sleep(self._interval) + + def stop(self): + """Stop thread execution and and waits until it is stopped.""" + if not self._running: + raise ValueError("already stopped") + self._running = False + self.join() + + +# =================================================================== +# --- subprocesses +# =================================================================== + + +def _reap_children_on_err(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except Exception: + reap_children() + raise + + return wrapper + + +@_reap_children_on_err +def spawn_testproc(cmd=None, **kwds): + """Create a python subprocess which does nothing for some secs and + return it as a subprocess.Popen instance. + If "cmd" is specified that is used instead of python. + By default stdin and stdout are redirected to /dev/null. + It also attempts to make sure the process is in a reasonably + initialized state. + The process is registered for cleanup on reap_children(). + """ + kwds.setdefault("stdin", DEVNULL) + kwds.setdefault("stdout", DEVNULL) + kwds.setdefault("cwd", os.getcwd()) + kwds.setdefault("env", PYTHON_EXE_ENV) + if WINDOWS: + # Prevents the subprocess to open error dialogs. This will also + # cause stderr to be suppressed, which is suboptimal in order + # to debug broken tests. + CREATE_NO_WINDOW = 0x8000000 + kwds.setdefault("creationflags", CREATE_NO_WINDOW) + if cmd is None: + testfn = get_testfn(dir=os.getcwd()) + try: + safe_rmpath(testfn) + pyline = ( + "import time;" + f"open(r'{testfn}', 'w').close();" + "[time.sleep(0.1) for x in range(100)];" # 10 secs + ) + cmd = [PYTHON_EXE, "-c", pyline] + sproc = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(sproc) + wait_for_file(testfn, delete=True, empty=True) + finally: + safe_rmpath(testfn) + else: + sproc = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(sproc) + wait_for_pid(sproc.pid) + return sproc + + +@_reap_children_on_err +def spawn_children_pair(): + """Create a subprocess which creates another one as in: + A (us) -> B (child) -> C (grandchild). + Return a (child, grandchild) tuple. + The 2 processes are fully initialized and will live for 60 secs + and are registered for cleanup on reap_children(). + """ + tfile = None + testfn = get_testfn(dir=os.getcwd()) + try: + s = textwrap.dedent(f"""\ + import subprocess, os, sys, time + s = "import os, time;" + s += "f = open('{os.path.basename(testfn)}', 'w');" + s += "f.write(str(os.getpid()));" + s += "f.close();" + s += "[time.sleep(0.1) for x in range(100 * 6)];" + p = subprocess.Popen([r'{PYTHON_EXE}', '-c', s]) + p.wait() + """) + # On Windows if we create a subprocess with CREATE_NO_WINDOW flag + # set (which is the default) a "conhost.exe" extra process will be + # spawned as a child. We don't want that. + if WINDOWS: + subp, tfile = pyrun(s, creationflags=0) + else: + subp, tfile = pyrun(s) + child = psutil.Process(subp.pid) + grandchild_pid = int(wait_for_file(testfn, delete=True, empty=False)) + _pids_started.add(grandchild_pid) + grandchild = psutil.Process(grandchild_pid) + return (child, grandchild) + finally: + safe_rmpath(testfn) + if tfile is not None: + safe_rmpath(tfile) + + +def spawn_zombie(): + """Create a zombie process and return a (parent, zombie) process tuple. + In order to kill the zombie parent must be terminate()d first, then + zombie must be wait()ed on. + """ + assert psutil.POSIX + unix_file = get_testfn() + src = textwrap.dedent(f"""\ + import os, sys, time, socket, contextlib + child_pid = os.fork() + if child_pid > 0: + time.sleep(3000) + else: + # this is the zombie process + with socket.socket(socket.AF_UNIX) as s: + s.connect('{unix_file}') + pid = bytes(str(os.getpid()), 'ascii') + s.sendall(pid) + """) + tfile = None + sock = bind_unix_socket(unix_file) + try: + sock.settimeout(GLOBAL_TIMEOUT) + parent, tfile = pyrun(src) + conn, _ = sock.accept() + try: + select.select([conn.fileno()], [], [], GLOBAL_TIMEOUT) + zpid = int(conn.recv(1024)) + _pids_started.add(zpid) + zombie = psutil.Process(zpid) + call_until(lambda: zombie.status() == psutil.STATUS_ZOMBIE) + return (parent, zombie) + finally: + conn.close() + finally: + sock.close() + safe_rmpath(unix_file) + if tfile is not None: + safe_rmpath(tfile) + + +@_reap_children_on_err +def pyrun(src, **kwds): + """Run python 'src' code string in a separate interpreter. + Returns a subprocess.Popen instance and the test file where the source + code was written. + """ + kwds.setdefault("stdout", None) + kwds.setdefault("stderr", None) + srcfile = get_testfn() + try: + with open(srcfile, "w") as f: + f.write(src) + subp = spawn_testproc([PYTHON_EXE, f.name], **kwds) + wait_for_pid(subp.pid) + return (subp, srcfile) + except Exception: + safe_rmpath(srcfile) + raise + + +@_reap_children_on_err +def sh(cmd, **kwds): + """Run cmd in a subprocess and return its output. + raises RuntimeError on error. + """ + # Prevents subprocess to open error dialogs in case of error. + flags = 0x8000000 if WINDOWS else 0 + kwds.setdefault("stdout", subprocess.PIPE) + kwds.setdefault("stderr", subprocess.PIPE) + kwds.setdefault("universal_newlines", True) + kwds.setdefault("creationflags", flags) + if isinstance(cmd, str): + cmd = shlex.split(cmd) + p = subprocess.Popen(cmd, **kwds) + _subprocesses_started.add(p) + stdout, stderr = p.communicate(timeout=GLOBAL_TIMEOUT) + if p.returncode != 0: + raise RuntimeError(stdout + stderr) + if stderr: + warn(stderr) + if stdout.endswith('\n'): + stdout = stdout[:-1] + return stdout + + +def terminate(proc_or_pid, sig=signal.SIGTERM, wait_timeout=GLOBAL_TIMEOUT): + """Terminate a process and wait() for it. + Process can be a PID or an instance of psutil.Process(), + subprocess.Popen() or psutil.Popen(). + If it's a subprocess.Popen() or psutil.Popen() instance also closes + its stdin / stdout / stderr fds. + PID is wait()ed even if the process is already gone (kills zombies). + Does nothing if the process does not exist. + Return process exit status. + """ + + def wait(proc, timeout): + proc.wait(timeout) + if WINDOWS and isinstance(proc, subprocess.Popen): + # Otherwise PID may still hang around. + try: + return psutil.Process(proc.pid).wait(timeout) + except psutil.NoSuchProcess: + pass + + def sendsig(proc, sig): + # XXX: otherwise the build hangs for some reason. + if MACOS and GITHUB_ACTIONS: + sig = signal.SIGKILL + # If the process received SIGSTOP, SIGCONT is necessary first, + # otherwise SIGTERM won't work. + if POSIX and sig != signal.SIGKILL: + proc.send_signal(signal.SIGCONT) + proc.send_signal(sig) + + def term_subprocess_proc(proc, timeout): + try: + sendsig(proc, sig) + except ProcessLookupError: + pass + except OSError as err: + if WINDOWS and err.winerror == 6: # "invalid handle" + pass + raise + return wait(proc, timeout) + + def term_psutil_proc(proc, timeout): + try: + sendsig(proc, sig) + except psutil.NoSuchProcess: + pass + return wait(proc, timeout) + + def term_pid(pid, timeout): + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + # Needed to kill zombies. + if POSIX: + return wait_pid(pid, timeout) + else: + return term_psutil_proc(proc, timeout) + + def flush_popen(proc): + if proc.stdout: + proc.stdout.close() + if proc.stderr: + proc.stderr.close() + # Flushing a BufferedWriter may raise an error. + if proc.stdin: + proc.stdin.close() + + p = proc_or_pid + try: + if isinstance(p, int): + return term_pid(p, wait_timeout) + elif isinstance(p, (psutil.Process, psutil.Popen)): + return term_psutil_proc(p, wait_timeout) + elif isinstance(p, subprocess.Popen): + return term_subprocess_proc(p, wait_timeout) + else: + raise TypeError(f"wrong type {p!r}") + finally: + if isinstance(p, (subprocess.Popen, psutil.Popen)): + flush_popen(p) + pid = p if isinstance(p, int) else p.pid + assert not psutil.pid_exists(pid), pid + + +def reap_children(recursive=False): + """Terminate and wait() any subprocess started by this test suite + and any children currently running, ensuring that no processes stick + around to hog resources. + If recursive is True it also tries to terminate and wait() + all grandchildren started by this process. + """ + # Get the children here before terminating them, as in case of + # recursive=True we don't want to lose the intermediate reference + # pointing to the grandchildren. + children = psutil.Process().children(recursive=recursive) + + # Terminate subprocess.Popen. + while _subprocesses_started: + subp = _subprocesses_started.pop() + terminate(subp) + + # Collect started pids. + while _pids_started: + pid = _pids_started.pop() + terminate(pid) + + # Terminate children. + if children: + for p in children: + terminate(p, wait_timeout=None) + _, alive = psutil.wait_procs(children, timeout=GLOBAL_TIMEOUT) + for p in alive: + warn(f"couldn't terminate process {p!r}; attempting kill()") + terminate(p, sig=signal.SIGKILL) + + +# =================================================================== +# --- OS +# =================================================================== + + +def kernel_version(): + """Return a tuple such as (2, 6, 36).""" + if not POSIX: + raise NotImplementedError("not POSIX") + s = "" + uname = os.uname()[2] + for c in uname: + if c.isdigit() or c == '.': + s += c + else: + break + if not s: + raise ValueError(f"can't parse {uname!r}") + minor = 0 + micro = 0 + nums = s.split('.') + major = int(nums[0]) + if len(nums) >= 2: + minor = int(nums[1]) + if len(nums) >= 3: + micro = int(nums[2]) + return (major, minor, micro) + + +def get_winver(): + if not WINDOWS: + raise NotImplementedError("not WINDOWS") + wv = sys.getwindowsversion() + sp = wv.service_pack_major or 0 + return (wv[0], wv[1], sp) + + +# =================================================================== +# --- sync primitives +# =================================================================== + + +class retry: + """A retry decorator.""" + + def __init__( + self, + exception=Exception, + timeout=None, + retries=None, + interval=0.001, + logfun=None, + ): + if timeout and retries: + raise ValueError("timeout and retries args are mutually exclusive") + self.exception = exception + self.timeout = timeout + self.retries = retries + self.interval = interval + self.logfun = logfun + + def __iter__(self): + if self.timeout: + stop_at = time.time() + self.timeout + while time.time() < stop_at: + yield + elif self.retries: + for _ in range(self.retries): + yield + else: + while True: + yield + + def sleep(self): + if self.interval is not None: + time.sleep(self.interval) + + def __call__(self, fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + exc = None + for _ in self: + try: + return fun(*args, **kwargs) + except self.exception as _: + exc = _ + if self.logfun is not None: + self.logfun(exc) + self.sleep() + continue + + raise exc + + # This way the user of the decorated function can change config + # parameters. + wrapper.decorator = self + return wrapper + + +@retry( + exception=psutil.NoSuchProcess, + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_pid(pid): + """Wait for pid to show up in the process list then return. + Used in the test suite to give time the sub process to initialize. + """ + if pid not in psutil.pids(): + raise psutil.NoSuchProcess(pid) + psutil.Process(pid) + + +@retry( + exception=(FileNotFoundError, AssertionError), + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def wait_for_file(fname, delete=True, empty=False): + """Wait for a file to be written on disk with some content.""" + with open(fname, "rb") as f: + data = f.read() + if not empty: + assert data + if delete: + safe_rmpath(fname) + return data + + +@retry( + exception=AssertionError, + logfun=None, + timeout=GLOBAL_TIMEOUT, + interval=0.001, +) +def call_until(fun): + """Keep calling function until it evaluates to True.""" + ret = fun() + assert ret + return ret + + +# =================================================================== +# --- fs +# =================================================================== + + +def safe_rmpath(path): + """Convenience function for removing temporary test files or dirs.""" + + def retry_fun(fun): + # On Windows it could happen that the file or directory has + # open handles or references preventing the delete operation + # to succeed immediately, so we retry for a while. See: + # https://bugs.python.org/issue33240 + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + return fun() + except FileNotFoundError: + pass + except OSError as _: + err = _ + warn(f"ignoring {err}") + time.sleep(0.01) + raise err + + try: + st = os.stat(path) + if stat.S_ISDIR(st.st_mode): + fun = functools.partial(shutil.rmtree, path) + else: + fun = functools.partial(os.remove, path) + if POSIX: + fun() + else: + retry_fun(fun) + except FileNotFoundError: + pass + + +def safe_mkdir(dir): + """Convenience function for creating a directory.""" + try: + os.mkdir(dir) + except FileExistsError: + pass + + +@contextlib.contextmanager +def chdir(dirname): + """Context manager which temporarily changes the current directory.""" + curdir = os.getcwd() + try: + os.chdir(dirname) + yield + finally: + os.chdir(curdir) + + +def create_py_exe(path): + """Create a Python executable file in the given location.""" + assert not os.path.exists(path), path + atexit.register(safe_rmpath, path) + shutil.copyfile(PYTHON_EXE, path) + if POSIX: + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + return path + + +def create_c_exe(path, c_code=None): + """Create a compiled C executable in the given location.""" + assert not os.path.exists(path), path + if not shutil.which("gcc"): + raise pytest.skip("gcc is not installed") + if c_code is None: + c_code = textwrap.dedent(""" + #include + int main() { + pause(); + return 1; + } + """) + else: + assert isinstance(c_code, str), c_code + + atexit.register(safe_rmpath, path) + with open(get_testfn(suffix='.c'), "w") as f: + f.write(c_code) + try: + subprocess.check_call(["gcc", f.name, "-o", path]) + finally: + safe_rmpath(f.name) + return path + + +def get_testfn(suffix="", dir=None): + """Return an absolute pathname of a file or dir that did not + exist at the time this call is made. Also schedule it for safe + deletion at interpreter exit. It's technically racy but probably + not really due to the time variant. + """ + while True: + name = tempfile.mktemp(prefix=TESTFN_PREFIX, suffix=suffix, dir=dir) + if not os.path.exists(name): # also include dirs + path = os.path.realpath(name) # needed for OSX + atexit.register(safe_rmpath, path) + return path + + +# =================================================================== +# --- testing +# =================================================================== + + +class fake_pytest: + """A class that mimics some basic pytest APIs. This is meant for + when unit tests are run in production, where pytest may not be + installed. Still, the user can test psutil installation via: + + $ python3 -m psutil.tests + """ + + @staticmethod + def main(*args, **kw): # noqa: ARG004 + """Mimics pytest.main(). It has the same effect as running + `python3 -m unittest -v` from the project root directory. + """ + suite = unittest.TestLoader().discover(HERE) + unittest.TextTestRunner(verbosity=2).run(suite) + warnings.warn( + "Fake pytest module was used. Test results may be inaccurate.", + UserWarning, + stacklevel=1, + ) + return suite + + @staticmethod + def raises(exc, match=None): + """Mimics `pytest.raises`.""" + + class ExceptionInfo: + _exc = None + + @property + def value(self): + return self._exc + + @contextlib.contextmanager + def context(exc, match=None): + einfo = ExceptionInfo() + try: + yield einfo + except exc as err: + if match and not re.search(match, str(err)): + msg = f'"{match}" does not match "{err}"' + raise AssertionError(msg) + einfo._exc = err + else: + raise AssertionError(f"{exc!r} not raised") + + return context(exc, match=match) + + @staticmethod + def warns(warning, match=None): + """Mimics `pytest.warns`.""" + if match: + return unittest.TestCase().assertWarnsRegex(warning, match) + return unittest.TestCase().assertWarns(warning) + + @staticmethod + def skip(reason=""): + """Mimics `unittest.SkipTest`.""" + raise unittest.SkipTest(reason) + + class mark: + + @staticmethod + def skipif(condition, reason=""): + """Mimics `@pytest.mark.skipif` decorator.""" + return unittest.skipIf(condition, reason) + + class xdist_group: + """Mimics `@pytest.mark.xdist_group` decorator (no-op).""" + + def __init__(self, name=None): + pass + + def __call__(self, cls_or_meth): + return cls_or_meth + + +if pytest is None: + pytest = fake_pytest + + +class PsutilTestCase(unittest.TestCase): + """Test class providing auto-cleanup wrappers on top of process + test utilities. All test classes should derive from this one, even + if we use pytest. + """ + + def get_testfn(self, suffix="", dir=None): + fname = get_testfn(suffix=suffix, dir=dir) + self.addCleanup(safe_rmpath, fname) + return fname + + def spawn_testproc(self, *args, **kwds): + sproc = spawn_testproc(*args, **kwds) + self.addCleanup(terminate, sproc) + return sproc + + def spawn_children_pair(self): + child1, child2 = spawn_children_pair() + self.addCleanup(terminate, child2) + self.addCleanup(terminate, child1) # executed first + return (child1, child2) + + def spawn_zombie(self): + parent, zombie = spawn_zombie() + self.addCleanup(terminate, zombie) + self.addCleanup(terminate, parent) # executed first + return (parent, zombie) + + def pyrun(self, *args, **kwds): + sproc, srcfile = pyrun(*args, **kwds) + self.addCleanup(safe_rmpath, srcfile) + self.addCleanup(terminate, sproc) # executed first + return sproc + + def _check_proc_exc(self, proc, exc): + assert isinstance(exc, psutil.Error) + assert exc.pid == proc.pid + assert exc.name == proc._name + if exc.name: + assert exc.name + if isinstance(exc, psutil.ZombieProcess): + assert exc.ppid == proc._ppid + if exc.ppid is not None: + assert exc.ppid >= 0 + str(exc) + repr(exc) + + def assertPidGone(self, pid): + with pytest.raises(psutil.NoSuchProcess) as cm: + try: + psutil.Process(pid) + except psutil.ZombieProcess: + raise AssertionError("wasn't supposed to raise ZombieProcess") + assert cm.value.pid == pid + assert cm.value.name is None + assert not psutil.pid_exists(pid), pid + assert pid not in psutil.pids() + assert pid not in [x.pid for x in psutil.process_iter()] + + def assertProcessGone(self, proc): + self.assertPidGone(proc.pid) + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + ret = fun() + except psutil.ZombieProcess: + raise + except psutil.NoSuchProcess as exc: + self._check_proc_exc(proc, exc) + else: + msg = ( + f"Process.{name}() didn't raise NSP and returned" + f" {ret!r}" + ) + raise AssertionError(msg) + proc.wait(timeout=0) # assert not raise TimeoutExpired + + def assertProcessZombie(self, proc): + # A zombie process should always be instantiable. + clone = psutil.Process(proc.pid) + # Cloned zombie on Open/NetBSD has null creation time, see: + # https://github.com/giampaolo/psutil/issues/2287 + assert proc == clone + if not (OPENBSD or NETBSD): + assert hash(proc) == hash(clone) + # Its status always be querable. + assert proc.status() == psutil.STATUS_ZOMBIE + # It should be considered 'running'. + assert proc.is_running() + assert psutil.pid_exists(proc.pid) + # as_dict() shouldn't crash. + proc.as_dict() + # It should show up in pids() and process_iter(). + assert proc.pid in psutil.pids() + assert proc.pid in [x.pid for x in psutil.process_iter()] + psutil._pmap = {} + assert proc.pid in [x.pid for x in psutil.process_iter()] + # Call all methods. + ns = process_namespace(proc) + for fun, name in ns.iter(ns.all, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + fun() + except (psutil.ZombieProcess, psutil.AccessDenied) as exc: + self._check_proc_exc(proc, exc) + if LINUX: + # https://github.com/giampaolo/psutil/pull/2288 + with pytest.raises(psutil.ZombieProcess) as cm: + proc.cmdline() + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: + proc.exe() + self._check_proc_exc(proc, cm.value) + with pytest.raises(psutil.ZombieProcess) as cm: + proc.memory_maps() + self._check_proc_exc(proc, cm.value) + # Zombie cannot be signaled or terminated. + proc.suspend() + proc.resume() + proc.terminate() + proc.kill() + assert proc.is_running() + assert psutil.pid_exists(proc.pid) + assert proc.pid in psutil.pids() + assert proc.pid in [x.pid for x in psutil.process_iter()] + psutil._pmap = {} + assert proc.pid in [x.pid for x in psutil.process_iter()] + + # Its parent should 'see' it (edit: not true on BSD and MACOS). + # descendants = [x.pid for x in psutil.Process().children( + # recursive=True)] + # self.assertIn(proc.pid, descendants) + + # __eq__ can't be relied upon because creation time may not be + # querable. + # self.assertEqual(proc, psutil.Process(proc.pid)) + + # XXX should we also assume ppid() to be usable? Note: this + # would be an important use case as the only way to get + # rid of a zombie is to kill its parent. + # self.assertEqual(proc.ppid(), os.getpid()) + + +@pytest.mark.skipif(PYPY, reason="unreliable on PYPY") +class TestMemoryLeak(PsutilTestCase): + """Test framework class for detecting function memory leaks, + typically functions implemented in C which forgot to free() memory + from the heap. It does so by checking whether the process memory + usage increased before and after calling the function many times. + + Note that this is hard (probably impossible) to do reliably, due + to how the OS handles memory, the GC and so on (memory can even + decrease!). In order to avoid false positives, in case of failure + (mem > 0) we retry the test for up to 5 times, increasing call + repetitions each time. If the memory keeps increasing then it's a + failure. + + If available (Linux, OSX, Windows), USS memory is used for comparison, + since it's supposed to be more precise, see: + https://gmpy.dev/blog/2016/real-process-memory-and-environ-in-python + If not, RSS memory is used. mallinfo() on Linux and _heapwalk() on + Windows may give even more precision, but at the moment are not + implemented. + + PyPy appears to be completely unstable for this framework, probably + because of its JIT, so tests on PYPY are skipped. + + Usage: + + class TestLeaks(psutil.tests.TestMemoryLeak): + + def test_fun(self): + self.execute(some_function) + """ + + # Configurable class attrs. + times = 200 + warmup_times = 10 + tolerance = 0 # memory + retries = 10 if CI_TESTING else 5 + verbose = True + _thisproc = psutil.Process() + _psutil_debug_orig = bool(os.getenv('PSUTIL_DEBUG')) + + @classmethod + def setUpClass(cls): + psutil._set_debug(False) # avoid spamming to stderr + + @classmethod + def tearDownClass(cls): + psutil._set_debug(cls._psutil_debug_orig) + + def _get_mem(self): + # USS is the closest thing we have to "real" memory usage and it + # should be less likely to produce false positives. + mem = self._thisproc.memory_full_info() + return getattr(mem, "uss", mem.rss) + + def _get_num_fds(self): + if POSIX: + return self._thisproc.num_fds() + else: + return self._thisproc.num_handles() + + def _log(self, msg): + if self.verbose: + print_color(msg, color="yellow", file=sys.stderr) + + def _check_fds(self, fun): + """Makes sure num_fds() (POSIX) or num_handles() (Windows) does + not increase after calling a function. Used to discover forgotten + close(2) and CloseHandle syscalls. + """ + before = self._get_num_fds() + self.call(fun) + after = self._get_num_fds() + diff = after - before + if diff < 0: + msg = ( + f"negative diff {diff!r} (gc probably collected a" + " resource from a previous test)" + ) + raise self.fail(msg) + if diff > 0: + type_ = "fd" if POSIX else "handle" + if diff > 1: + type_ += "s" + msg = f"{diff} unclosed {type_} after calling {fun!r}" + raise self.fail(msg) + + def _call_ntimes(self, fun, times): + """Get 2 distinct memory samples, before and after having + called fun repeatedly, and return the memory difference. + """ + gc.collect(generation=1) + mem1 = self._get_mem() + for x in range(times): + ret = self.call(fun) + del x, ret + gc.collect(generation=1) + mem2 = self._get_mem() + assert gc.garbage == [] + diff = mem2 - mem1 # can also be negative + return diff + + def _check_mem(self, fun, times, retries, tolerance): + messages = [] + prev_mem = 0 + increase = times + for idx in range(1, retries + 1): + mem = self._call_ntimes(fun, times) + msg = "Run #{}: extra-mem={}, per-call={}, calls={}".format( + idx, + bytes2human(mem), + bytes2human(mem / times), + times, + ) + messages.append(msg) + success = mem <= tolerance or mem <= prev_mem + if success: + if idx > 1: + self._log(msg) + return + else: + if idx == 1: + print() # noqa: T201 + self._log(msg) + times += increase + prev_mem = mem + raise self.fail(". ".join(messages)) + + # --- + + def call(self, fun): + return fun() + + def execute( + self, fun, times=None, warmup_times=None, retries=None, tolerance=None + ): + """Test a callable.""" + times = times if times is not None else self.times + warmup_times = ( + warmup_times if warmup_times is not None else self.warmup_times + ) + retries = retries if retries is not None else self.retries + tolerance = tolerance if tolerance is not None else self.tolerance + try: + assert times >= 1, "times must be >= 1" + assert warmup_times >= 0, "warmup_times must be >= 0" + assert retries >= 0, "retries must be >= 0" + assert tolerance >= 0, "tolerance must be >= 0" + except AssertionError as err: + raise ValueError(str(err)) + + self._call_ntimes(fun, warmup_times) # warm up + self._check_fds(fun) + self._check_mem(fun, times=times, retries=retries, tolerance=tolerance) + + def execute_w_exc(self, exc, fun, **kwargs): + """Convenience method to test a callable while making sure it + raises an exception on every call. + """ + + def call(): + self.assertRaises(exc, fun) + + self.execute(call, **kwargs) + + +def print_sysinfo(): + import collections + import datetime + import getpass + import locale + import pprint + + try: + import pip + except ImportError: + pip = None + try: + import wheel + except ImportError: + wheel = None + + info = collections.OrderedDict() + + # OS + if psutil.LINUX and shutil.which("lsb_release"): + info['OS'] = sh('lsb_release -d -s') + elif psutil.OSX: + info['OS'] = f"Darwin {platform.mac_ver()[0]}" + elif psutil.WINDOWS: + info['OS'] = "Windows " + ' '.join(map(str, platform.win32_ver())) + if hasattr(platform, 'win32_edition'): + info['OS'] += ", " + platform.win32_edition() + else: + info['OS'] = f"{platform.system()} {platform.version()}" + info['arch'] = ', '.join( + list(platform.architecture()) + [platform.machine()] + ) + if psutil.POSIX: + info['kernel'] = platform.uname()[2] + + # python + info['python'] = ', '.join([ + platform.python_implementation(), + platform.python_version(), + platform.python_compiler(), + ]) + info['pip'] = getattr(pip, '__version__', 'not installed') + if wheel is not None: + info['pip'] += f" (wheel={wheel.__version__})" + + # UNIX + if psutil.POSIX: + if shutil.which("gcc"): + out = sh(['gcc', '--version']) + info['gcc'] = str(out).split('\n')[0] + else: + info['gcc'] = 'not installed' + s = platform.libc_ver()[1] + if s: + info['glibc'] = s + + # system + info['fs-encoding'] = sys.getfilesystemencoding() + lang = locale.getlocale() + info['lang'] = f"{lang[0]}, {lang[1]}" + info['boot-time'] = datetime.datetime.fromtimestamp( + psutil.boot_time() + ).strftime("%Y-%m-%d %H:%M:%S") + info['time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + info['user'] = getpass.getuser() + info['home'] = os.path.expanduser("~") + info['cwd'] = os.getcwd() + info['pyexe'] = PYTHON_EXE + info['hostname'] = platform.node() + info['PID'] = os.getpid() + + # metrics + info['cpus'] = psutil.cpu_count() + info['loadavg'] = "{:.1f}%, {:.1f}%, {:.1f}%".format( + *tuple(x / psutil.cpu_count() * 100 for x in psutil.getloadavg()) + ) + mem = psutil.virtual_memory() + info['memory'] = "{}%%, used={}, total={}".format( + int(mem.percent), + bytes2human(mem.used), + bytes2human(mem.total), + ) + swap = psutil.swap_memory() + info['swap'] = "{}%%, used={}, total={}".format( + int(swap.percent), + bytes2human(swap.used), + bytes2human(swap.total), + ) + info['pids'] = len(psutil.pids()) + pinfo = psutil.Process().as_dict() + pinfo.pop('memory_maps', None) + info['proc'] = pprint.pformat(pinfo) + + print("=" * 70, file=sys.stderr) # noqa: T201 + for k, v in info.items(): + print("{:<17} {}".format(k + ":", v), file=sys.stderr) # noqa: T201 + print("=" * 70, file=sys.stderr) # noqa: T201 + sys.stdout.flush() + + # if WINDOWS: + # os.system("tasklist") + # elif shutil.which("ps"): + # os.system("ps aux") + # print("=" * 70, file=sys.stderr) + + sys.stdout.flush() + + +def is_win_secure_system_proc(pid): + # see: https://github.com/giampaolo/psutil/issues/2338 + @memoize + def get_procs(): + ret = {} + out = sh("tasklist.exe /NH /FO csv") + for line in out.splitlines()[1:]: + bits = [x.replace('"', "") for x in line.split(",")] + name, pid = bits[0], int(bits[1]) + ret[pid] = name + return ret + + try: + return get_procs()[pid] == "Secure System" + except KeyError: + return False + + +def _get_eligible_cpu(): + p = psutil.Process() + if hasattr(p, "cpu_num"): + return p.cpu_num() + elif hasattr(p, "cpu_affinity"): + return random.choice(p.cpu_affinity()) + return 0 + + +class process_namespace: + """A container that lists all Process class method names + some + reasonable parameters to be called with. Utility methods (parent(), + children(), ...) are excluded. + + >>> ns = process_namespace(psutil.Process()) + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + utils = [('cpu_percent', (), {}), ('memory_percent', (), {})] + + ignored = [ + ('as_dict', (), {}), + ('children', (), {'recursive': True}), + ('connections', (), {}), # deprecated + ('is_running', (), {}), + ('oneshot', (), {}), + ('parent', (), {}), + ('parents', (), {}), + ('pid', (), {}), + ('wait', (0,), {}), + ] + + getters = [ + ('cmdline', (), {}), + ('cpu_times', (), {}), + ('create_time', (), {}), + ('cwd', (), {}), + ('exe', (), {}), + ('memory_full_info', (), {}), + ('memory_info', (), {}), + ('name', (), {}), + ('net_connections', (), {'kind': 'all'}), + ('nice', (), {}), + ('num_ctx_switches', (), {}), + ('num_threads', (), {}), + ('open_files', (), {}), + ('ppid', (), {}), + ('status', (), {}), + ('threads', (), {}), + ('username', (), {}), + ] + if POSIX: + getters += [('uids', (), {})] + getters += [('gids', (), {})] + getters += [('terminal', (), {})] + getters += [('num_fds', (), {})] + if HAS_PROC_IO_COUNTERS: + getters += [('io_counters', (), {})] + if HAS_IONICE: + getters += [('ionice', (), {})] + if HAS_RLIMIT: + getters += [('rlimit', (psutil.RLIMIT_NOFILE,), {})] + if HAS_CPU_AFFINITY: + getters += [('cpu_affinity', (), {})] + if HAS_PROC_CPU_NUM: + getters += [('cpu_num', (), {})] + if HAS_ENVIRON: + getters += [('environ', (), {})] + if WINDOWS: + getters += [('num_handles', (), {})] + if HAS_MEMORY_MAPS: + getters += [('memory_maps', (), {'grouped': False})] + + setters = [] + if POSIX: + setters += [('nice', (0,), {})] + else: + setters += [('nice', (psutil.NORMAL_PRIORITY_CLASS,), {})] + if HAS_RLIMIT: + setters += [('rlimit', (psutil.RLIMIT_NOFILE, (1024, 4096)), {})] + if HAS_IONICE: + if LINUX: + setters += [('ionice', (psutil.IOPRIO_CLASS_NONE, 0), {})] + else: + setters += [('ionice', (psutil.IOPRIO_NORMAL,), {})] + if HAS_CPU_AFFINITY: + setters += [('cpu_affinity', ([_get_eligible_cpu()],), {})] + + killers = [ + ('send_signal', (signal.SIGTERM,), {}), + ('suspend', (), {}), + ('resume', (), {}), + ('terminate', (), {}), + ('kill', (), {}), + ] + if WINDOWS: + killers += [('send_signal', (signal.CTRL_C_EVENT,), {})] + killers += [('send_signal', (signal.CTRL_BREAK_EVENT,), {})] + + all = utils + getters + setters + killers + + def __init__(self, proc): + self._proc = proc + + def iter(self, ls, clear_cache=True): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + if clear_cache: + self.clear_cache() + fun = getattr(self._proc, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + def clear_cache(self): + """Clear the cache of a Process instance.""" + self._proc._init(self._proc.pid, _ignore_nsp=True) + + @classmethod + def test_class_coverage(cls, test_class, ls): + """Given a TestCase instance and a list of tuples checks that + the class defines the required test method names. + """ + for fun_name, _, _ in ls: + meth_name = 'test_' + fun_name + if not hasattr(test_class, meth_name): + msg = ( + f"{test_class.__class__.__name__!r} class should define a" + f" {meth_name!r} method" + ) + raise AttributeError(msg) + + @classmethod + def test(cls): + this = {x[0] for x in cls.all} + ignored = {x[0] for x in cls.ignored} + klass = {x for x in dir(psutil.Process) if x[0] != '_'} + leftout = (this | ignored) ^ klass + if leftout: + raise ValueError(f"uncovered Process class names: {leftout!r}") + + +class system_namespace: + """A container that lists all the module-level, system-related APIs. + Utilities such as cpu_percent() are excluded. Usage: + + >>> ns = system_namespace + >>> for fun, name in ns.iter(ns.getters): + ... fun() + """ + + getters = [ + ('boot_time', (), {}), + ('cpu_count', (), {'logical': False}), + ('cpu_count', (), {'logical': True}), + ('cpu_stats', (), {}), + ('cpu_times', (), {'percpu': False}), + ('cpu_times', (), {'percpu': True}), + ('disk_io_counters', (), {'perdisk': True}), + ('disk_partitions', (), {'all': True}), + ('disk_usage', (os.getcwd(),), {}), + ('net_connections', (), {'kind': 'all'}), + ('net_if_addrs', (), {}), + ('net_if_stats', (), {}), + ('net_io_counters', (), {'pernic': True}), + ('pid_exists', (os.getpid(),), {}), + ('pids', (), {}), + ('swap_memory', (), {}), + ('users', (), {}), + ('virtual_memory', (), {}), + ] + if HAS_CPU_FREQ: + if MACOS and platform.machine() == 'arm64': # skipped due to #1892 + pass + else: + getters += [('cpu_freq', (), {'percpu': True})] + if HAS_GETLOADAVG: + getters += [('getloadavg', (), {})] + if HAS_SENSORS_TEMPERATURES: + getters += [('sensors_temperatures', (), {})] + if HAS_SENSORS_FANS: + getters += [('sensors_fans', (), {})] + if HAS_SENSORS_BATTERY: + getters += [('sensors_battery', (), {})] + if WINDOWS: + getters += [('win_service_iter', (), {})] + getters += [('win_service_get', ('alg',), {})] + + ignored = [ + ('process_iter', (), {}), + ('wait_procs', ([psutil.Process()],), {}), + ('cpu_percent', (), {}), + ('cpu_times_percent', (), {}), + ] + + all = getters + + @staticmethod + def iter(ls): + """Given a list of tuples yields a set of (fun, fun_name) tuples + in random order. + """ + ls = list(ls) + random.shuffle(ls) + for fun_name, args, kwds in ls: + fun = getattr(psutil, fun_name) + fun = functools.partial(fun, *args, **kwds) + yield (fun, fun_name) + + test_class_coverage = process_namespace.test_class_coverage + + +def retry_on_failure(retries=NO_RETRIES): + """Decorator which runs a test function and retries N times before + actually failing. + """ + + def logfun(exc): + print(f"{exc!r}, retrying", file=sys.stderr) # noqa: T201 + + return retry( + exception=AssertionError, timeout=None, retries=retries, logfun=logfun + ) + + +def skip_on_access_denied(only_if=None): + """Decorator to Ignore AccessDenied exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except psutil.AccessDenied: + if only_if is not None: + if not only_if: + raise + raise pytest.skip("raises AccessDenied") + + return wrapper + + return decorator + + +def skip_on_not_implemented(only_if=None): + """Decorator to Ignore NotImplementedError exceptions.""" + + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + try: + return fun(*args, **kwargs) + except NotImplementedError: + if only_if is not None: + if not only_if: + raise + msg = ( + f"{fun.__name__!r} was skipped because it raised" + " NotImplementedError" + ) + raise pytest.skip(msg) + + return wrapper + + return decorator + + +# =================================================================== +# --- network +# =================================================================== + + +# XXX: no longer used +def get_free_port(host='127.0.0.1'): + """Return an unused TCP port. Subject to race conditions.""" + with socket.socket() as sock: + sock.bind((host, 0)) + return sock.getsockname()[1] + + +def bind_socket(family=AF_INET, type=SOCK_STREAM, addr=None): + """Binds a generic socket.""" + if addr is None and family in {AF_INET, AF_INET6}: + addr = ("", 0) + sock = socket.socket(family, type) + try: + if os.name not in {'nt', 'cygwin'}: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addr) + if type == socket.SOCK_STREAM: + sock.listen(5) + return sock + except Exception: + sock.close() + raise + + +def bind_unix_socket(name, type=socket.SOCK_STREAM): + """Bind a UNIX socket.""" + assert psutil.POSIX + assert not os.path.exists(name), name + sock = socket.socket(socket.AF_UNIX, type) + try: + sock.bind(name) + if type == socket.SOCK_STREAM: + sock.listen(5) + except Exception: + sock.close() + raise + return sock + + +def tcp_socketpair(family, addr=("", 0)): + """Build a pair of TCP sockets connected to each other. + Return a (server, client) tuple. + """ + with socket.socket(family, SOCK_STREAM) as ll: + ll.bind(addr) + ll.listen(5) + addr = ll.getsockname() + c = socket.socket(family, SOCK_STREAM) + try: + c.connect(addr) + caddr = c.getsockname() + while True: + a, addr = ll.accept() + # check that we've got the correct client + if addr == caddr: + return (a, c) + a.close() + except OSError: + c.close() + raise + + +def unix_socketpair(name): + """Build a pair of UNIX sockets connected to each other through + the same UNIX file name. + Return a (server, client) tuple. + """ + assert psutil.POSIX + server = client = None + try: + server = bind_unix_socket(name, type=socket.SOCK_STREAM) + server.setblocking(0) + client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client.setblocking(0) + client.connect(name) + # new = server.accept() + except Exception: + if server is not None: + server.close() + if client is not None: + client.close() + raise + return (server, client) + + +@contextlib.contextmanager +def create_sockets(): + """Open as many socket families / types as possible.""" + socks = [] + fname1 = fname2 = None + try: + socks.extend(( + bind_socket(socket.AF_INET, socket.SOCK_STREAM), + bind_socket(socket.AF_INET, socket.SOCK_DGRAM), + )) + if supports_ipv6(): + socks.extend(( + bind_socket(socket.AF_INET6, socket.SOCK_STREAM), + bind_socket(socket.AF_INET6, socket.SOCK_DGRAM), + )) + if POSIX and HAS_NET_CONNECTIONS_UNIX: + fname1 = get_testfn() + fname2 = get_testfn() + s1, s2 = unix_socketpair(fname1) + s3 = bind_unix_socket(fname2, type=socket.SOCK_DGRAM) + for s in (s1, s2, s3): + socks.append(s) + yield socks + finally: + for s in socks: + s.close() + for fname in (fname1, fname2): + if fname is not None: + safe_rmpath(fname) + + +def check_net_address(addr, family): + """Check a net address validity. Supported families are IPv4, + IPv6 and MAC addresses. + """ + assert isinstance(family, enum.IntEnum), family + if family == socket.AF_INET: + octs = [int(x) for x in addr.split('.')] + assert len(octs) == 4, addr + for num in octs: + assert 0 <= num <= 255, addr + ipaddress.IPv4Address(addr) + elif family == socket.AF_INET6: + assert isinstance(addr, str), addr + ipaddress.IPv6Address(addr) + elif family == psutil.AF_LINK: + assert re.match(r'([a-fA-F0-9]{2}[:|\-]?){6}', addr) is not None, addr + else: + raise ValueError(f"unknown family {family!r}") + + +def check_connection_ntuple(conn): + """Check validity of a connection namedtuple.""" + + def check_ntuple(conn): + has_pid = len(conn) == 7 + assert len(conn) in {6, 7}, len(conn) + assert conn[0] == conn.fd, conn.fd + assert conn[1] == conn.family, conn.family + assert conn[2] == conn.type, conn.type + assert conn[3] == conn.laddr, conn.laddr + assert conn[4] == conn.raddr, conn.raddr + assert conn[5] == conn.status, conn.status + if has_pid: + assert conn[6] == conn.pid, conn.pid + + def check_family(conn): + assert conn.family in {AF_INET, AF_INET6, AF_UNIX}, conn.family + assert isinstance(conn.family, enum.IntEnum), conn + if conn.family == AF_INET: + # actually try to bind the local socket; ignore IPv6 + # sockets as their address might be represented as + # an IPv4-mapped-address (e.g. "::127.0.0.1") + # and that's rejected by bind() + with socket.socket(conn.family, conn.type) as s: + try: + s.bind((conn.laddr[0], 0)) + except OSError as err: + if err.errno != errno.EADDRNOTAVAIL: + raise + elif conn.family == AF_UNIX: + assert conn.status == psutil.CONN_NONE, conn.status + + def check_type(conn): + # SOCK_SEQPACKET may happen in case of AF_UNIX socks + SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) + assert conn.type in { + socket.SOCK_STREAM, + socket.SOCK_DGRAM, + SOCK_SEQPACKET, + }, conn.type + assert isinstance(conn.type, enum.IntEnum), conn + if conn.type == socket.SOCK_DGRAM: + assert conn.status == psutil.CONN_NONE, conn.status + + def check_addrs(conn): + # check IP address and port sanity + for addr in (conn.laddr, conn.raddr): + if conn.family in {AF_INET, AF_INET6}: + assert isinstance(addr, tuple), type(addr) + if not addr: + continue + assert isinstance(addr.port, int), type(addr.port) + assert 0 <= addr.port <= 65535, addr.port + check_net_address(addr.ip, conn.family) + elif conn.family == AF_UNIX: + assert isinstance(addr, str), type(addr) + + def check_status(conn): + assert isinstance(conn.status, str), conn.status + valids = [ + getattr(psutil, x) for x in dir(psutil) if x.startswith('CONN_') + ] + assert conn.status in valids, conn.status + if conn.family in {AF_INET, AF_INET6} and conn.type == SOCK_STREAM: + assert conn.status != psutil.CONN_NONE, conn.status + else: + assert conn.status == psutil.CONN_NONE, conn.status + + check_ntuple(conn) + check_family(conn) + check_type(conn) + check_addrs(conn) + check_status(conn) + + +def filter_proc_net_connections(cons): + """Our process may start with some open UNIX sockets which are not + initialized by us, invalidating unit tests. + """ + new = [] + for conn in cons: + if POSIX and conn.family == socket.AF_UNIX: + if MACOS and "/syslog" in conn.raddr: + debug(f"skipping {conn}") + continue + new.append(conn) + return new + + +# =================================================================== +# --- import utils +# =================================================================== + + +def reload_module(module): + return importlib.reload(module) + + +def import_module_by_path(path): + name = os.path.splitext(os.path.basename(path))[0] + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +# =================================================================== +# --- others +# =================================================================== + + +def warn(msg): + """Raise a warning msg.""" + warnings.warn(msg, UserWarning, stacklevel=2) + + +def is_namedtuple(x): + """Check if object is an instance of namedtuple.""" + t = type(x) + b = t.__bases__ + if len(b) != 1 or b[0] is not tuple: + return False + f = getattr(t, '_fields', None) + if not isinstance(f, tuple): + return False + return all(isinstance(n, str) for n in f) + + +if POSIX: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared CO lib used + by this process, copies it in another location and loads it + in memory via ctypes. Return the new absolutized path. + """ + exe = 'pypy' if PYPY else 'python' + ext = ".so" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if os.path.splitext(x.path)[1] == ext and exe in x.path.lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + try: + ctypes.CDLL(dst) + yield dst + finally: + safe_rmpath(dst) + +else: + + @contextlib.contextmanager + def copyload_shared_lib(suffix=""): + """Ctx manager which picks up a random shared DLL lib used + by this process, copies it in another location and loads it + in memory via ctypes. + Return the new absolutized, normcased path. + """ + from ctypes import WinError + from ctypes import wintypes + + ext = ".dll" + dst = get_testfn(suffix=suffix + ext) + libs = [ + x.path + for x in psutil.Process().memory_maps() + if x.path.lower().endswith(ext) + and 'python' in os.path.basename(x.path).lower() + and 'wow64' not in x.path.lower() + ] + if PYPY and not libs: + libs = [ + x.path + for x in psutil.Process().memory_maps() + if 'pypy' in os.path.basename(x.path).lower() + ] + src = random.choice(libs) + shutil.copyfile(src, dst) + cfile = None + try: + cfile = ctypes.WinDLL(dst) + yield dst + finally: + # Work around OverflowError: + # - https://ci.appveyor.com/project/giampaolo/psutil/build/1207/ + # job/o53330pbnri9bcw7 + # - http://bugs.python.org/issue30286 + # - http://stackoverflow.com/questions/23522055 + if cfile is not None: + FreeLibrary = ctypes.windll.kernel32.FreeLibrary + FreeLibrary.argtypes = [wintypes.HMODULE] + ret = FreeLibrary(cfile._handle) + if ret == 0: + raise WinError() + safe_rmpath(dst) + + +# =================================================================== +# --- Exit funs (first is executed last) +# =================================================================== + + +# this is executed first +@atexit.register +def cleanup_test_procs(): + reap_children(recursive=True) + + +# atexit module does not execute exit functions in case of SIGTERM, which +# gets sent to test subprocesses, which is a problem if they import this +# module. With this it will. See: +# https://gmpy.dev/blog/2016/how-to-always-execute-exit-functions-in-python +if POSIX: + signal.signal(signal.SIGTERM, lambda sig, _: sys.exit(sig)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__main__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..ce6fc24c7f09a273983c81d9ae5b2180e83fbb9f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__main__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Run unit tests. This is invoked by: +$ python -m psutil.tests. +""" + +from psutil.tests import pytest + + +pytest.main(["-v", "-s", "--tb=short"]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66910afc963bbc0873ec92091de8df98026201c8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f15c1310b023521062b3a00fa3a56279d9523f8a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/__main__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4451bff7c0a85e26d1d7e983eac57610fb035f8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_aix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8727a48a23961eb1171903f0589b10cddf2a9c1a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_bsd.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8d7a382bf1e41cbd4c076b2f265b8adf5ee46e5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_connections.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_contracts.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_contracts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3dd7ca8d9af2e3556ddb676ac4e94c0e0794bad0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_contracts.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_memleaks.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_memleaks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..133a08b71c0b0653fc60f61b8ff3268392a0fd81 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_memleaks.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_misc.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_misc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..970831ad658e650e3e7e789a8280e0383eb7ced9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_misc.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_osx.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_osx.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d1982f267b4a8bfae70ce6c01321034ddc14b27 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_osx.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b24291321a4e977099a04da2ec560d9ba48c7657 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_posix.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4f91a96a06acda24717f1584e9facc435ffaa69 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process_all.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process_all.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60bbe14360df396583a18bf91de1309f6d9ce988 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_process_all.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..765ca5a1f38a902f2ea9eeb4dd759fba7f347c5c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_scripts.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b8a80f228fd5b9ed7bef98ad541e09055a9e885 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_sunos.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f83499d7bf722e2799371896edbab6485b4d3af3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_system.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_testutils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_testutils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62e408f7b5010bb64ce616a7b2d95044c24e8acc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_testutils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fcff023059e6a3cc5cb08e9222b668ca58c94125 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_unicode.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_windows.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_windows.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b80d914f3e8476ec2eadc72e5972624c79cf4831 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/__pycache__/test_windows.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_aix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_aix.py new file mode 100644 index 0000000000000000000000000000000000000000..10934c12d41ee19b641fca103bee992b9336fc16 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_aix.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola' +# Copyright (c) 2017, Arnon Yaari +# All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""AIX specific tests.""" + +import re + +import psutil +from psutil import AIX +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import sh + + +@pytest.mark.skipif(not AIX, reason="AIX only") +class AIXSpecificTestCase(PsutilTestCase): + def test_virtual_memory(self): + out = sh('/usr/bin/svmon -O unit=KB') + re_pattern = r"memory\s*" + for field in [ + "size", + "inuse", + "free", + "pin", + "virtual", + "available", + "mmode", + ]: + re_pattern += rf"(?P<{field}>\S+)\s+" + matchobj = re.search(re_pattern, out) + + assert matchobj is not None + + KB = 1024 + total = int(matchobj.group("size")) * KB + available = int(matchobj.group("available")) * KB + used = int(matchobj.group("inuse")) * KB + free = int(matchobj.group("free")) * KB + + psutil_result = psutil.virtual_memory() + + # TOLERANCE_SYS_MEM from psutil.tests is not enough. For some reason + # we're seeing differences of ~1.2 MB. 2 MB is still a good tolerance + # when compared to GBs. + TOLERANCE_SYS_MEM = 2 * KB * KB # 2 MB + assert psutil_result.total == total + assert abs(psutil_result.used - used) < TOLERANCE_SYS_MEM + assert abs(psutil_result.available - available) < TOLERANCE_SYS_MEM + assert abs(psutil_result.free - free) < TOLERANCE_SYS_MEM + + def test_swap_memory(self): + out = sh('/usr/sbin/lsps -a') + # From the man page, "The size is given in megabytes" so we assume + # we'll always have 'MB' in the result + # TODO maybe try to use "swap -l" to check "used" too, but its units + # are not guaranteed to be "MB" so parsing may not be consistent + matchobj = re.search( + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\S+)\s+" + r"(?P\d+)MB", + out, + ) + + assert matchobj is not None + + total_mb = int(matchobj.group("size")) + MB = 1024**2 + psutil_result = psutil.swap_memory() + # we divide our result by MB instead of multiplying the lsps value by + # MB because lsps may round down, so we round down too + assert int(psutil_result.total / MB) == total_mb + + def test_cpu_stats(self): + out = sh('/usr/bin/mpstat -a') + + re_pattern = r"ALL\s*" + for field in [ + "min", + "maj", + "mpcs", + "mpcr", + "dev", + "soft", + "dec", + "ph", + "cs", + "ics", + "bound", + "rq", + "push", + "S3pull", + "S3grd", + "S0rd", + "S1rd", + "S2rd", + "S3rd", + "S4rd", + "S5rd", + "sysc", + ]: + re_pattern += rf"(?P<{field}>\S+)\s+" + matchobj = re.search(re_pattern, out) + + assert matchobj is not None + + # numbers are usually in the millions so 1000 is ok for tolerance + CPU_STATS_TOLERANCE = 1000 + psutil_result = psutil.cpu_stats() + assert ( + abs(psutil_result.ctx_switches - int(matchobj.group("cs"))) + < CPU_STATS_TOLERANCE + ) + assert ( + abs(psutil_result.syscalls - int(matchobj.group("sysc"))) + < CPU_STATS_TOLERANCE + ) + assert ( + abs(psutil_result.interrupts - int(matchobj.group("dev"))) + < CPU_STATS_TOLERANCE + ) + assert ( + abs(psutil_result.soft_interrupts - int(matchobj.group("soft"))) + < CPU_STATS_TOLERANCE + ) + + def test_cpu_count_logical(self): + out = sh('/usr/bin/mpstat -a') + mpstat_lcpu = int(re.search(r"lcpu=(\d+)", out).group(1)) + psutil_lcpu = psutil.cpu_count(logical=True) + assert mpstat_lcpu == psutil_lcpu + + def test_net_if_addrs_names(self): + out = sh('/etc/ifconfig -l') + ifconfig_names = set(out.split()) + psutil_names = set(psutil.net_if_addrs().keys()) + assert ifconfig_names == psutil_names diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_bsd.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_bsd.py new file mode 100644 index 0000000000000000000000000000000000000000..2786c348577855207cc4169714721bebb8776828 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_bsd.py @@ -0,0 +1,593 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# TODO: (FreeBSD) add test for comparing connections with 'sockstat' cmd. + + +"""Tests specific to all BSD platforms.""" + +import datetime +import os +import re +import shutil +import time + +import psutil +from psutil import BSD +from psutil import FREEBSD +from psutil import NETBSD +from psutil import OPENBSD +from psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if BSD: + from psutil._psutil_posix import getpagesize + + PAGESIZE = getpagesize() + # muse requires root privileges + MUSE_AVAILABLE = os.getuid() == 0 and shutil.which("muse") +else: + PAGESIZE = None + MUSE_AVAILABLE = False + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + result = sh("sysctl " + cmdline) + if FREEBSD: + result = result[result.find(": ") + 2 :] + elif OPENBSD or NETBSD: + result = result[result.find("=") + 1 :] + try: + return int(result) + except ValueError: + return result + + +def muse(field): + """Thin wrapper around 'muse' cmdline utility.""" + out = sh('muse') + for line in out.split('\n'): + if line.startswith(field): + break + else: + raise ValueError("line not found") + return int(line.split()[1]) + + +# ===================================================================== +# --- All BSD* +# ===================================================================== + + +@pytest.mark.skipif(not BSD, reason="BSD only") +class BSDTestCase(PsutilTestCase): + """Generic tests common to all BSD variants.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @pytest.mark.skipif(NETBSD, reason="-o lstart doesn't work on NETBSD") + def test_process_create_time(self): + output = sh(f"ps -o lstart -p {self.pid}") + start_ps = output.replace('STARTED', '').strip() + start_psutil = psutil.Process(self.pid).create_time() + start_psutil = time.strftime( + "%a %b %e %H:%M:%S %Y", time.localtime(start_psutil) + ) + assert start_ps == start_psutil + + def test_disks(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh(f'df -k "{path}"').strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total = int(total) * 1024 + used = int(used) * 1024 + free = int(free) * 1024 + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + dev, total, used, free = df(part.mountpoint) + assert part.device == dev + assert usage.total == total + # 10 MB tolerance + if abs(usage.free - free) > 10 * 1024 * 1024: + raise self.fail(f"psutil={usage.free}, df={free}") + if abs(usage.used - used) > 10 * 1024 * 1024: + raise self.fail(f"psutil={usage.used}, df={used}") + + @pytest.mark.skipif( + not shutil.which("sysctl"), reason="sysctl cmd not available" + ) + def test_cpu_count_logical(self): + syst = sysctl("hw.ncpu") + assert psutil.cpu_count(logical=True) == syst + + @pytest.mark.skipif( + not shutil.which("sysctl"), reason="sysctl cmd not available" + ) + @pytest.mark.skipif( + NETBSD, reason="skipped on NETBSD" # we check /proc/meminfo + ) + def test_virtual_memory_total(self): + num = sysctl('hw.physmem') + assert num == psutil.virtual_memory().total + + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig cmd not available" + ) + def test_net_if_stats(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + assert stats.isup == ('RUNNING' in out) + if "mtu" in out: + assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) + + +# ===================================================================== +# --- FreeBSD +# ===================================================================== + + +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") +class FreeBSDPsutilTestCase(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + @retry_on_failure() + def test_memory_maps(self): + out = sh(f"procstat -v {self.pid}") + maps = psutil.Process(self.pid).memory_maps(grouped=False) + lines = out.split('\n')[1:] + while lines: + line = lines.pop() + fields = line.split() + _, start, stop, _perms, res = fields[:5] + map = maps.pop() + assert f"{start}-{stop}" == map.addr + assert int(res) == map.rss + if not map.path.startswith('['): + assert fields[10] == map.path + + def test_exe(self): + out = sh(f"procstat -b {self.pid}") + assert psutil.Process(self.pid).exe() == out.split('\n')[1].split()[-1] + + def test_cmdline(self): + out = sh(f"procstat -c {self.pid}") + assert ' '.join(psutil.Process(self.pid).cmdline()) == ' '.join( + out.split('\n')[1].split()[2:] + ) + + def test_uids_gids(self): + out = sh(f"procstat -s {self.pid}") + euid, ruid, suid, egid, rgid, sgid = out.split('\n')[1].split()[2:8] + p = psutil.Process(self.pid) + uids = p.uids() + gids = p.gids() + assert uids.real == int(ruid) + assert uids.effective == int(euid) + assert uids.saved == int(suid) + assert gids.real == int(rgid) + assert gids.effective == int(egid) + assert gids.saved == int(sgid) + + @retry_on_failure() + def test_ctx_switches(self): + tested = [] + out = sh(f"procstat -r {self.pid}") + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if ' voluntary context' in line: + pstat_value = int(line.split()[-1]) + psutil_value = p.num_ctx_switches().voluntary + assert pstat_value == psutil_value + tested.append(None) + elif ' involuntary context' in line: + pstat_value = int(line.split()[-1]) + psutil_value = p.num_ctx_switches().involuntary + assert pstat_value == psutil_value + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + @retry_on_failure() + def test_cpu_times(self): + tested = [] + out = sh(f"procstat -r {self.pid}") + p = psutil.Process(self.pid) + for line in out.split('\n'): + line = line.lower().strip() + if 'user time' in line: + pstat_value = float('0.' + line.split()[-1].split('.')[-1]) + psutil_value = p.cpu_times().user + assert pstat_value == psutil_value + tested.append(None) + elif 'system time' in line: + pstat_value = float('0.' + line.split()[-1].split('.')[-1]) + psutil_value = p.cpu_times().system + assert pstat_value == psutil_value + tested.append(None) + if len(tested) != 2: + raise RuntimeError("couldn't find lines match in procstat out") + + +@pytest.mark.skipif(not FREEBSD, reason="FREEBSD only") +class FreeBSDSystemTestCase(PsutilTestCase): + @staticmethod + def parse_swapinfo(): + # the last line is always the total + output = sh("swapinfo -k").splitlines()[-1] + parts = re.split(r'\s+', output) + + if not parts: + raise ValueError(f"Can't parse swapinfo: {output}") + + # the size is in 1k units, so multiply by 1024 + total, used, free = (int(p) * 1024 for p in parts[1:4]) + return total, used, free + + def test_cpu_frequency_against_sysctl(self): + # Currently only cpu 0 is frequency is supported in FreeBSD + # All other cores use the same frequency. + sensor = "dev.cpu.0.freq" + try: + sysctl_result = int(sysctl(sensor)) + except RuntimeError: + raise pytest.skip("frequencies not supported by kernel") + assert psutil.cpu_freq().current == sysctl_result + + sensor = "dev.cpu.0.freq_levels" + sysctl_result = sysctl(sensor) + # sysctl returns a string of the format: + # / /... + # Ordered highest available to lowest available. + max_freq = int(sysctl_result.split()[0].split("/")[0]) + min_freq = int(sysctl_result.split()[-1].split("/")[0]) + assert psutil.cpu_freq().max == max_freq + assert psutil.cpu_freq().min == min_freq + + # --- virtual_memory(); tests against sysctl + + @retry_on_failure() + def test_vmem_active(self): + syst = sysctl("vm.stats.vm.v_active_count") * PAGESIZE + assert abs(psutil.virtual_memory().active - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_inactive(self): + syst = sysctl("vm.stats.vm.v_inactive_count") * PAGESIZE + assert abs(psutil.virtual_memory().inactive - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_wired(self): + syst = sysctl("vm.stats.vm.v_wire_count") * PAGESIZE + assert abs(psutil.virtual_memory().wired - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_cached(self): + syst = sysctl("vm.stats.vm.v_cache_count") * PAGESIZE + assert abs(psutil.virtual_memory().cached - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_free(self): + syst = sysctl("vm.stats.vm.v_free_count") * PAGESIZE + assert abs(psutil.virtual_memory().free - syst) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_buffers(self): + syst = sysctl("vfs.bufspace") + assert abs(psutil.virtual_memory().buffers - syst) < TOLERANCE_SYS_MEM + + # --- virtual_memory(); tests against muse + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + def test_muse_vmem_total(self): + num = muse('Total') + assert psutil.virtual_memory().total == num + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_active(self): + num = muse('Active') + assert abs(psutil.virtual_memory().active - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_inactive(self): + num = muse('Inactive') + assert abs(psutil.virtual_memory().inactive - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_wired(self): + num = muse('Wired') + assert abs(psutil.virtual_memory().wired - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_cached(self): + num = muse('Cache') + assert abs(psutil.virtual_memory().cached - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_free(self): + num = muse('Free') + assert abs(psutil.virtual_memory().free - num) < TOLERANCE_SYS_MEM + + @pytest.mark.skipif(not MUSE_AVAILABLE, reason="muse not installed") + @retry_on_failure() + def test_muse_vmem_buffers(self): + num = muse('Buffer') + assert abs(psutil.virtual_memory().buffers - num) < TOLERANCE_SYS_MEM + + def test_cpu_stats_ctx_switches(self): + assert ( + abs( + psutil.cpu_stats().ctx_switches + - sysctl('vm.stats.sys.v_swtch') + ) + < 1000 + ) + + def test_cpu_stats_interrupts(self): + assert ( + abs(psutil.cpu_stats().interrupts - sysctl('vm.stats.sys.v_intr')) + < 1000 + ) + + def test_cpu_stats_soft_interrupts(self): + assert ( + abs( + psutil.cpu_stats().soft_interrupts + - sysctl('vm.stats.sys.v_soft') + ) + < 1000 + ) + + @retry_on_failure() + def test_cpu_stats_syscalls(self): + # pretty high tolerance but it looks like it's OK. + assert ( + abs(psutil.cpu_stats().syscalls - sysctl('vm.stats.sys.v_syscall')) + < 200000 + ) + + # def test_cpu_stats_traps(self): + # self.assertAlmostEqual(psutil.cpu_stats().traps, + # sysctl('vm.stats.sys.v_trap'), delta=1000) + + # --- swap memory + + def test_swapmem_free(self): + _total, _used, free = self.parse_swapinfo() + assert abs(psutil.swap_memory().free - free) < TOLERANCE_SYS_MEM + + def test_swapmem_used(self): + _total, used, _free = self.parse_swapinfo() + assert abs(psutil.swap_memory().used - used) < TOLERANCE_SYS_MEM + + def test_swapmem_total(self): + total, _used, _free = self.parse_swapinfo() + assert abs(psutil.swap_memory().total - total) < TOLERANCE_SYS_MEM + + # --- others + + def test_boot_time(self): + s = sysctl('sysctl kern.boottime') + s = s[s.find(" sec = ") + 7 :] + s = s[: s.find(',')] + btime = int(s) + assert btime == psutil.boot_time() + + # --- sensors_battery + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + def secs2hours(secs): + m, _s = divmod(secs, 60) + h, m = divmod(m, 60) + return f"{int(h)}:{int(m):02}" + + out = sh("acpiconf -i 0") + fields = {x.split('\t')[0]: x.split('\t')[-1] for x in out.split("\n")} + metrics = psutil.sensors_battery() + percent = int(fields['Remaining capacity:'].replace('%', '')) + remaining_time = fields['Remaining time:'] + assert metrics.percent == percent + if remaining_time == 'unknown': + assert metrics.secsleft == psutil.POWER_TIME_UNLIMITED + else: + assert secs2hours(metrics.secsleft) == remaining_time + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery_against_sysctl(self): + assert psutil.sensors_battery().percent == sysctl( + "hw.acpi.battery.life" + ) + assert psutil.sensors_battery().power_plugged == ( + sysctl("hw.acpi.acline") == 1 + ) + secsleft = psutil.sensors_battery().secsleft + if secsleft < 0: + assert sysctl("hw.acpi.battery.time") == -1 + else: + assert secsleft == sysctl("hw.acpi.battery.time") * 60 + + @pytest.mark.skipif(HAS_BATTERY, reason="has battery") + def test_sensors_battery_no_battery(self): + # If no battery is present one of these calls is supposed + # to fail, see: + # https://github.com/giampaolo/psutil/issues/1074 + with pytest.raises(RuntimeError): + sysctl("hw.acpi.battery.life") + sysctl("hw.acpi.battery.time") + sysctl("hw.acpi.acline") + assert psutil.sensors_battery() is None + + # --- sensors_temperatures + + def test_sensors_temperatures_against_sysctl(self): + num_cpus = psutil.cpu_count(True) + for cpu in range(num_cpus): + sensor = f"dev.cpu.{cpu}.temperature" + # sysctl returns a string in the format 46.0C + try: + sysctl_result = int(float(sysctl(sensor)[:-1])) + except RuntimeError: + raise pytest.skip("temperatures not supported by kernel") + assert ( + abs( + psutil.sensors_temperatures()["coretemp"][cpu].current + - sysctl_result + ) + < 10 + ) + + sensor = f"dev.cpu.{cpu}.coretemp.tjmax" + sysctl_result = int(float(sysctl(sensor)[:-1])) + assert ( + psutil.sensors_temperatures()["coretemp"][cpu].high + == sysctl_result + ) + + +# ===================================================================== +# --- OpenBSD +# ===================================================================== + + +@pytest.mark.skipif(not OPENBSD, reason="OPENBSD only") +class OpenBSDTestCase(PsutilTestCase): + def test_boot_time(self): + s = sysctl('kern.boottime') + sys_bt = datetime.datetime.strptime(s, "%a %b %d %H:%M:%S %Y") + psutil_bt = datetime.datetime.fromtimestamp(psutil.boot_time()) + assert sys_bt == psutil_bt + + +# ===================================================================== +# --- NetBSD +# ===================================================================== + + +@pytest.mark.skipif(not NETBSD, reason="NETBSD only") +class NetBSDTestCase(PsutilTestCase): + @staticmethod + def parse_meminfo(look_for): + with open('/proc/meminfo') as f: + for line in f: + if line.startswith(look_for): + return int(line.split()[1]) * 1024 + raise ValueError(f"can't find {look_for}") + + # --- virtual mem + + def test_vmem_total(self): + assert psutil.virtual_memory().total == self.parse_meminfo("MemTotal:") + + def test_vmem_free(self): + assert ( + abs(psutil.virtual_memory().free - self.parse_meminfo("MemFree:")) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_buffers(self): + assert ( + abs( + psutil.virtual_memory().buffers + - self.parse_meminfo("Buffers:") + ) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_shared(self): + assert ( + abs( + psutil.virtual_memory().shared + - self.parse_meminfo("MemShared:") + ) + < TOLERANCE_SYS_MEM + ) + + def test_vmem_cached(self): + assert ( + abs(psutil.virtual_memory().cached - self.parse_meminfo("Cached:")) + < TOLERANCE_SYS_MEM + ) + + # --- swap mem + + def test_swapmem_total(self): + assert ( + abs(psutil.swap_memory().total - self.parse_meminfo("SwapTotal:")) + < TOLERANCE_SYS_MEM + ) + + def test_swapmem_free(self): + assert ( + abs(psutil.swap_memory().free - self.parse_meminfo("SwapFree:")) + < TOLERANCE_SYS_MEM + ) + + def test_swapmem_used(self): + smem = psutil.swap_memory() + assert smem.used == smem.total - smem.free + + # --- others + + def test_cpu_stats_interrupts(self): + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'intr'): + interrupts = int(line.split()[1]) + break + else: + raise ValueError("couldn't find line") + assert abs(psutil.cpu_stats().interrupts - interrupts) < 1000 + + def test_cpu_stats_ctx_switches(self): + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'ctxt'): + ctx_switches = int(line.split()[1]) + break + else: + raise ValueError("couldn't find line") + assert abs(psutil.cpu_stats().ctx_switches - ctx_switches) < 1000 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_connections.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_connections.py new file mode 100644 index 0000000000000000000000000000000000000000..5ddeb855f2a78ba21132c2694b6f426f548cbb48 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_connections.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for psutil.net_connections() and Process.net_connections() APIs.""" + +import os +import socket +import textwrap +from contextlib import closing +from socket import AF_INET +from socket import AF_INET6 +from socket import SOCK_DGRAM +from socket import SOCK_STREAM + +import psutil +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import supports_ipv6 +from psutil.tests import AF_UNIX +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import filter_proc_net_connections +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import skip_on_access_denied +from psutil.tests import tcp_socketpair +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file + + +SOCK_SEQPACKET = getattr(socket, "SOCK_SEQPACKET", object()) + + +def this_proc_net_connections(kind): + cons = psutil.Process().net_connections(kind=kind) + if kind in {"all", "unix"}: + return filter_proc_net_connections(cons) + return cons + + +@pytest.mark.xdist_group(name="serial") +class ConnectionTestCase(PsutilTestCase): + def setUp(self): + assert this_proc_net_connections(kind='all') == [] + + def tearDown(self): + # Make sure we closed all resources. + assert this_proc_net_connections(kind='all') == [] + + def compare_procsys_connections(self, pid, proc_cons, kind='all'): + """Given a process PID and its list of connections compare + those against system-wide connections retrieved via + psutil.net_connections. + """ + try: + sys_cons = psutil.net_connections(kind=kind) + except psutil.AccessDenied: + # On MACOS, system-wide connections are retrieved by iterating + # over all processes + if MACOS: + return + else: + raise + # Filter for this proc PID and exlucde PIDs from the tuple. + sys_cons = [c[:-1] for c in sys_cons if c.pid == pid] + sys_cons.sort() + proc_cons.sort() + assert proc_cons == sys_cons + + +class TestBasicOperations(ConnectionTestCase): + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") + def test_system(self): + with create_sockets(): + for conn in psutil.net_connections(kind='all'): + check_connection_ntuple(conn) + + def test_process(self): + with create_sockets(): + for conn in this_proc_net_connections(kind='all'): + check_connection_ntuple(conn) + + def test_invalid_kind(self): + with pytest.raises(ValueError): + this_proc_net_connections(kind='???') + with pytest.raises(ValueError): + psutil.net_connections(kind='???') + + +@pytest.mark.xdist_group(name="serial") +class TestUnconnectedSockets(ConnectionTestCase): + """Tests sockets which are open but not connected to anything.""" + + def get_conn_from_sock(self, sock): + cons = this_proc_net_connections(kind='all') + smap = {c.fd: c for c in cons} + if NETBSD or FREEBSD: + # NetBSD opens a UNIX socket to /var/log/run + # so there may be more connections. + return smap[sock.fileno()] + else: + assert len(cons) == 1 + if cons[0].fd != -1: + assert smap[sock.fileno()].fd == sock.fileno() + return cons[0] + + def check_socket(self, sock): + """Given a socket, makes sure it matches the one obtained + via psutil. It assumes this process created one connection + only (the one supposed to be checked). + """ + conn = self.get_conn_from_sock(sock) + check_connection_ntuple(conn) + + # fd, family, type + if conn.fd != -1: + assert conn.fd == sock.fileno() + assert conn.family == sock.family + # see: http://bugs.python.org/issue30204 + assert conn.type == sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) + + # local address + laddr = sock.getsockname() + if not laddr and isinstance(laddr, bytes): + # See: http://bugs.python.org/issue30205 + laddr = laddr.decode() + if sock.family == AF_INET6: + laddr = laddr[:2] + assert conn.laddr == laddr + + # XXX Solaris can't retrieve system-wide UNIX sockets + if sock.family == AF_UNIX and HAS_NET_CONNECTIONS_UNIX: + cons = this_proc_net_connections(kind='all') + self.compare_procsys_connections(os.getpid(), cons, kind='all') + return conn + + def test_tcp_v4(self): + addr = ("127.0.0.1", 0) + with closing(bind_socket(AF_INET, SOCK_STREAM, addr=addr)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == () + assert conn.status == psutil.CONN_LISTEN + + @pytest.mark.skipif(not supports_ipv6(), reason="IPv6 not supported") + def test_tcp_v6(self): + addr = ("::1", 0) + with closing(bind_socket(AF_INET6, SOCK_STREAM, addr=addr)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == () + assert conn.status == psutil.CONN_LISTEN + + def test_udp_v4(self): + addr = ("127.0.0.1", 0) + with closing(bind_socket(AF_INET, SOCK_DGRAM, addr=addr)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == () + assert conn.status == psutil.CONN_NONE + + @pytest.mark.skipif(not supports_ipv6(), reason="IPv6 not supported") + def test_udp_v6(self): + addr = ("::1", 0) + with closing(bind_socket(AF_INET6, SOCK_DGRAM, addr=addr)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == () + assert conn.status == psutil.CONN_NONE + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_unix_tcp(self): + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == "" + assert conn.status == psutil.CONN_NONE + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_unix_udp(self): + testfn = self.get_testfn() + with closing(bind_unix_socket(testfn, type=SOCK_STREAM)) as sock: + conn = self.check_socket(sock) + assert conn.raddr == "" + assert conn.status == psutil.CONN_NONE + + +@pytest.mark.xdist_group(name="serial") +class TestConnectedSocket(ConnectionTestCase): + """Test socket pairs which are actually connected to + each other. + """ + + # On SunOS, even after we close() it, the server socket stays around + # in TIME_WAIT state. + @pytest.mark.skipif(SUNOS, reason="unreliable on SUONS") + def test_tcp(self): + addr = ("127.0.0.1", 0) + assert this_proc_net_connections(kind='tcp4') == [] + server, client = tcp_socketpair(AF_INET, addr=addr) + try: + cons = this_proc_net_connections(kind='tcp4') + assert len(cons) == 2 + assert cons[0].status == psutil.CONN_ESTABLISHED + assert cons[1].status == psutil.CONN_ESTABLISHED + # May not be fast enough to change state so it stays + # commenteed. + # client.close() + # cons = this_proc_net_connections(kind='all') + # self.assertEqual(len(cons), 1) + # self.assertEqual(cons[0].status, psutil.CONN_CLOSE_WAIT) + finally: + server.close() + client.close() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_unix(self): + testfn = self.get_testfn() + server, client = unix_socketpair(testfn) + try: + cons = this_proc_net_connections(kind='unix') + assert not (cons[0].laddr and cons[0].raddr), cons + assert not (cons[1].laddr and cons[1].raddr), cons + if NETBSD or FREEBSD: + # On NetBSD creating a UNIX socket will cause + # a UNIX connection to /var/run/log. + cons = [c for c in cons if c.raddr != '/var/run/log'] + assert len(cons) == 2 + if LINUX or FREEBSD or SUNOS or OPENBSD: + # remote path is never set + assert cons[0].raddr == "" + assert cons[1].raddr == "" + # one local address should though + assert testfn == (cons[0].laddr or cons[1].laddr) + else: + # On other systems either the laddr or raddr + # of both peers are set. + assert (cons[0].laddr or cons[1].laddr) == testfn + finally: + server.close() + client.close() + + +class TestFilters(ConnectionTestCase): + def test_filters(self): + def check(kind, families, types): + for conn in this_proc_net_connections(kind=kind): + assert conn.family in families + assert conn.type in types + if not SKIP_SYSCONS: + for conn in psutil.net_connections(kind=kind): + assert conn.family in families + assert conn.type in types + + with create_sockets(): + check( + 'all', + [AF_INET, AF_INET6, AF_UNIX], + [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET], + ) + check('inet', [AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]) + check('inet4', [AF_INET], [SOCK_STREAM, SOCK_DGRAM]) + check('tcp', [AF_INET, AF_INET6], [SOCK_STREAM]) + check('tcp4', [AF_INET], [SOCK_STREAM]) + check('tcp6', [AF_INET6], [SOCK_STREAM]) + check('udp', [AF_INET, AF_INET6], [SOCK_DGRAM]) + check('udp4', [AF_INET], [SOCK_DGRAM]) + check('udp6', [AF_INET6], [SOCK_DGRAM]) + if HAS_NET_CONNECTIONS_UNIX: + check( + 'unix', + [AF_UNIX], + [SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET], + ) + + @skip_on_access_denied(only_if=MACOS) + def test_combos(self): + reap_children() + + def check_conn(proc, conn, family, type, laddr, raddr, status, kinds): + all_kinds = ( + "all", + "inet", + "inet4", + "inet6", + "tcp", + "tcp4", + "tcp6", + "udp", + "udp4", + "udp6", + ) + check_connection_ntuple(conn) + assert conn.family == family + assert conn.type == type + assert conn.laddr == laddr + assert conn.raddr == raddr + assert conn.status == status + for kind in all_kinds: + cons = proc.net_connections(kind=kind) + if kind in kinds: + assert cons != [] + else: + assert cons == [] + # compare against system-wide connections + # XXX Solaris can't retrieve system-wide UNIX + # sockets. + if HAS_NET_CONNECTIONS_UNIX: + self.compare_procsys_connections(proc.pid, [conn]) + + tcp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_STREAM) + s.bind(('{addr}', 0)) + s.listen(5) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + [time.sleep(0.1) for x in range(100)] + """) + + udp_template = textwrap.dedent(""" + import socket, time + s = socket.socket({family}, socket.SOCK_DGRAM) + s.bind(('{addr}', 0)) + with open('{testfn}', 'w') as f: + f.write(str(s.getsockname()[:2])) + [time.sleep(0.1) for x in range(100)] + """) + + # must be relative on Windows + testfile = os.path.basename(self.get_testfn(dir=os.getcwd())) + tcp4_template = tcp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + udp4_template = udp_template.format( + family=int(AF_INET), addr="127.0.0.1", testfn=testfile + ) + tcp6_template = tcp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + udp6_template = udp_template.format( + family=int(AF_INET6), addr="::1", testfn=testfile + ) + + # launch various subprocess instantiating a socket of various + # families and types to enrich psutil results + tcp4_proc = self.pyrun(tcp4_template) + tcp4_addr = eval(wait_for_file(testfile, delete=True)) + udp4_proc = self.pyrun(udp4_template) + udp4_addr = eval(wait_for_file(testfile, delete=True)) + if supports_ipv6(): + tcp6_proc = self.pyrun(tcp6_template) + tcp6_addr = eval(wait_for_file(testfile, delete=True)) + udp6_proc = self.pyrun(udp6_template) + udp6_addr = eval(wait_for_file(testfile, delete=True)) + else: + tcp6_proc = None + udp6_proc = None + tcp6_addr = None + udp6_addr = None + + for p in psutil.Process().children(): + cons = p.net_connections() + assert len(cons) == 1 + for conn in cons: + # TCP v4 + if p.pid == tcp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_STREAM, + tcp4_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet4", "tcp", "tcp4"), + ) + # UDP v4 + elif p.pid == udp4_proc.pid: + check_conn( + p, + conn, + AF_INET, + SOCK_DGRAM, + udp4_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet4", "udp", "udp4"), + ) + # TCP v6 + elif p.pid == getattr(tcp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_STREAM, + tcp6_addr, + (), + psutil.CONN_LISTEN, + ("all", "inet", "inet6", "tcp", "tcp6"), + ) + # UDP v6 + elif p.pid == getattr(udp6_proc, "pid", None): + check_conn( + p, + conn, + AF_INET6, + SOCK_DGRAM, + udp6_addr, + (), + psutil.CONN_NONE, + ("all", "inet", "inet6", "udp", "udp6"), + ) + + def test_count(self): + with create_sockets(): + # tcp + cons = this_proc_net_connections(kind='tcp') + assert len(cons) == (2 if supports_ipv6() else 1) + for conn in cons: + assert conn.family in {AF_INET, AF_INET6} + assert conn.type == SOCK_STREAM + # tcp4 + cons = this_proc_net_connections(kind='tcp4') + assert len(cons) == 1 + assert cons[0].family == AF_INET + assert cons[0].type == SOCK_STREAM + # tcp6 + if supports_ipv6(): + cons = this_proc_net_connections(kind='tcp6') + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_STREAM + # udp + cons = this_proc_net_connections(kind='udp') + assert len(cons) == (2 if supports_ipv6() else 1) + for conn in cons: + assert conn.family in {AF_INET, AF_INET6} + assert conn.type == SOCK_DGRAM + # udp4 + cons = this_proc_net_connections(kind='udp4') + assert len(cons) == 1 + assert cons[0].family == AF_INET + assert cons[0].type == SOCK_DGRAM + # udp6 + if supports_ipv6(): + cons = this_proc_net_connections(kind='udp6') + assert len(cons) == 1 + assert cons[0].family == AF_INET6 + assert cons[0].type == SOCK_DGRAM + # inet + cons = this_proc_net_connections(kind='inet') + assert len(cons) == (4 if supports_ipv6() else 2) + for conn in cons: + assert conn.family in {AF_INET, AF_INET6} + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} + # inet6 + if supports_ipv6(): + cons = this_proc_net_connections(kind='inet6') + assert len(cons) == 2 + for conn in cons: + assert conn.family == AF_INET6 + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} + # Skipped on BSD becayse by default the Python process + # creates a UNIX socket to '/var/run/log'. + if HAS_NET_CONNECTIONS_UNIX and not (FREEBSD or NETBSD): + cons = this_proc_net_connections(kind='unix') + assert len(cons) == 3 + for conn in cons: + assert conn.family == AF_UNIX + assert conn.type in {SOCK_STREAM, SOCK_DGRAM} + + +@pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") +class TestSystemWideConnections(ConnectionTestCase): + """Tests for net_connections().""" + + def test_it(self): + def check(cons, families, types_): + for conn in cons: + assert conn.family in families + if conn.family != AF_UNIX: + assert conn.type in types_ + check_connection_ntuple(conn) + + with create_sockets(): + from psutil._common import conn_tmap + + for kind, groups in conn_tmap.items(): + # XXX: SunOS does not retrieve UNIX sockets. + if kind == 'unix' and not HAS_NET_CONNECTIONS_UNIX: + continue + families, types_ = groups + cons = psutil.net_connections(kind) + assert len(cons) == len(set(cons)) + check(cons, families, types_) + + @retry_on_failure() + def test_multi_sockets_procs(self): + # Creates multiple sub processes, each creating different + # sockets. For each process check that proc.net_connections() + # and psutil.net_connections() return the same results. + # This is done mainly to check whether net_connections()'s + # pid is properly set, see: + # https://github.com/giampaolo/psutil/issues/1013 + with create_sockets() as socks: + expected = len(socks) + pids = [] + times = 10 + fnames = [] + for _ in range(times): + fname = self.get_testfn() + fnames.append(fname) + src = textwrap.dedent(f"""\ + import time, os + from psutil.tests import create_sockets + with create_sockets(): + with open(r'{fname}', 'w') as f: + f.write("hello") + [time.sleep(0.1) for x in range(100)] + """) + sproc = self.pyrun(src) + pids.append(sproc.pid) + + # sync + for fname in fnames: + wait_for_file(fname) + + syscons = [ + x for x in psutil.net_connections(kind='all') if x.pid in pids + ] + for pid in pids: + assert len([x for x in syscons if x.pid == pid]) == expected + p = psutil.Process(pid) + assert len(p.net_connections('all')) == expected + + +class TestMisc(PsutilTestCase): + def test_net_connection_constants(self): + ints = [] + strs = [] + for name in dir(psutil): + if name.startswith('CONN_'): + num = getattr(psutil, name) + str_ = str(num) + assert str_.isupper(), str_ + assert str not in strs + assert num not in ints + ints.append(num) + strs.append(str_) + if SUNOS: + psutil.CONN_IDLE # noqa: B018 + psutil.CONN_BOUND # noqa: B018 + if WINDOWS: + psutil.CONN_DELETE_TCB # noqa: B018 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_contracts.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_contracts.py new file mode 100644 index 0000000000000000000000000000000000000000..55f3a5ddb82beb9363e92eb4a2d61f3fac9ea329 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_contracts.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Contracts tests. These tests mainly check API sanity in terms of +returned types and APIs availability. +Some of these are duplicates of tests test_system.py and test_process.py. +""" + +import platform +import signal + +import psutil +from psutil import AIX +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import SKIP_SYSCONS +from psutil.tests import PsutilTestCase +from psutil.tests import create_sockets +from psutil.tests import enum +from psutil.tests import is_namedtuple +from psutil.tests import kernel_version +from psutil.tests import pytest + + +# =================================================================== +# --- APIs availability +# =================================================================== + +# Make sure code reflects what doc promises in terms of APIs +# availability. + + +class TestAvailConstantsAPIs(PsutilTestCase): + def test_PROCFS_PATH(self): + assert hasattr(psutil, "PROCFS_PATH") == (LINUX or SUNOS or AIX) + + def test_win_priority(self): + ae = self.assertEqual + ae(hasattr(psutil, "ABOVE_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "BELOW_NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "HIGH_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "IDLE_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "NORMAL_PRIORITY_CLASS"), WINDOWS) + ae(hasattr(psutil, "REALTIME_PRIORITY_CLASS"), WINDOWS) + + def test_linux_ioprio_linux(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_CLASS_NONE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_RT"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_BE"), LINUX) + ae(hasattr(psutil, "IOPRIO_CLASS_IDLE"), LINUX) + + def test_linux_ioprio_windows(self): + ae = self.assertEqual + ae(hasattr(psutil, "IOPRIO_HIGH"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_NORMAL"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_LOW"), WINDOWS) + ae(hasattr(psutil, "IOPRIO_VERYLOW"), WINDOWS) + + @pytest.mark.skipif( + GITHUB_ACTIONS and LINUX, + reason="unsupported on GITHUB_ACTIONS + LINUX", + ) + def test_rlimit(self): + ae = self.assertEqual + ae(hasattr(psutil, "RLIM_INFINITY"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_AS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CORE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_CPU"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_DATA"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_FSIZE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_MEMLOCK"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NOFILE"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPROC"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_RSS"), LINUX or FREEBSD) + ae(hasattr(psutil, "RLIMIT_STACK"), LINUX or FREEBSD) + + ae(hasattr(psutil, "RLIMIT_LOCKS"), LINUX) + if POSIX: + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_MSGQUEUE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_NICE"), LINUX) + if kernel_version() >= (2, 6, 12): + ae(hasattr(psutil, "RLIMIT_RTPRIO"), LINUX) + if kernel_version() >= (2, 6, 25): + ae(hasattr(psutil, "RLIMIT_RTTIME"), LINUX) + if kernel_version() >= (2, 6, 8): + ae(hasattr(psutil, "RLIMIT_SIGPENDING"), LINUX) + + ae(hasattr(psutil, "RLIMIT_SWAP"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_SBSIZE"), FREEBSD) + ae(hasattr(psutil, "RLIMIT_NPTS"), FREEBSD) + + +class TestAvailSystemAPIs(PsutilTestCase): + def test_win_service_iter(self): + assert hasattr(psutil, "win_service_iter") == WINDOWS + + def test_win_service_get(self): + assert hasattr(psutil, "win_service_get") == WINDOWS + + def test_cpu_freq(self): + assert hasattr(psutil, "cpu_freq") == ( + LINUX or MACOS or WINDOWS or FREEBSD or OPENBSD + ) + + def test_sensors_temperatures(self): + assert hasattr(psutil, "sensors_temperatures") == (LINUX or FREEBSD) + + def test_sensors_fans(self): + assert hasattr(psutil, "sensors_fans") == LINUX + + def test_battery(self): + assert hasattr(psutil, "sensors_battery") == ( + LINUX or WINDOWS or FREEBSD or MACOS + ) + + +class TestAvailProcessAPIs(PsutilTestCase): + def test_environ(self): + assert hasattr(psutil.Process, "environ") == ( + LINUX + or MACOS + or WINDOWS + or AIX + or SUNOS + or FREEBSD + or OPENBSD + or NETBSD + ) + + def test_uids(self): + assert hasattr(psutil.Process, "uids") == POSIX + + def test_gids(self): + assert hasattr(psutil.Process, "uids") == POSIX + + def test_terminal(self): + assert hasattr(psutil.Process, "terminal") == POSIX + + def test_ionice(self): + assert hasattr(psutil.Process, "ionice") == (LINUX or WINDOWS) + + @pytest.mark.skipif( + GITHUB_ACTIONS and LINUX, + reason="unsupported on GITHUB_ACTIONS + LINUX", + ) + def test_rlimit(self): + assert hasattr(psutil.Process, "rlimit") == (LINUX or FREEBSD) + + def test_io_counters(self): + hasit = hasattr(psutil.Process, "io_counters") + assert hasit == (not (MACOS or SUNOS)) + + def test_num_fds(self): + assert hasattr(psutil.Process, "num_fds") == POSIX + + def test_num_handles(self): + assert hasattr(psutil.Process, "num_handles") == WINDOWS + + def test_cpu_affinity(self): + assert hasattr(psutil.Process, "cpu_affinity") == ( + LINUX or WINDOWS or FREEBSD + ) + + def test_cpu_num(self): + assert hasattr(psutil.Process, "cpu_num") == ( + LINUX or FREEBSD or SUNOS + ) + + def test_memory_maps(self): + hasit = hasattr(psutil.Process, "memory_maps") + assert hasit == (not (OPENBSD or NETBSD or AIX or MACOS)) + + +# =================================================================== +# --- API types +# =================================================================== + + +class TestSystemAPITypes(PsutilTestCase): + """Check the return types of system related APIs. + https://github.com/giampaolo/psutil/issues/1039. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def assert_ntuple_of_nums(self, nt, type_=float, gezero=True): + assert is_namedtuple(nt) + for n in nt: + assert isinstance(n, type_) + if gezero: + assert n >= 0 + + def test_cpu_times(self): + self.assert_ntuple_of_nums(psutil.cpu_times()) + for nt in psutil.cpu_times(percpu=True): + self.assert_ntuple_of_nums(nt) + + def test_cpu_percent(self): + assert isinstance(psutil.cpu_percent(interval=None), float) + assert isinstance(psutil.cpu_percent(interval=0.00001), float) + + def test_cpu_times_percent(self): + self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=None)) + self.assert_ntuple_of_nums(psutil.cpu_times_percent(interval=0.0001)) + + def test_cpu_count(self): + assert isinstance(psutil.cpu_count(), int) + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + if psutil.cpu_freq() is None: + raise pytest.skip("cpu_freq() returns None") + self.assert_ntuple_of_nums(psutil.cpu_freq(), type_=(float, int)) + + def test_disk_io_counters(self): + # Duplicate of test_system.py. Keep it anyway. + for k, v in psutil.disk_io_counters(perdisk=True).items(): + assert isinstance(k, str) + self.assert_ntuple_of_nums(v, type_=int) + + def test_disk_partitions(self): + # Duplicate of test_system.py. Keep it anyway. + for disk in psutil.disk_partitions(): + assert isinstance(disk.device, str) + assert isinstance(disk.mountpoint, str) + assert isinstance(disk.fstype, str) + assert isinstance(disk.opts, str) + + @pytest.mark.skipif(SKIP_SYSCONS, reason="requires root") + def test_net_connections(self): + with create_sockets(): + ret = psutil.net_connections('all') + assert len(ret) == len(set(ret)) + for conn in ret: + assert is_namedtuple(conn) + + def test_net_if_addrs(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname, addrs in psutil.net_if_addrs().items(): + assert isinstance(ifname, str) + for addr in addrs: + assert isinstance(addr.family, enum.IntEnum) + assert isinstance(addr.address, str) + assert isinstance(addr.netmask, (str, type(None))) + assert isinstance(addr.broadcast, (str, type(None))) + + def test_net_if_stats(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname, info in psutil.net_if_stats().items(): + assert isinstance(ifname, str) + assert isinstance(info.isup, bool) + assert isinstance(info.duplex, enum.IntEnum) + assert isinstance(info.speed, int) + assert isinstance(info.mtu, int) + + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters(self): + # Duplicate of test_system.py. Keep it anyway. + for ifname in psutil.net_io_counters(pernic=True): + assert isinstance(ifname, str) + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + # Duplicate of test_system.py. Keep it anyway. + for name, units in psutil.sensors_fans().items(): + assert isinstance(name, str) + for unit in units: + assert isinstance(unit.label, str) + assert isinstance(unit.current, (float, int, type(None))) + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures(self): + # Duplicate of test_system.py. Keep it anyway. + for name, units in psutil.sensors_temperatures().items(): + assert isinstance(name, str) + for unit in units: + assert isinstance(unit.label, str) + assert isinstance(unit.current, (float, int, type(None))) + assert isinstance(unit.high, (float, int, type(None))) + assert isinstance(unit.critical, (float, int, type(None))) + + def test_boot_time(self): + # Duplicate of test_system.py. Keep it anyway. + assert isinstance(psutil.boot_time(), float) + + def test_users(self): + # Duplicate of test_system.py. Keep it anyway. + for user in psutil.users(): + assert isinstance(user.name, str) + assert isinstance(user.terminal, (str, type(None))) + assert isinstance(user.host, (str, type(None))) + assert isinstance(user.pid, (int, type(None))) + + +class TestProcessWaitType(PsutilTestCase): + @pytest.mark.skipif(not POSIX, reason="not POSIX") + def test_negative_signal(self): + p = psutil.Process(self.spawn_testproc().pid) + p.terminate() + code = p.wait() + assert code == -signal.SIGTERM + assert isinstance(code, enum.IntEnum) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_linux.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_linux.py new file mode 100644 index 0000000000000000000000000000000000000000..f4342d7aa34a74168e74f6fcb306d5b59ea638b6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_linux.py @@ -0,0 +1,2292 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Linux specific tests.""" + + +import collections +import contextlib +import errno +import io +import os +import platform +import re +import shutil +import socket +import struct +import textwrap +import time +import warnings +from unittest import mock + +import psutil +from psutil import LINUX +from psutil.tests import AARCH64 +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_RLIMIT +from psutil.tests import PYPY +from psutil.tests import PYTEST_PARALLEL +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import pytest +from psutil.tests import reload_module +from psutil.tests import retry_on_failure +from psutil.tests import safe_rmpath +from psutil.tests import sh +from psutil.tests import skip_on_not_implemented + + +if LINUX: + from psutil._pslinux import CLOCK_TICKS + from psutil._pslinux import RootFsDeviceFinder + from psutil._pslinux import calculate_avail_vmem + from psutil._pslinux import open_binary + + +HERE = os.path.abspath(os.path.dirname(__file__)) +SIOCGIFADDR = 0x8915 +SIOCGIFHWADDR = 0x8927 +SIOCGIFNETMASK = 0x891B +SIOCGIFBRDADDR = 0x8919 +if LINUX: + SECTOR_SIZE = 512 +# ===================================================================== +# --- utils +# ===================================================================== + + +def get_ipv4_address(ifname): + import fcntl + + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + return socket.inet_ntoa( + fcntl.ioctl(s.fileno(), SIOCGIFADDR, struct.pack('256s', ifname))[ + 20:24 + ] + ) + + +def get_ipv4_netmask(ifname): + import fcntl + + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + return socket.inet_ntoa( + fcntl.ioctl( + s.fileno(), SIOCGIFNETMASK, struct.pack('256s', ifname) + )[20:24] + ) + + +def get_ipv4_broadcast(ifname): + import fcntl + + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + return socket.inet_ntoa( + fcntl.ioctl( + s.fileno(), SIOCGIFBRDADDR, struct.pack('256s', ifname) + )[20:24] + ) + + +def get_ipv6_addresses(ifname): + with open("/proc/net/if_inet6") as f: + all_fields = [] + for line in f: + fields = line.split() + if fields[-1] == ifname: + all_fields.append(fields) + + if len(all_fields) == 0: + raise ValueError(f"could not find interface {ifname!r}") + + for i in range(len(all_fields)): + unformatted = all_fields[i][0] + groups = [ + unformatted[j : j + 4] for j in range(0, len(unformatted), 4) + ] + formatted = ":".join(groups) + packed = socket.inet_pton(socket.AF_INET6, formatted) + all_fields[i] = socket.inet_ntop(socket.AF_INET6, packed) + return all_fields + + +def get_mac_address(ifname): + import fcntl + + ifname = bytes(ifname[:15], "ascii") + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + info = fcntl.ioctl( + s.fileno(), SIOCGIFHWADDR, struct.pack('256s', ifname) + ) + return "".join([f"{char:02x}:" for char in info[18:24]])[:-1] + + +def free_swap(): + """Parse 'free' cmd and return swap memory's s total, used and free + values. + """ + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Swap'): + _, total, used, free = line.split() + nt = collections.namedtuple('free', 'total used free') + return nt(int(total), int(used), int(free)) + raise ValueError(f"can't find 'Swap' in 'free' output:\n{out}") + + +def free_physmem(): + """Parse 'free' cmd and return physical memory's total, used + and free values. + """ + # Note: free can have 2 different formats, invalidating 'shared' + # and 'cached' memory which may have different positions so we + # do not return them. + # https://github.com/giampaolo/psutil/issues/538#issuecomment-57059946 + out = sh(["free", "-b"], env={"LANG": "C.UTF-8"}) + lines = out.split('\n') + for line in lines: + if line.startswith('Mem'): + total, used, free, shared = (int(x) for x in line.split()[1:5]) + nt = collections.namedtuple( + 'free', 'total used free shared output' + ) + return nt(total, used, free, shared, out) + raise ValueError(f"can't find 'Mem' in 'free' output:\n{out}") + + +def vmstat(stat): + out = sh(["vmstat", "-s"], env={"LANG": "C.UTF-8"}) + for line in out.split("\n"): + line = line.strip() + if stat in line: + return int(line.split(' ')[0]) + raise ValueError(f"can't find {stat!r} in 'vmstat' output") + + +def get_free_version_info(): + out = sh(["free", "-V"]).strip() + if 'UNKNOWN' in out: + raise pytest.skip("can't determine free version") + return tuple(map(int, re.findall(r'\d+', out.split()[-1]))) + + +@contextlib.contextmanager +def mock_open_content(pairs): + """Mock open() builtin and forces it to return a certain content + for a given path. `pairs` is a {"path": "content", ...} dict. + """ + + def open_mock(name, *args, **kwargs): + if name in pairs: + content = pairs[name] + if isinstance(content, str): + return io.StringIO(content) + else: + return io.BytesIO(content) + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: + yield m + + +@contextlib.contextmanager +def mock_open_exception(for_path, exc): + """Mock open() builtin and raises `exc` if the path being opened + matches `for_path`. + """ + + def open_mock(name, *args, **kwargs): + if name == for_path: + raise exc + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", create=True, side_effect=open_mock) as m: + yield m + + +# ===================================================================== +# --- system virtual memory +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryAgainstFree(PsutilTestCase): + def test_total(self): + cli_value = free_physmem().total + psutil_value = psutil.virtual_memory().total + assert cli_value == psutil_value + + @retry_on_failure() + def test_used(self): + # Older versions of procps used slab memory to calculate used memory. + # This got changed in: + # https://gitlab.com/procps-ng/procps/commit/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + if get_free_version_info() < (3, 3, 12): + raise pytest.skip("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise pytest.skip("free version too recent") + cli_value = free_physmem().used + psutil_value = psutil.virtual_memory().used + assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_free(self): + cli_value = free_physmem().free + psutil_value = psutil.virtual_memory().free + assert abs(cli_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_shared(self): + free = free_physmem() + free_value = free.shared + if free_value == 0: + raise pytest.skip("free does not support 'shared' column") + psutil_value = psutil.virtual_memory().shared + assert ( + abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + ), f"{free_value} {psutil_value} \n{free.output}" + + @retry_on_failure() + def test_available(self): + # "free" output format has changed at some point: + # https://github.com/giampaolo/psutil/issues/538#issuecomment-147192098 + out = sh(["free", "-b"]) + lines = out.split('\n') + if 'available' not in lines[0]: + raise pytest.skip("free does not support 'available' column") + free_value = int(lines[1].split()[-1]) + psutil_value = psutil.virtual_memory().available + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryAgainstVmstat(PsutilTestCase): + def test_total(self): + vmstat_value = vmstat('total memory') * 1024 + psutil_value = psutil.virtual_memory().total + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_used(self): + # Older versions of procps used slab memory to calculate used memory. + # This got changed in: + # https://gitlab.com/procps-ng/procps/commit/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + # Newer versions of procps are using yet another way to compute used + # memory. + # https://gitlab.com/procps-ng/procps/commit/ + # 2184e90d2e7cdb582f9a5b706b47015e56707e4d + if get_free_version_info() < (3, 3, 12): + raise pytest.skip("free version too old") + if get_free_version_info() >= (4, 0, 0): + raise pytest.skip("free version too recent") + vmstat_value = vmstat('used memory') * 1024 + psutil_value = psutil.virtual_memory().used + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_free(self): + vmstat_value = vmstat('free memory') * 1024 + psutil_value = psutil.virtual_memory().free + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_buffers(self): + vmstat_value = vmstat('buffer memory') * 1024 + psutil_value = psutil.virtual_memory().buffers + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_active(self): + vmstat_value = vmstat('active memory') * 1024 + psutil_value = psutil.virtual_memory().active + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_inactive(self): + vmstat_value = vmstat('inactive memory') * 1024 + psutil_value = psutil.virtual_memory().inactive + assert abs(vmstat_value - psutil_value) < TOLERANCE_SYS_MEM + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemVirtualMemoryMocks(PsutilTestCase): + def test_warnings_on_misses(self): + # Emulate a case where /proc/meminfo provides few info. + # psutil is supposed to set the missing fields to 0 and + # raise a warning. + content = textwrap.dedent("""\ + Active(anon): 6145416 kB + Active(file): 2950064 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: -1 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.virtual_memory() + assert m.called + assert len(ws) == 1 + w = ws[0] + assert "memory stats couldn't be determined" in str(w.message) + assert "cached" in str(w.message) + assert "shared" in str(w.message) + assert "active" in str(w.message) + assert "inactive" in str(w.message) + assert "buffers" in str(w.message) + assert "available" in str(w.message) + assert ret.cached == 0 + assert ret.active == 0 + assert ret.inactive == 0 + assert ret.shared == 0 + assert ret.buffers == 0 + assert ret.available == 0 + assert ret.slab == 0 + + @retry_on_failure() + def test_avail_old_percent(self): + # Make sure that our calculation of avail mem for old kernels + # is off by max 15%. + mems = {} + with open_binary('/proc/meminfo') as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + + a = calculate_avail_vmem(mems) + if b'MemAvailable:' in mems: + b = mems[b'MemAvailable:'] + diff_percent = abs(a - b) / a * 100 + assert diff_percent < 15 + + def test_avail_old_comes_from_kernel(self): + # Make sure "MemAvailable:" coluimn is used instead of relying + # on our internal algorithm to calculate avail mem. + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemAvailable: 6574984 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({'/proc/meminfo': content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + assert ret.available == 6574984 * 1024 + w = ws[0] + assert "inactive memory stats couldn't be determined" in str( + w.message + ) + + def test_avail_old_missing_fields(self): + # Remove Active(file), Inactive(file) and SReclaimable + # from /proc/meminfo and make sure the fallback is used + # (free + cached), + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert m.called + assert ret.available == 2057400 * 1024 + 4818144 * 1024 + w = ws[0] + assert "inactive memory stats couldn't be determined" in str( + w.message + ) + + def test_avail_old_missing_zoneinfo(self): + # Remove /proc/zoneinfo file. Make sure fallback is used + # (free + cached). + content = textwrap.dedent("""\ + Active: 9444728 kB + Active(anon): 6145416 kB + Active(file): 2950064 kB + Buffers: 287952 kB + Cached: 4818144 kB + Inactive(file): 1578132 kB + Inactive(anon): 574764 kB + Inactive(file): 1567648 kB + MemFree: 2057400 kB + MemTotal: 16325648 kB + Shmem: 577588 kB + SReclaimable: 346648 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}): + with mock_open_exception("/proc/zoneinfo", FileNotFoundError): + with warnings.catch_warnings(record=True) as ws: + ret = psutil.virtual_memory() + assert ret.available == 2057400 * 1024 + 4818144 * 1024 + w = ws[0] + assert ( + "inactive memory stats couldn't be determined" + in str(w.message) + ) + + def test_virtual_memory_mocked(self): + # Emulate /proc/meminfo because neither vmstat nor free return slab. + content = textwrap.dedent("""\ + MemTotal: 100 kB + MemFree: 2 kB + MemAvailable: 3 kB + Buffers: 4 kB + Cached: 5 kB + SwapCached: 6 kB + Active: 7 kB + Inactive: 8 kB + Active(anon): 9 kB + Inactive(anon): 10 kB + Active(file): 11 kB + Inactive(file): 12 kB + Unevictable: 13 kB + Mlocked: 14 kB + SwapTotal: 15 kB + SwapFree: 16 kB + Dirty: 17 kB + Writeback: 18 kB + AnonPages: 19 kB + Mapped: 20 kB + Shmem: 21 kB + Slab: 22 kB + SReclaimable: 23 kB + SUnreclaim: 24 kB + KernelStack: 25 kB + PageTables: 26 kB + NFS_Unstable: 27 kB + Bounce: 28 kB + WritebackTmp: 29 kB + CommitLimit: 30 kB + Committed_AS: 31 kB + VmallocTotal: 32 kB + VmallocUsed: 33 kB + VmallocChunk: 34 kB + HardwareCorrupted: 35 kB + AnonHugePages: 36 kB + ShmemHugePages: 37 kB + ShmemPmdMapped: 38 kB + CmaTotal: 39 kB + CmaFree: 40 kB + HugePages_Total: 41 kB + HugePages_Free: 42 kB + HugePages_Rsvd: 43 kB + HugePages_Surp: 44 kB + Hugepagesize: 45 kB + DirectMap46k: 46 kB + DirectMap47M: 47 kB + DirectMap48G: 48 kB + """).encode() + with mock_open_content({"/proc/meminfo": content}) as m: + mem = psutil.virtual_memory() + assert m.called + assert mem.total == 100 * 1024 + assert mem.free == 2 * 1024 + assert mem.buffers == 4 * 1024 + # cached mem also includes reclaimable memory + assert mem.cached == (5 + 23) * 1024 + assert mem.shared == 21 * 1024 + assert mem.active == 7 * 1024 + assert mem.inactive == 8 * 1024 + assert mem.slab == 22 * 1024 + assert mem.available == 3 * 1024 + + +# ===================================================================== +# --- system swap memory +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemSwapMemory(PsutilTestCase): + @staticmethod + def meminfo_has_swap_info(): + """Return True if /proc/meminfo provides swap metrics.""" + with open("/proc/meminfo") as f: + data = f.read() + return 'SwapTotal:' in data and 'SwapFree:' in data + + def test_total(self): + free_value = free_swap().total + psutil_value = psutil.swap_memory().total + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_used(self): + free_value = free_swap().used + psutil_value = psutil.swap_memory().used + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_free(self): + free_value = free_swap().free + psutil_value = psutil.swap_memory().free + assert abs(free_value - psutil_value) < TOLERANCE_SYS_MEM + + def test_missing_sin_sout(self): + with mock.patch('psutil._common.open', create=True) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + assert len(ws) == 1 + w = ws[0] + assert ( + "'sin' and 'sout' swap memory stats couldn't be determined" + in str(w.message) + ) + assert ret.sin == 0 + assert ret.sout == 0 + + def test_no_vmstat_mocked(self): + # see https://github.com/giampaolo/psutil/issues/722 + with mock_open_exception("/proc/vmstat", FileNotFoundError) as m: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + ret = psutil.swap_memory() + assert m.called + assert len(ws) == 1 + w = ws[0] + assert ( + "'sin' and 'sout' swap memory stats couldn't " + "be determined and were set to 0" + in str(w.message) + ) + assert ret.sin == 0 + assert ret.sout == 0 + + def test_meminfo_against_sysinfo(self): + # Make sure the content of /proc/meminfo about swap memory + # matches sysinfo() syscall, see: + # https://github.com/giampaolo/psutil/issues/1015 + if not self.meminfo_has_swap_info(): + raise pytest.skip("/proc/meminfo has no swap metrics") + with mock.patch('psutil._pslinux.cext.linux_sysinfo') as m: + swap = psutil.swap_memory() + assert not m.called + import psutil._psutil_linux as cext + + _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() + total *= unit_multiplier + free *= unit_multiplier + assert swap.total == total + assert abs(swap.free - free) < TOLERANCE_SYS_MEM + + def test_emulate_meminfo_has_no_metrics(self): + # Emulate a case where /proc/meminfo provides no swap metrics + # in which case sysinfo() syscall is supposed to be used + # as a fallback. + with mock_open_content({"/proc/meminfo": b""}) as m: + psutil.swap_memory() + assert m.called + + +# ===================================================================== +# --- system CPU +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUTimes(PsutilTestCase): + def test_fields(self): + fields = psutil.cpu_times()._fields + kernel_ver = re.findall(r'\d+\.\d+\.\d+', os.uname()[2])[0] + kernel_ver_info = tuple(map(int, kernel_ver.split('.'))) + if kernel_ver_info >= (2, 6, 11): + assert 'steal' in fields + else: + assert 'steal' not in fields + if kernel_ver_info >= (2, 6, 24): + assert 'guest' in fields + else: + assert 'guest' not in fields + if kernel_ver_info >= (3, 2, 0): + assert 'guest_nice' in fields + else: + assert 'guest_nice' not in fields + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUCountLogical(PsutilTestCase): + @pytest.mark.skipif( + not os.path.exists("/sys/devices/system/cpu/online"), + reason="/sys/devices/system/cpu/online does not exist", + ) + def test_against_sysdev_cpu_online(self): + with open("/sys/devices/system/cpu/online") as f: + value = f.read().strip() + if "-" in str(value): + value = int(value.split('-')[1]) + 1 + assert psutil.cpu_count() == value + + @pytest.mark.skipif( + not os.path.exists("/sys/devices/system/cpu"), + reason="/sys/devices/system/cpu does not exist", + ) + def test_against_sysdev_cpu_num(self): + ls = os.listdir("/sys/devices/system/cpu") + count = len([x for x in ls if re.search(r"cpu\d+$", x) is not None]) + assert psutil.cpu_count() == count + + @pytest.mark.skipif( + not shutil.which("nproc"), reason="nproc utility not available" + ) + def test_against_nproc(self): + num = int(sh("nproc --all")) + assert psutil.cpu_count(logical=True) == num + + @pytest.mark.skipif( + not shutil.which("lscpu"), reason="lscpu utility not available" + ) + def test_against_lscpu(self): + out = sh("lscpu -p") + num = len([x for x in out.split('\n') if not x.startswith('#')]) + assert psutil.cpu_count(logical=True) == num + + def test_emulate_fallbacks(self): + import psutil._pslinux + + original = psutil._pslinux.cpu_count_logical() + # Here we want to mock os.sysconf("SC_NPROCESSORS_ONLN") in + # order to cause the parsing of /proc/cpuinfo and /proc/stat. + with mock.patch( + 'psutil._pslinux.os.sysconf', side_effect=ValueError + ) as m: + assert psutil._pslinux.cpu_count_logical() == original + assert m.called + + # Let's have open() return empty data and make sure None is + # returned ('cause we mimic os.cpu_count()). + with mock.patch('psutil._common.open', create=True) as m: + assert psutil._pslinux.cpu_count_logical() is None + assert m.call_count == 2 + # /proc/stat should be the last one + assert m.call_args[0][0] == '/proc/stat' + + # Let's push this a bit further and make sure /proc/cpuinfo + # parsing works as expected. + with open('/proc/cpuinfo', 'rb') as f: + cpuinfo_data = f.read() + fake_file = io.BytesIO(cpuinfo_data) + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert psutil._pslinux.cpu_count_logical() == original + + # Finally, let's make /proc/cpuinfo return meaningless data; + # this way we'll fall back on relying on /proc/stat + with mock_open_content({"/proc/cpuinfo": b""}) as m: + assert psutil._pslinux.cpu_count_logical() == original + assert m.called + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUCountCores(PsutilTestCase): + @pytest.mark.skipif( + not shutil.which("lscpu"), reason="lscpu utility not available" + ) + def test_against_lscpu(self): + out = sh("lscpu -p") + core_ids = set() + for line in out.split('\n'): + if not line.startswith('#'): + fields = line.split(',') + core_ids.add(fields[1]) + assert psutil.cpu_count(logical=False) == len(core_ids) + + @pytest.mark.skipif( + platform.machine() not in {"x86_64", "i686"}, reason="x86_64/i686 only" + ) + def test_method_2(self): + meth_1 = psutil._pslinux.cpu_count_cores() + with mock.patch('glob.glob', return_value=[]) as m: + meth_2 = psutil._pslinux.cpu_count_cores() + assert m.called + if meth_1 is not None: + assert meth_1 == meth_2 + + def test_emulate_none(self): + with mock.patch('glob.glob', return_value=[]) as m1: + with mock.patch('psutil._common.open', create=True) as m2: + assert psutil._pslinux.cpu_count_cores() is None + assert m1.called + assert m2.called + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUFrequency(PsutilTestCase): + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + AARCH64, reason="aarch64 does not always expose frequency" + ) + def test_emulate_use_second_file(self): + # https://github.com/giampaolo/psutil/issues/981 + def path_exists_mock(path): + if path.startswith("/sys/devices/system/cpu/cpufreq/policy"): + return False + else: + return orig_exists(path) + + orig_exists = os.path.exists + with mock.patch( + "os.path.exists", side_effect=path_exists_mock, create=True + ): + assert psutil.cpu_freq() + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + @pytest.mark.skipif( + AARCH64, reason="aarch64 does not report mhz in /proc/cpuinfo" + ) + def test_emulate_use_cpuinfo(self): + # Emulate a case where /sys/devices/system/cpu/cpufreq* does not + # exist and /proc/cpuinfo is used instead. + def path_exists_mock(path): + if path.startswith('/sys/devices/system/cpu/'): + return False + else: + return os_path_exists(path) + + os_path_exists = os.path.exists + try: + with mock.patch("os.path.exists", side_effect=path_exists_mock): + reload_module(psutil._pslinux) + ret = psutil.cpu_freq() + assert ret, ret + assert ret.max == 0.0 + assert ret.min == 0.0 + for freq in psutil.cpu_freq(percpu=True): + assert freq.max == 0.0 + assert freq.min == 0.0 + finally: + reload_module(psutil._pslinux) + reload_module(psutil) + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_emulate_data(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/scaling_cur_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"500000") + elif name.endswith('/scaling_min_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"600000") + elif name.endswith('/scaling_max_freq') and name.startswith( + "/sys/devices/system/cpu/cpufreq/policy" + ): + return io.BytesIO(b"700000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 500") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + freq = psutil.cpu_freq() + assert freq.current == 500.0 + # when /proc/cpuinfo is used min and max frequencies are not + # available and are set to 0. + if freq.min != 0.0: + assert freq.min == 600.0 + if freq.max != 0.0: + assert freq.max == 700.0 + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_emulate_multi_cpu(self): + def open_mock(name, *args, **kwargs): + n = name + if n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"100000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"200000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy0" + ): + return io.BytesIO(b"300000") + elif n.endswith('/scaling_cur_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"400000") + elif n.endswith('/scaling_min_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"500000") + elif n.endswith('/scaling_max_freq') and n.startswith( + "/sys/devices/system/cpu/cpufreq/policy1" + ): + return io.BytesIO(b"600000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 100\ncpu MHz : 400") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + with mock.patch( + 'psutil._pslinux.cpu_count_logical', return_value=2 + ): + freq = psutil.cpu_freq(percpu=True) + assert freq[0].current == 100.0 + if freq[0].min != 0.0: + assert freq[0].min == 200.0 + if freq[0].max != 0.0: + assert freq[0].max == 300.0 + assert freq[1].current == 400.0 + if freq[1].min != 0.0: + assert freq[1].min == 500.0 + if freq[1].max != 0.0: + assert freq[1].max == 600.0 + + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_emulate_no_scaling_cur_freq_file(self): + # See: https://github.com/giampaolo/psutil/issues/1071 + def open_mock(name, *args, **kwargs): + if name.endswith('/scaling_cur_freq'): + raise FileNotFoundError + if name.endswith('/cpuinfo_cur_freq'): + return io.BytesIO(b"200000") + elif name == '/proc/cpuinfo': + return io.BytesIO(b"cpu MHz : 200") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch('os.path.exists', return_value=True): + with mock.patch( + 'psutil._pslinux.cpu_count_logical', return_value=1 + ): + freq = psutil.cpu_freq() + assert freq.current == 200 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemCPUStats(PsutilTestCase): + + # XXX: fails too often. + # def test_ctx_switches(self): + # vmstat_value = vmstat("context switches") + # psutil_value = psutil.cpu_stats().ctx_switches + # self.assertAlmostEqual(vmstat_value, psutil_value, delta=500) + + def test_interrupts(self): + vmstat_value = vmstat("interrupts") + psutil_value = psutil.cpu_stats().interrupts + assert abs(vmstat_value - psutil_value) < 500 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestLoadAvg(PsutilTestCase): + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") + def test_getloadavg(self): + psutil_value = psutil.getloadavg() + with open("/proc/loadavg") as f: + proc_value = f.read().split() + + assert abs(float(proc_value[0]) - psutil_value[0]) < 1 + assert abs(float(proc_value[1]) - psutil_value[1]) < 1 + assert abs(float(proc_value[2]) - psutil_value[2]) < 1 + + +# ===================================================================== +# --- system network +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIfAddrs(PsutilTestCase): + def test_ips(self): + for name, addrs in psutil.net_if_addrs().items(): + for addr in addrs: + if addr.family == psutil.AF_LINK: + assert addr.address == get_mac_address(name) + elif addr.family == socket.AF_INET: + assert addr.address == get_ipv4_address(name) + assert addr.netmask == get_ipv4_netmask(name) + if addr.broadcast is not None: + assert addr.broadcast == get_ipv4_broadcast(name) + else: + assert get_ipv4_broadcast(name) == '0.0.0.0' + elif addr.family == socket.AF_INET6: + # IPv6 addresses can have a percent symbol at the end. + # E.g. these 2 are equivalent: + # "fe80::1ff:fe23:4567:890a" + # "fe80::1ff:fe23:4567:890a%eth0" + # That is the "zone id" portion, which usually is the name + # of the network interface. + address = addr.address.split('%')[0] + assert address in get_ipv6_addresses(name) + + # XXX - not reliable when having virtual NICs installed by Docker. + # @pytest.mark.skipif(not shutil.which("ip"), + # reason="'ip' utility not available") + # def test_net_if_names(self): + # out = sh("ip addr").strip() + # nics = [x for x in psutil.net_if_addrs().keys() if ':' not in x] + # found = 0 + # for line in out.split('\n'): + # line = line.strip() + # if re.search(r"^\d+:", line): + # found += 1 + # name = line.split(':')[1].strip() + # self.assertIn(name, nics) + # self.assertEqual(len(nics), found, msg="{}\n---\n{}".format( + # pprint.pformat(nics), out)) + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIfStats(PsutilTestCase): + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig utility not available" + ) + def test_against_ifconfig(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + assert stats.isup == ('RUNNING' in out), out + assert stats.mtu == int( + re.findall(r'(?i)MTU[: ](\d+)', out)[0] + ) + + def test_mtu(self): + for name, stats in psutil.net_if_stats().items(): + with open(f"/sys/class/net/{name}/mtu") as f: + assert stats.mtu == int(f.read().strip()) + + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig utility not available" + ) + def test_flags(self): + # first line looks like this: + # "eth0: flags=4163 mtu 1500" + matches_found = 0 + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + match = re.search(r"flags=(\d+)?<(.*?)>", out) + if match and len(match.groups()) >= 2: + matches_found += 1 + ifconfig_flags = set(match.group(2).lower().split(",")) + psutil_flags = set(stats.flags.split(",")) + assert ifconfig_flags == psutil_flags + else: + # ifconfig has a different output on CentOS 6 + # let's try that + match = re.search(r"(.*) MTU:(\d+) Metric:(\d+)", out) + if match and len(match.groups()) >= 3: + matches_found += 1 + ifconfig_flags = set(match.group(1).lower().split()) + psutil_flags = set(stats.flags.split(",")) + assert ifconfig_flags == psutil_flags + + if not matches_found: + raise self.fail("no matches were found") + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetIOCounters(PsutilTestCase): + @pytest.mark.skipif( + not shutil.which("ifconfig"), reason="ifconfig utility not available" + ) + @retry_on_failure() + def test_against_ifconfig(self): + def ifconfig(nic): + ret = {} + out = sh(f"ifconfig {nic}") + ret['packets_recv'] = int( + re.findall(r'RX packets[: ](\d+)', out)[0] + ) + ret['packets_sent'] = int( + re.findall(r'TX packets[: ](\d+)', out)[0] + ) + ret['errin'] = int(re.findall(r'errors[: ](\d+)', out)[0]) + ret['errout'] = int(re.findall(r'errors[: ](\d+)', out)[1]) + ret['dropin'] = int(re.findall(r'dropped[: ](\d+)', out)[0]) + ret['dropout'] = int(re.findall(r'dropped[: ](\d+)', out)[1]) + ret['bytes_recv'] = int( + re.findall(r'RX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + ret['bytes_sent'] = int( + re.findall(r'TX (?:packets \d+ +)?bytes[: ](\d+)', out)[0] + ) + return ret + + nio = psutil.net_io_counters(pernic=True, nowrap=False) + for name, stats in nio.items(): + try: + ifconfig_ret = ifconfig(name) + except RuntimeError: + continue + assert ( + abs(stats.bytes_recv - ifconfig_ret['bytes_recv']) < 1024 * 10 + ) + assert ( + abs(stats.bytes_sent - ifconfig_ret['bytes_sent']) < 1024 * 10 + ) + assert ( + abs(stats.packets_recv - ifconfig_ret['packets_recv']) < 1024 + ) + assert ( + abs(stats.packets_sent - ifconfig_ret['packets_sent']) < 1024 + ) + assert abs(stats.errin - ifconfig_ret['errin']) < 10 + assert abs(stats.errout - ifconfig_ret['errout']) < 10 + assert abs(stats.dropin - ifconfig_ret['dropin']) < 10 + assert abs(stats.dropout - ifconfig_ret['dropout']) < 10 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemNetConnections(PsutilTestCase): + @mock.patch('psutil._pslinux.socket.inet_ntop', side_effect=ValueError) + @mock.patch('psutil._pslinux.supports_ipv6', return_value=False) + def test_emulate_ipv6_unsupported(self, supports_ipv6, inet_ntop): + # see: https://github.com/giampaolo/psutil/issues/623 + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + self.addCleanup(s.close) + s.bind(("::1", 0)) + except OSError: + pass + psutil.net_connections(kind='inet6') + + def test_emulate_unix(self): + content = textwrap.dedent("""\ + 0: 00000003 000 000 0001 03 462170 @/tmp/dbus-Qw2hMPIU3n + 0: 00000003 000 000 0001 03 35010 @/tmp/dbus-tB2X8h69BQ + 0: 00000003 000 000 0001 03 34424 @/tmp/dbus-cHy80Y8O + 000000000000000000000000000000000000000000000000000000 + """) + with mock_open_content({"/proc/net/unix": content}) as m: + psutil.net_connections(kind='unix') + assert m.called + + +# ===================================================================== +# --- system disks +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemDiskPartitions(PsutilTestCase): + @pytest.mark.skipif( + not hasattr(os, 'statvfs'), reason="os.statvfs() not available" + ) + @skip_on_not_implemented() + def test_against_df(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh(f'df -P -B 1 "{path}"').strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total, used, free = int(total), int(used), int(free) + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + _, total, used, free = df(part.mountpoint) + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE + + def test_zfs_fs(self): + # Test that ZFS partitions are returned. + with open("/proc/filesystems") as f: + data = f.read() + if 'zfs' in data: + for part in psutil.disk_partitions(): + if part.fstype == 'zfs': + return + + # No ZFS partitions on this system. Let's fake one. + fake_file = io.StringIO("nodev\tzfs\n") + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m1: + with mock.patch( + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/sdb3', '/', 'zfs', 'rw')], + ) as m2: + ret = psutil.disk_partitions() + assert m1.called + assert m2.called + assert ret + assert ret[0].fstype == 'zfs' + + def test_emulate_realpath_fail(self): + # See: https://github.com/giampaolo/psutil/issues/1307 + try: + with mock.patch( + 'os.path.realpath', return_value='/non/existent' + ) as m: + with pytest.raises(FileNotFoundError): + psutil.disk_partitions() + assert m.called + finally: + psutil.PROCFS_PATH = "/proc" + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSystemDiskIoCounters(PsutilTestCase): + def test_emulate_kernel_2_4(self): + # Tests /proc/diskstats parsing format for 2.4 kernels, see: + # https://github.com/giampaolo/psutil/issues/767 + content = " 3 0 1 hda 2 3 4 5 6 7 8 9 10 11 12" + with mock_open_content({'/proc/diskstats': content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + assert ret.read_count == 1 + assert ret.read_merged_count == 2 + assert ret.read_bytes == 3 * SECTOR_SIZE + assert ret.read_time == 4 + assert ret.write_count == 5 + assert ret.write_merged_count == 6 + assert ret.write_bytes == 7 * SECTOR_SIZE + assert ret.write_time == 8 + assert ret.busy_time == 10 + + def test_emulate_kernel_2_6_full(self): + # Tests /proc/diskstats parsing format for 2.6 kernels, + # lines reporting all metrics: + # https://github.com/giampaolo/psutil/issues/767 + content = " 3 0 hda 1 2 3 4 5 6 7 8 9 10 11" + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + assert ret.read_count == 1 + assert ret.read_merged_count == 2 + assert ret.read_bytes == 3 * SECTOR_SIZE + assert ret.read_time == 4 + assert ret.write_count == 5 + assert ret.write_merged_count == 6 + assert ret.write_bytes == 7 * SECTOR_SIZE + assert ret.write_time == 8 + assert ret.busy_time == 10 + + def test_emulate_kernel_2_6_limited(self): + # Tests /proc/diskstats parsing format for 2.6 kernels, + # where one line of /proc/partitions return a limited + # amount of metrics when it bumps into a partition + # (instead of a disk). See: + # https://github.com/giampaolo/psutil/issues/767 + with mock_open_content({"/proc/diskstats": " 3 1 hda 1 2 3 4"}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=True + ): + ret = psutil.disk_io_counters(nowrap=False) + assert ret.read_count == 1 + assert ret.read_bytes == 2 * SECTOR_SIZE + assert ret.write_count == 3 + assert ret.write_bytes == 4 * SECTOR_SIZE + + assert ret.read_merged_count == 0 + assert ret.read_time == 0 + assert ret.write_merged_count == 0 + assert ret.write_time == 0 + assert ret.busy_time == 0 + + def test_emulate_include_partitions(self): + # Make sure that when perdisk=True disk partitions are returned, + # see: + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=True, nowrap=False) + assert len(ret) == 2 + assert ret['nvme0n1'].read_count == 1 + assert ret['nvme0n1p1'].read_count == 1 + assert ret['nvme0n1'].write_count == 5 + assert ret['nvme0n1p1'].write_count == 5 + + def test_emulate_exclude_partitions(self): + # Make sure that when perdisk=False partitions (e.g. 'sda1', + # 'nvme0n1p1') are skipped and not included in the total count. + # https://github.com/giampaolo/psutil/pull/1313#issuecomment-408626842 + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', return_value=False + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + assert ret is None + + def is_storage_device(name): + return name == 'nvme0n1' + + content = textwrap.dedent("""\ + 3 0 nvme0n1 1 2 3 4 5 6 7 8 9 10 11 + 3 0 nvme0n1p1 1 2 3 4 5 6 7 8 9 10 11 + """) + with mock_open_content({"/proc/diskstats": content}): + with mock.patch( + 'psutil._pslinux.is_storage_device', + create=True, + side_effect=is_storage_device, + ): + ret = psutil.disk_io_counters(perdisk=False, nowrap=False) + assert ret.read_count == 1 + assert ret.write_count == 5 + + def test_emulate_use_sysfs(self): + def exists(path): + return path == '/proc/diskstats' + + wprocfs = psutil.disk_io_counters(perdisk=True) + with mock.patch( + 'psutil._pslinux.os.path.exists', create=True, side_effect=exists + ): + wsysfs = psutil.disk_io_counters(perdisk=True) + assert len(wprocfs) == len(wsysfs) + + def test_emulate_not_impl(self): + def exists(path): + return False + + with mock.patch( + 'psutil._pslinux.os.path.exists', create=True, side_effect=exists + ): + with pytest.raises(NotImplementedError): + psutil.disk_io_counters() + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestRootFsDeviceFinder(PsutilTestCase): + def setUp(self): + dev = os.stat("/").st_dev + self.major = os.major(dev) + self.minor = os.minor(dev) + + def test_call_methods(self): + finder = RootFsDeviceFinder() + if os.path.exists("/proc/partitions"): + finder.ask_proc_partitions() + else: + with pytest.raises(FileNotFoundError): + finder.ask_proc_partitions() + if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): + finder.ask_sys_dev_block() + else: + with pytest.raises(FileNotFoundError): + finder.ask_sys_dev_block() + finder.ask_sys_class_block() + + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") + def test_comparisons(self): + finder = RootFsDeviceFinder() + assert finder.find() is not None + + a = b = c = None + if os.path.exists("/proc/partitions"): + a = finder.ask_proc_partitions() + if os.path.exists(f"/sys/dev/block/{self.major}:{self.minor}/uevent"): + b = finder.ask_sys_class_block() + c = finder.ask_sys_dev_block() + + base = a or b or c + if base and a: + assert base == a + if base and b: + assert base == b + if base and c: + assert base == c + + @pytest.mark.skipif( + not shutil.which("findmnt"), reason="findmnt utility not available" + ) + @pytest.mark.skipif(GITHUB_ACTIONS, reason="unsupported on GITHUB_ACTIONS") + def test_against_findmnt(self): + psutil_value = RootFsDeviceFinder().find() + findmnt_value = sh("findmnt -o SOURCE -rn /") + assert psutil_value == findmnt_value + + def test_disk_partitions_mocked(self): + with mock.patch( + 'psutil._pslinux.cext.disk_partitions', + return_value=[('/dev/root', '/', 'ext4', 'rw')], + ) as m: + part = psutil.disk_partitions()[0] + assert m.called + if not GITHUB_ACTIONS: + assert part.device != "/dev/root" + assert part.device == RootFsDeviceFinder().find() + else: + assert part.device == "/dev/root" + + +# ===================================================================== +# --- misc +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestMisc(PsutilTestCase): + def test_boot_time(self): + vmstat_value = vmstat('boot time') + psutil_value = psutil.boot_time() + assert int(vmstat_value) == int(psutil_value) + + def test_no_procfs_on_import(self): + my_procfs = self.get_testfn() + os.mkdir(my_procfs) + + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 0 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 0 0 0 0 0 0 0 0 0 0\n') + + try: + orig_open = open + + def open_mock(name, *args, **kwargs): + if name.startswith('/proc'): + raise FileNotFoundError + return orig_open(name, *args, **kwargs) + + with mock.patch("builtins.open", side_effect=open_mock): + reload_module(psutil) + + with pytest.raises(OSError): + psutil.cpu_times() + with pytest.raises(OSError): + psutil.cpu_times(percpu=True) + with pytest.raises(OSError): + psutil.cpu_percent() + with pytest.raises(OSError): + psutil.cpu_percent(percpu=True) + with pytest.raises(OSError): + psutil.cpu_times_percent() + with pytest.raises(OSError): + psutil.cpu_times_percent(percpu=True) + + psutil.PROCFS_PATH = my_procfs + + assert psutil.cpu_percent() == 0 + assert sum(psutil.cpu_times_percent()) == 0 + + # since we don't know the number of CPUs at import time, + # we awkwardly say there are none until the second call + per_cpu_percent = psutil.cpu_percent(percpu=True) + assert sum(per_cpu_percent) == 0 + + # ditto awkward length + per_cpu_times_percent = psutil.cpu_times_percent(percpu=True) + assert sum(map(sum, per_cpu_times_percent)) == 0 + + # much user, very busy + with open(os.path.join(my_procfs, 'stat'), 'w') as f: + f.write('cpu 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu0 1 0 0 0 0 0 0 0 0 0\n') + f.write('cpu1 1 0 0 0 0 0 0 0 0 0\n') + + assert psutil.cpu_percent() != 0 + assert sum(psutil.cpu_percent(percpu=True)) != 0 + assert sum(psutil.cpu_times_percent()) != 0 + assert ( + sum(map(sum, psutil.cpu_times_percent(percpu=True))) != 0 + ) + finally: + shutil.rmtree(my_procfs) + reload_module(psutil) + + assert psutil.PROCFS_PATH == '/proc' + + def test_cpu_steal_decrease(self): + # Test cumulative cpu stats decrease. We should ignore this. + # See issue #1210. + content = textwrap.dedent("""\ + cpu 0 0 0 0 0 0 0 1 0 0 + cpu0 0 0 0 0 0 0 0 1 0 0 + cpu1 0 0 0 0 0 0 0 1 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}) as m: + # first call to "percent" functions should read the new stat file + # and compare to the "real" file read at import time - so the + # values are meaningless + psutil.cpu_percent() + assert m.called + psutil.cpu_percent(percpu=True) + psutil.cpu_times_percent() + psutil.cpu_times_percent(percpu=True) + + content = textwrap.dedent("""\ + cpu 1 0 0 0 0 0 0 0 0 0 + cpu0 1 0 0 0 0 0 0 0 0 0 + cpu1 1 0 0 0 0 0 0 0 0 0 + """).encode() + with mock_open_content({"/proc/stat": content}): + # Increase "user" while steal goes "backwards" to zero. + cpu_percent = psutil.cpu_percent() + assert m.called + cpu_percent_percpu = psutil.cpu_percent(percpu=True) + cpu_times_percent = psutil.cpu_times_percent() + cpu_times_percent_percpu = psutil.cpu_times_percent(percpu=True) + assert cpu_percent != 0 + assert sum(cpu_percent_percpu) != 0 + assert sum(cpu_times_percent) != 0 + assert sum(cpu_times_percent) != 100.0 + assert sum(map(sum, cpu_times_percent_percpu)) != 0 + assert sum(map(sum, cpu_times_percent_percpu)) != 100.0 + assert cpu_times_percent.steal == 0 + assert cpu_times_percent.user != 0 + + def test_boot_time_mocked(self): + with mock.patch('psutil._common.open', create=True) as m: + with pytest.raises(RuntimeError): + psutil._pslinux.boot_time() + assert m.called + + def test_users(self): + # Make sure the C extension converts ':0' and ':0.0' to + # 'localhost'. + for user in psutil.users(): + assert user.host not in {":0", ":0.0"} + + def test_procfs_path(self): + tdir = self.get_testfn() + os.mkdir(tdir) + try: + psutil.PROCFS_PATH = tdir + with pytest.raises(OSError): + psutil.virtual_memory() + with pytest.raises(OSError): + psutil.cpu_times() + with pytest.raises(OSError): + psutil.cpu_times(percpu=True) + with pytest.raises(OSError): + psutil.boot_time() + # self.assertRaises(OSError, psutil.pids) + with pytest.raises(OSError): + psutil.net_connections() + with pytest.raises(OSError): + psutil.net_io_counters() + with pytest.raises(OSError): + psutil.net_if_stats() + # self.assertRaises(OSError, psutil.disk_io_counters) + with pytest.raises(OSError): + psutil.disk_partitions() + with pytest.raises(psutil.NoSuchProcess): + psutil.Process() + finally: + psutil.PROCFS_PATH = "/proc" + + @retry_on_failure() + @pytest.mark.skipif(PYTEST_PARALLEL, reason="skip if pytest-parallel") + def test_issue_687(self): + # In case of thread ID: + # - pid_exists() is supposed to return False + # - Process(tid) is supposed to work + # - pids() should not return the TID + # See: https://github.com/giampaolo/psutil/issues/687 + with ThreadTask(): + p = psutil.Process() + threads = p.threads() + assert len(threads) == 2 + tid = sorted(threads, key=lambda x: x.id)[1].id + assert p.pid != tid + pt = psutil.Process(tid) + pt.as_dict() + assert tid not in psutil.pids() + + def test_pid_exists_no_proc_status(self): + # Internally pid_exists relies on /proc/{pid}/status. + # Emulate a case where this file is empty in which case + # psutil is supposed to fall back on using pids(). + with mock_open_content({"/proc/%s/status": ""}) as m: + assert psutil.pid_exists(os.getpid()) + assert m.called + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +@pytest.mark.skipif(not HAS_BATTERY, reason="no battery") +class TestSensorsBattery(PsutilTestCase): + @pytest.mark.skipif( + not shutil.which("acpi"), reason="acpi utility not available" + ) + def test_percent(self): + out = sh("acpi -b") + acpi_value = int(out.split(",")[1].strip().replace('%', '')) + psutil_value = psutil.sensors_battery().percent + assert abs(acpi_value - psutil_value) < 1 + + def test_emulate_power_plugged(self): + # Pretend the AC power cable is connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"1") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is True + assert ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_power_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise FileNotFoundError + if name.endswith("/status"): + return io.StringIO("charging") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is True + assert m.called + + def test_emulate_power_not_plugged(self): + # Pretend the AC power cable is not connected. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + return io.BytesIO(b"0") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is False + assert m.called + + def test_emulate_power_not_plugged_2(self): + # Same as above but pretend /AC0/online does not exist in which + # case code relies on /status file. + def open_mock(name, *args, **kwargs): + if name.endswith(('AC0/online', 'AC/online')): + raise FileNotFoundError + if name.endswith("/status"): + return io.StringIO("discharging") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is False + assert m.called + + def test_emulate_power_undetermined(self): + # Pretend we can't know whether the AC power cable not + # connected (assert fallback to False). + def open_mock(name, *args, **kwargs): + if name.startswith(( + '/sys/class/power_supply/AC0/online', + '/sys/class/power_supply/AC/online', + )): + raise FileNotFoundError + if name.startswith("/sys/class/power_supply/BAT0/status"): + return io.BytesIO(b"???") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock) as m: + assert psutil.sensors_battery().power_plugged is None + assert m.called + + def test_emulate_energy_full_0(self): + # Emulate a case where energy_full files returns 0. + with mock_open_content( + {"/sys/class/power_supply/BAT0/energy_full": b"0"} + ) as m: + assert psutil.sensors_battery().percent == 0 + assert m.called + + def test_emulate_energy_full_not_avail(self): + # Emulate a case where energy_full file does not exist. + # Expected fallback on /capacity. + with mock_open_exception( + "/sys/class/power_supply/BAT0/energy_full", + FileNotFoundError, + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/charge_full", + FileNotFoundError, + ): + with mock_open_content( + {"/sys/class/power_supply/BAT0/capacity": b"88"} + ): + assert psutil.sensors_battery().percent == 88 + + def test_emulate_no_power(self): + # Emulate a case where /AC0/online file nor /BAT0/status exist. + with mock_open_exception( + "/sys/class/power_supply/AC/online", FileNotFoundError + ): + with mock_open_exception( + "/sys/class/power_supply/AC0/online", FileNotFoundError + ): + with mock_open_exception( + "/sys/class/power_supply/BAT0/status", + FileNotFoundError, + ): + assert psutil.sensors_battery().power_plugged is None + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsBatteryEmulated(PsutilTestCase): + def test_it(self): + def open_mock(name, *args, **kwargs): + if name.endswith("/energy_now"): + return io.StringIO("60000000") + elif name.endswith("/power_now"): + return io.StringIO("0") + elif name.endswith("/energy_full"): + return io.StringIO("60000001") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch('os.listdir', return_value=["BAT0"]) as mlistdir: + with mock.patch("builtins.open", side_effect=open_mock) as mopen: + assert psutil.sensors_battery() is not None + assert mlistdir.called + assert mopen.called + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsTemperatures(PsutilTestCase): + def test_emulate_class_hwmon(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO("name") + elif name.endswith('/temp1_label'): + return io.StringIO("label") + elif name.endswith('/temp1_input'): + return io.BytesIO(b"30000") + elif name.endswith('/temp1_max'): + return io.BytesIO(b"40000") + elif name.endswith('/temp1_crit'): + return io.BytesIO(b"50000") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + # Test case with /sys/class/hwmon + with mock.patch( + 'glob.glob', return_value=['/sys/class/hwmon/hwmon0/temp1'] + ): + temp = psutil.sensors_temperatures()['name'][0] + assert temp.label == 'label' + assert temp.current == 30.0 + assert temp.high == 40.0 + assert temp.critical == 50.0 + + def test_emulate_class_thermal(self): + def open_mock(name, *args, **kwargs): + if name.endswith('0_temp'): + return io.BytesIO(b"50000") + elif name.endswith('temp'): + return io.BytesIO(b"30000") + elif name.endswith('0_type'): + return io.StringIO("critical") + elif name.endswith('type'): + return io.StringIO("name") + else: + return orig_open(name, *args, **kwargs) + + def glob_mock(path): + if path in { + '/sys/class/hwmon/hwmon*/temp*_*', + '/sys/class/hwmon/hwmon*/device/temp*_*', + }: + return [] + elif path == '/sys/class/thermal/thermal_zone*': + return ['/sys/class/thermal/thermal_zone0'] + elif path == '/sys/class/thermal/thermal_zone0/trip_point*': + return [ + '/sys/class/thermal/thermal_zone1/trip_point_0_type', + '/sys/class/thermal/thermal_zone1/trip_point_0_temp', + ] + return [] + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch('glob.glob', create=True, side_effect=glob_mock): + temp = psutil.sensors_temperatures()['name'][0] + assert temp.label == '' + assert temp.current == 30.0 + assert temp.high == 50.0 + assert temp.critical == 50.0 + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestSensorsFans(PsutilTestCase): + def test_emulate_data(self): + def open_mock(name, *args, **kwargs): + if name.endswith('/name'): + return io.StringIO("name") + elif name.endswith('/fan1_label'): + return io.StringIO("label") + elif name.endswith('/fan1_input'): + return io.StringIO("2000") + else: + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock): + with mock.patch( + 'glob.glob', return_value=['/sys/class/hwmon/hwmon2/fan1'] + ): + fan = psutil.sensors_fans()['name'][0] + assert fan.label == 'label' + assert fan.current == 2000 + + +# ===================================================================== +# --- test process +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestProcess(PsutilTestCase): + @retry_on_failure() + def test_parse_smaps_vs_memory_maps(self): + sproc = self.spawn_testproc() + uss, pss, swap = psutil._pslinux.Process(sproc.pid)._parse_smaps() + maps = psutil.Process(sproc.pid).memory_maps(grouped=False) + assert ( + abs(uss - sum(x.private_dirty + x.private_clean for x in maps)) + < 4096 + ) + assert abs(pss - sum(x.pss for x in maps)) < 4096 + assert abs(swap - sum(x.swap for x in maps)) < 4096 + + def test_parse_smaps_mocked(self): + # See: https://github.com/giampaolo/psutil/issues/1222 + content = textwrap.dedent("""\ + fffff0 r-xp 00000000 00:00 0 [vsyscall] + Size: 1 kB + Rss: 2 kB + Pss: 3 kB + Shared_Clean: 4 kB + Shared_Dirty: 5 kB + Private_Clean: 6 kB + Private_Dirty: 7 kB + Referenced: 8 kB + Anonymous: 9 kB + LazyFree: 10 kB + AnonHugePages: 11 kB + ShmemPmdMapped: 12 kB + Shared_Hugetlb: 13 kB + Private_Hugetlb: 14 kB + Swap: 15 kB + SwapPss: 16 kB + KernelPageSize: 17 kB + MMUPageSize: 18 kB + Locked: 19 kB + VmFlags: rd ex + """).encode() + with mock_open_content({f"/proc/{os.getpid()}/smaps": content}) as m: + p = psutil._pslinux.Process(os.getpid()) + uss, pss, swap = p._parse_smaps() + assert m.called + assert uss == (6 + 7 + 14) * 1024 + assert pss == 3 * 1024 + assert swap == 15 * 1024 + + # On PYPY file descriptors are not closed fast enough. + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") + def test_open_files_mode(self): + def get_test_file(fname): + p = psutil.Process() + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + for file in p.open_files(): + if file.path == os.path.abspath(fname): + return file + elif time.time() > giveup_at: + break + raise RuntimeError("timeout looking for test file") + + testfn = self.get_testfn() + with open(testfn, "w"): + assert get_test_file(testfn).mode == "w" + with open(testfn): + assert get_test_file(testfn).mode == "r" + with open(testfn, "a"): + assert get_test_file(testfn).mode == "a" + with open(testfn, "r+"): + assert get_test_file(testfn).mode == "r+" + with open(testfn, "w+"): + assert get_test_file(testfn).mode == "r+" + with open(testfn, "a+"): + assert get_test_file(testfn).mode == "a+" + + safe_rmpath(testfn) + with open(testfn, "x"): + assert get_test_file(testfn).mode == "w" + safe_rmpath(testfn) + with open(testfn, "x+"): + assert get_test_file(testfn).mode == "r+" + + def test_open_files_file_gone(self): + # simulates a file which gets deleted during open_files() + # execution + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=FileNotFoundError, + ) as m: + assert p.open_files() == [] + assert m.called + # also simulate the case where os.readlink() returns EINVAL + # in which case psutil is supposed to 'continue' + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.EINVAL, ""), + ) as m: + assert p.open_files() == [] + assert m.called + + def test_open_files_fd_gone(self): + # Simulate a case where /proc/{pid}/fdinfo/{fd} disappears + # while iterating through fds. + # https://travis-ci.org/giampaolo/psutil/jobs/225694530 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + with mock.patch( + "builtins.open", side_effect=FileNotFoundError + ) as m: + assert p.open_files() == [] + assert m.called + + def test_open_files_enametoolong(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink + # points to a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + p = psutil.Process() + files = p.open_files() + with open(self.get_testfn(), 'w'): + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + patch_point = 'psutil._pslinux.os.readlink' + with mock.patch( + patch_point, side_effect=OSError(errno.ENAMETOOLONG, "") + ) as m: + with mock.patch("psutil._pslinux.debug"): + assert p.open_files() == [] + assert m.called + + # --- mocked tests + + def test_terminal_mocked(self): + with mock.patch( + 'psutil._pslinux._psposix.get_terminal_map', return_value={} + ) as m: + assert psutil._pslinux.Process(os.getpid()).terminal() is None + assert m.called + + # TODO: re-enable this test. + # def test_num_ctx_switches_mocked(self): + # with mock.patch('psutil._common.open', create=True) as m: + # self.assertRaises( + # NotImplementedError, + # psutil._pslinux.Process(os.getpid()).num_ctx_switches) + # assert m.called + + def test_cmdline_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/639 + p = psutil.Process() + fake_file = io.StringIO('foo\x00bar\x00') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar'] + assert m.called + fake_file = io.StringIO('foo\x00bar\x00\x00') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar', ''] + assert m.called + + def test_cmdline_spaces_mocked(self): + # see: https://github.com/giampaolo/psutil/issues/1179 + p = psutil.Process() + fake_file = io.StringIO('foo bar ') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar'] + assert m.called + fake_file = io.StringIO('foo bar ') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar', ''] + assert m.called + + def test_cmdline_mixed_separators(self): + # https://github.com/giampaolo/psutil/issues/ + # 1179#issuecomment-552984549 + p = psutil.Process() + fake_file = io.StringIO('foo\x20bar\x00') + with mock.patch( + 'psutil._common.open', return_value=fake_file, create=True + ) as m: + assert p.cmdline() == ['foo', 'bar'] + assert m.called + + def test_readlink_path_deleted_mocked(self): + with mock.patch( + 'psutil._pslinux.os.readlink', return_value='/home/foo (deleted)' + ): + assert psutil.Process().exe() == "/home/foo" + assert psutil.Process().cwd() == "/home/foo" + + def test_threads_mocked(self): + # Test the case where os.listdir() returns a file (thread) + # which no longer exists by the time we open() it (race + # condition). threads() is supposed to ignore that instead + # of raising NSP. + def open_mock_1(name, *args, **kwargs): + if name.startswith(f"/proc/{os.getpid()}/task"): + raise FileNotFoundError + return orig_open(name, *args, **kwargs) + + orig_open = open + with mock.patch("builtins.open", side_effect=open_mock_1) as m: + ret = psutil.Process().threads() + assert m.called + assert ret == [] + + # ...but if it bumps into something != ENOENT we want an + # exception. + def open_mock_2(name, *args, **kwargs): + if name.startswith(f"/proc/{os.getpid()}/task"): + raise PermissionError + return orig_open(name, *args, **kwargs) + + with mock.patch("builtins.open", side_effect=open_mock_2): + with pytest.raises(psutil.AccessDenied): + psutil.Process().threads() + + def test_exe_mocked(self): + with mock.patch( + 'psutil._pslinux.readlink', side_effect=FileNotFoundError + ) as m: + # de-activate guessing from cmdline() + with mock.patch( + 'psutil._pslinux.Process.cmdline', return_value=[] + ): + ret = psutil.Process().exe() + assert m.called + assert ret == "" + + def test_issue_1014(self): + # Emulates a case where smaps file does not exist. In this case + # wrap_exception decorator should not raise NoSuchProcess. + with mock_open_exception( + f"/proc/{os.getpid()}/smaps", FileNotFoundError + ) as m: + p = psutil.Process() + with pytest.raises(FileNotFoundError): + p.memory_maps() + assert m.called + + def test_issue_2418(self): + p = psutil.Process() + with mock_open_exception( + f"/proc/{os.getpid()}/statm", FileNotFoundError + ): + with mock.patch("os.path.exists", return_value=False): + with pytest.raises(psutil.NoSuchProcess): + p.memory_info() + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_zombie(self): + # Emulate a case where rlimit() raises ENOSYS, which may + # happen in case of zombie process: + # https://travis-ci.org/giampaolo/psutil/jobs/51368273 + with mock.patch( + "resource.prlimit", side_effect=OSError(errno.ENOSYS, "") + ) as m1: + with mock.patch( + "psutil._pslinux.Process._is_zombie", return_value=True + ) as m2: + p = psutil.Process() + p.name() + with pytest.raises(psutil.ZombieProcess) as cm: + p.rlimit(psutil.RLIMIT_NOFILE) + assert m1.called + assert m2.called + assert cm.value.pid == p.pid + assert cm.value.name == p.name() + + def test_stat_file_parsing(self): + args = [ + "0", # pid + "(cat)", # name + "Z", # status + "1", # ppid + "0", # pgrp + "0", # session + "0", # tty + "0", # tpgid + "0", # flags + "0", # minflt + "0", # cminflt + "0", # majflt + "0", # cmajflt + "2", # utime + "3", # stime + "4", # cutime + "5", # cstime + "0", # priority + "0", # nice + "0", # num_threads + "0", # itrealvalue + "6", # starttime + "0", # vsize + "0", # rss + "0", # rsslim + "0", # startcode + "0", # endcode + "0", # startstack + "0", # kstkesp + "0", # kstkeip + "0", # signal + "0", # blocked + "0", # sigignore + "0", # sigcatch + "0", # wchan + "0", # nswap + "0", # cnswap + "0", # exit_signal + "6", # processor + "0", # rt priority + "0", # policy + "7", # delayacct_blkio_ticks + ] + content = " ".join(args).encode() + with mock_open_content({f"/proc/{os.getpid()}/stat": content}): + p = psutil.Process() + assert p.name() == 'cat' + assert p.status() == psutil.STATUS_ZOMBIE + assert p.ppid() == 1 + assert p.create_time() == 6 / CLOCK_TICKS + psutil.boot_time() + cpu = p.cpu_times() + assert cpu.user == 2 / CLOCK_TICKS + assert cpu.system == 3 / CLOCK_TICKS + assert cpu.children_user == 4 / CLOCK_TICKS + assert cpu.children_system == 5 / CLOCK_TICKS + assert cpu.iowait == 7 / CLOCK_TICKS + assert p.cpu_num() == 6 + + def test_status_file_parsing(self): + content = textwrap.dedent("""\ + Uid:\t1000\t1001\t1002\t1003 + Gid:\t1004\t1005\t1006\t1007 + Threads:\t66 + Cpus_allowed:\tf + Cpus_allowed_list:\t0-7 + voluntary_ctxt_switches:\t12 + nonvoluntary_ctxt_switches:\t13""").encode() + with mock_open_content({f"/proc/{os.getpid()}/status": content}): + p = psutil.Process() + assert p.num_ctx_switches().voluntary == 12 + assert p.num_ctx_switches().involuntary == 13 + assert p.num_threads() == 66 + uids = p.uids() + assert uids.real == 1000 + assert uids.effective == 1001 + assert uids.saved == 1002 + gids = p.gids() + assert gids.real == 1004 + assert gids.effective == 1005 + assert gids.saved == 1006 + assert p._proc._get_eligible_cpus() == list(range(8)) + + def test_net_connections_enametoolong(self): + # Simulate a case where /proc/{pid}/fd/{fd} symlink points to + # a file with full path longer than PATH_MAX, see: + # https://github.com/giampaolo/psutil/issues/1940 + with mock.patch( + 'psutil._pslinux.os.readlink', + side_effect=OSError(errno.ENAMETOOLONG, ""), + ) as m: + p = psutil.Process() + with mock.patch("psutil._pslinux.debug"): + assert p.net_connections() == [] + assert m.called + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestProcessAgainstStatus(PsutilTestCase): + """/proc/pid/stat and /proc/pid/status have many values in common. + Whenever possible, psutil uses /proc/pid/stat (it's faster). + For all those cases we check that the value found in + /proc/pid/stat (by psutil) matches the one found in + /proc/pid/status. + """ + + @classmethod + def setUpClass(cls): + cls.proc = psutil.Process() + + def read_status_file(self, linestart): + with psutil._psplatform.open_text( + f"/proc/{self.proc.pid}/status" + ) as f: + for line in f: + line = line.strip() + if line.startswith(linestart): + value = line.partition('\t')[2] + try: + return int(value) + except ValueError: + return value + raise ValueError(f"can't find {linestart!r}") + + def test_name(self): + value = self.read_status_file("Name:") + assert self.proc.name() == value + + def test_status(self): + value = self.read_status_file("State:") + value = value[value.find('(') + 1 : value.rfind(')')] + value = value.replace(' ', '-') + assert self.proc.status() == value + + def test_ppid(self): + value = self.read_status_file("PPid:") + assert self.proc.ppid() == value + + def test_num_threads(self): + value = self.read_status_file("Threads:") + assert self.proc.num_threads() == value + + def test_uids(self): + value = self.read_status_file("Uid:") + value = tuple(map(int, value.split()[1:4])) + assert self.proc.uids() == value + + def test_gids(self): + value = self.read_status_file("Gid:") + value = tuple(map(int, value.split()[1:4])) + assert self.proc.gids() == value + + @retry_on_failure() + def test_num_ctx_switches(self): + value = self.read_status_file("voluntary_ctxt_switches:") + assert self.proc.num_ctx_switches().voluntary == value + value = self.read_status_file("nonvoluntary_ctxt_switches:") + assert self.proc.num_ctx_switches().involuntary == value + + def test_cpu_affinity(self): + value = self.read_status_file("Cpus_allowed_list:") + if '-' in str(value): + min_, max_ = map(int, value.split('-')) + assert self.proc.cpu_affinity() == list(range(min_, max_ + 1)) + + def test_cpu_affinity_eligible_cpus(self): + value = self.read_status_file("Cpus_allowed_list:") + with mock.patch("psutil._pslinux.per_cpu_times") as m: + self.proc._proc._get_eligible_cpus() + if '-' in str(value): + assert not m.called + else: + assert m.called + + +# ===================================================================== +# --- test utils +# ===================================================================== + + +@pytest.mark.skipif(not LINUX, reason="LINUX only") +class TestUtils(PsutilTestCase): + def test_readlink(self): + with mock.patch("os.readlink", return_value="foo (deleted)") as m: + assert psutil._psplatform.readlink("bar") == "foo" + assert m.called diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_memleaks.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_memleaks.py new file mode 100644 index 0000000000000000000000000000000000000000..7f78fae67cbce20c4ec31e3703406f5135860a8e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_memleaks.py @@ -0,0 +1,487 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for detecting function memory leaks (typically the ones +implemented in C). It does so by calling a function many times and +checking whether process memory usage keeps increasing between +calls or over time. +Note that this may produce false positives (especially on Windows +for some reason). +PyPy appears to be completely unstable for this framework, probably +because of how its JIT handles memory, so tests are skipped. +""" + + +import functools +import os +import platform + +import psutil +import psutil._common +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import TestMemoryLeak +from psutil.tests import create_sockets +from psutil.tests import get_testfn +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import system_namespace +from psutil.tests import terminate + + +cext = psutil._psplatform.cext +thisproc = psutil.Process() +FEW_TIMES = 5 + + +def fewtimes_if_linux(): + """Decorator for those Linux functions which are implemented in pure + Python, and which we want to run faster. + """ + + def decorator(fun): + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + if LINUX: + before = self.__class__.times + try: + self.__class__.times = FEW_TIMES + return fun(self, *args, **kwargs) + finally: + self.__class__.times = before + else: + return fun(self, *args, **kwargs) + + return wrapper + + return decorator + + +# =================================================================== +# Process class +# =================================================================== + + +class TestProcessObjectLeaks(TestMemoryLeak): + """Test leaks of Process class methods.""" + + proc = thisproc + + def test_coverage(self): + ns = process_namespace(None) + ns.test_class_coverage(self, ns.getters + ns.setters) + + @fewtimes_if_linux() + def test_name(self): + self.execute(self.proc.name) + + @fewtimes_if_linux() + def test_cmdline(self): + self.execute(self.proc.cmdline) + + @fewtimes_if_linux() + def test_exe(self): + self.execute(self.proc.exe) + + @fewtimes_if_linux() + def test_ppid(self): + self.execute(self.proc.ppid) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_uids(self): + self.execute(self.proc.uids) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_gids(self): + self.execute(self.proc.gids) + + @fewtimes_if_linux() + def test_status(self): + self.execute(self.proc.status) + + def test_nice(self): + self.execute(self.proc.nice) + + def test_nice_set(self): + niceness = thisproc.nice() + self.execute(lambda: self.proc.nice(niceness)) + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + def test_ionice(self): + self.execute(self.proc.ionice) + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + def test_ionice_set(self): + if WINDOWS: + value = thisproc.ionice() + self.execute(lambda: self.proc.ionice(value)) + else: + self.execute(lambda: self.proc.ionice(psutil.IOPRIO_CLASS_NONE)) + fun = functools.partial(cext.proc_ioprio_set, os.getpid(), -1, 0) + self.execute_w_exc(OSError, fun) + + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") + @fewtimes_if_linux() + def test_io_counters(self): + self.execute(self.proc.io_counters) + + @pytest.mark.skipif(POSIX, reason="worthless on POSIX") + def test_username(self): + # always open 1 handle on Windows (only once) + psutil.Process().username() + self.execute(self.proc.username) + + @fewtimes_if_linux() + def test_create_time(self): + self.execute(self.proc.create_time) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_num_threads(self): + self.execute(self.proc.num_threads) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_num_handles(self): + self.execute(self.proc.num_handles) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_num_fds(self): + self.execute(self.proc.num_fds) + + @fewtimes_if_linux() + def test_num_ctx_switches(self): + self.execute(self.proc.num_ctx_switches) + + @fewtimes_if_linux() + @skip_on_access_denied(only_if=OPENBSD) + def test_threads(self): + self.execute(self.proc.threads) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(self.proc.cpu_times) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") + def test_cpu_num(self): + self.execute(self.proc.cpu_num) + + @fewtimes_if_linux() + def test_memory_info(self): + self.execute(self.proc.memory_info) + + @fewtimes_if_linux() + def test_memory_full_info(self): + self.execute(self.proc.memory_full_info) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @fewtimes_if_linux() + def test_terminal(self): + self.execute(self.proc.terminal) + + def test_resume(self): + times = FEW_TIMES if POSIX else self.times + self.execute(self.proc.resume, times=times) + + @fewtimes_if_linux() + def test_cwd(self): + self.execute(self.proc.cwd) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity(self): + self.execute(self.proc.cpu_affinity) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_set(self): + affinity = thisproc.cpu_affinity() + self.execute(lambda: self.proc.cpu_affinity(affinity)) + self.execute_w_exc(ValueError, lambda: self.proc.cpu_affinity([-1])) + + @fewtimes_if_linux() + def test_open_files(self): + with open(get_testfn(), 'w'): + self.execute(self.proc.open_files) + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @fewtimes_if_linux() + def test_memory_maps(self): + self.execute(self.proc.memory_maps) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit(self): + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE)) + + @pytest.mark.skipif(not LINUX, reason="LINUX only") + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_set(self): + limit = thisproc.rlimit(psutil.RLIMIT_NOFILE) + self.execute(lambda: self.proc.rlimit(psutil.RLIMIT_NOFILE, limit)) + self.execute_w_exc((OSError, ValueError), lambda: self.proc.rlimit(-1)) + + @fewtimes_if_linux() + # Windows implementation is based on a single system-wide + # function (tested later). + @pytest.mark.skipif(WINDOWS, reason="worthless on WINDOWS") + def test_net_connections(self): + # TODO: UNIX sockets are temporarily implemented by parsing + # 'pfiles' cmd output; we don't want that part of the code to + # be executed. + with create_sockets(): + kind = 'inet' if SUNOS else 'all' + self.execute(lambda: self.proc.net_connections(kind)) + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + def test_environ(self): + self.execute(self.proc.environ) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_proc_info(self): + self.execute(lambda: cext.proc_info(os.getpid())) + + +class TestTerminatedProcessLeaks(TestProcessObjectLeaks): + """Repeat the tests above looking for leaks occurring when dealing + with terminated processes raising NoSuchProcess exception. + The C functions are still invoked but will follow different code + paths. We'll check those code paths. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.subp = spawn_testproc() + cls.proc = psutil.Process(cls.subp.pid) + cls.proc.kill() + cls.proc.wait() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + terminate(cls.subp) + + def call(self, fun): + try: + fun() + except psutil.NoSuchProcess: + pass + + if WINDOWS: + + def test_kill(self): + self.execute(self.proc.kill) + + def test_terminate(self): + self.execute(self.proc.terminate) + + def test_suspend(self): + self.execute(self.proc.suspend) + + def test_resume(self): + self.execute(self.proc.resume) + + def test_wait(self): + self.execute(self.proc.wait) + + def test_proc_info(self): + # test dual implementation + def call(): + try: + return cext.proc_info(self.proc.pid) + except ProcessLookupError: + pass + + self.execute(call) + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestProcessDualImplementation(TestMemoryLeak): + def test_cmdline_peb_true(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=True)) + + def test_cmdline_peb_false(self): + self.execute(lambda: cext.proc_cmdline(os.getpid(), use_peb=False)) + + +# =================================================================== +# system APIs +# =================================================================== + + +class TestModuleFunctionsLeaks(TestMemoryLeak): + """Test leaks of psutil module functions.""" + + def test_coverage(self): + ns = system_namespace() + ns.test_class_coverage(self, ns.all) + + # --- cpu + + @fewtimes_if_linux() + def test_cpu_count(self): # logical + self.execute(lambda: psutil.cpu_count(logical=True)) + + @fewtimes_if_linux() + def test_cpu_count_cores(self): + self.execute(lambda: psutil.cpu_count(logical=False)) + + @fewtimes_if_linux() + def test_cpu_times(self): + self.execute(psutil.cpu_times) + + @fewtimes_if_linux() + def test_per_cpu_times(self): + self.execute(lambda: psutil.cpu_times(percpu=True)) + + @fewtimes_if_linux() + def test_cpu_stats(self): + self.execute(psutil.cpu_stats) + + @fewtimes_if_linux() + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + self.execute(psutil.cpu_freq) + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_getloadavg(self): + psutil.getloadavg() + self.execute(psutil.getloadavg) + + # --- mem + + def test_virtual_memory(self): + self.execute(psutil.virtual_memory) + + # TODO: remove this skip when this gets fixed + @pytest.mark.skipif(SUNOS, reason="worthless on SUNOS (uses a subprocess)") + def test_swap_memory(self): + self.execute(psutil.swap_memory) + + def test_pid_exists(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.pid_exists(os.getpid()), times=times) + + # --- disk + + def test_disk_usage(self): + times = FEW_TIMES if POSIX else self.times + self.execute(lambda: psutil.disk_usage('.'), times=times) + + def test_disk_partitions(self): + self.execute(psutil.disk_partitions) + + @pytest.mark.skipif( + LINUX and not os.path.exists('/proc/diskstats'), + reason="/proc/diskstats not available on this Linux version", + ) + @fewtimes_if_linux() + def test_disk_io_counters(self): + self.execute(lambda: psutil.disk_io_counters(nowrap=False)) + + # --- proc + + @fewtimes_if_linux() + def test_pids(self): + self.execute(psutil.pids) + + # --- net + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters(self): + self.execute(lambda: psutil.net_io_counters(nowrap=False)) + + @fewtimes_if_linux() + @pytest.mark.skipif(MACOS and os.getuid() != 0, reason="need root access") + def test_net_connections(self): + # always opens and handle on Windows() (once) + psutil.net_connections(kind='all') + with create_sockets(): + self.execute(lambda: psutil.net_connections(kind='all')) + + def test_net_if_addrs(self): + # Note: verified that on Windows this was a false positive. + tolerance = 80 * 1024 if WINDOWS else self.tolerance + self.execute(psutil.net_if_addrs, tolerance=tolerance) + + def test_net_if_stats(self): + self.execute(psutil.net_if_stats) + + # --- sensors + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + def test_sensors_battery(self): + self.execute(psutil.sensors_battery) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures(self): + self.execute(psutil.sensors_temperatures) + + @fewtimes_if_linux() + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + self.execute(psutil.sensors_fans) + + # --- others + + @fewtimes_if_linux() + def test_boot_time(self): + self.execute(psutil.boot_time) + + def test_users(self): + self.execute(psutil.users) + + def test_set_debug(self): + self.execute(lambda: psutil._set_debug(False)) + + if WINDOWS: + + # --- win services + + def test_win_service_iter(self): + self.execute(cext.winservice_enumerate) + + def test_win_service_get(self): + pass + + def test_win_service_get_config(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_config(name)) + + def test_win_service_get_status(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_status(name)) + + def test_win_service_get_description(self): + name = next(psutil.win_service_iter()).name() + self.execute(lambda: cext.winservice_query_descr(name)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_misc.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_misc.py new file mode 100644 index 0000000000000000000000000000000000000000..c484264b9190adcf4d7651a88eab53f1c354e845 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_misc.py @@ -0,0 +1,873 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Miscellaneous tests.""" + +import collections +import contextlib +import io +import json +import os +import pickle +import socket +import sys +from unittest import mock + +import psutil +import psutil.tests +from psutil import WINDOWS +from psutil._common import bcat +from psutil._common import cat +from psutil._common import debug +from psutil._common import isfile_strict +from psutil._common import memoize +from psutil._common import memoize_when_activated +from psutil._common import parse_environ_block +from psutil._common import supports_ipv6 +from psutil._common import wrap_numbers +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import PsutilTestCase +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reload_module +from psutil.tests import system_namespace + + +# =================================================================== +# --- Test classes' repr(), str(), ... +# =================================================================== + + +class TestSpecialMethods(PsutilTestCase): + def test_check_pid_range(self): + with pytest.raises(OverflowError): + psutil._psplatform.cext.check_pid_range(2**128) + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(2**128) + + def test_process__repr__(self, func=repr): + p = psutil.Process(self.spawn_testproc().pid) + r = func(p) + assert "psutil.Process" in r + assert f"pid={p.pid}" in r + assert f"name='{p.name()}'" in r.replace("name=u'", "name='") + assert "status=" in r + assert "exitcode=" not in r + p.terminate() + p.wait() + r = func(p) + assert "status='terminated'" in r + assert "exitcode=" in r + + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.ZombieProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={p.pid}" in r + assert "status='zombie'" in r + assert "name=" not in r + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={p.pid}" in r + assert "terminated" in r + assert "name=" not in r + with mock.patch.object( + psutil.Process, + "name", + side_effect=psutil.AccessDenied(os.getpid()), + ): + p = psutil.Process() + r = func(p) + assert f"pid={p.pid}" in r + assert "name=" not in r + + def test_process__str__(self): + self.test_process__repr__(func=str) + + def test_error__repr__(self): + assert repr(psutil.Error()) == "psutil.Error()" + + def test_error__str__(self): + assert str(psutil.Error()) == "" + + def test_no_such_process__repr__(self): + assert ( + repr(psutil.NoSuchProcess(321)) + == "psutil.NoSuchProcess(pid=321, msg='process no longer exists')" + ) + assert ( + repr(psutil.NoSuchProcess(321, name="name", msg="msg")) + == "psutil.NoSuchProcess(pid=321, name='name', msg='msg')" + ) + + def test_no_such_process__str__(self): + assert ( + str(psutil.NoSuchProcess(321)) + == "process no longer exists (pid=321)" + ) + assert ( + str(psutil.NoSuchProcess(321, name="name", msg="msg")) + == "msg (pid=321, name='name')" + ) + + def test_zombie_process__repr__(self): + assert ( + repr(psutil.ZombieProcess(321)) + == 'psutil.ZombieProcess(pid=321, msg="PID still ' + 'exists but it\'s a zombie")' + ) + assert ( + repr(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")) + == "psutil.ZombieProcess(pid=321, ppid=320, name='name'," + " msg='foo')" + ) + + def test_zombie_process__str__(self): + assert ( + str(psutil.ZombieProcess(321)) + == "PID still exists but it's a zombie (pid=321)" + ) + assert ( + str(psutil.ZombieProcess(321, name="name", ppid=320, msg="foo")) + == "foo (pid=321, ppid=320, name='name')" + ) + + def test_access_denied__repr__(self): + assert repr(psutil.AccessDenied(321)) == "psutil.AccessDenied(pid=321)" + assert ( + repr(psutil.AccessDenied(321, name="name", msg="msg")) + == "psutil.AccessDenied(pid=321, name='name', msg='msg')" + ) + + def test_access_denied__str__(self): + assert str(psutil.AccessDenied(321)) == "(pid=321)" + assert ( + str(psutil.AccessDenied(321, name="name", msg="msg")) + == "msg (pid=321, name='name')" + ) + + def test_timeout_expired__repr__(self): + assert ( + repr(psutil.TimeoutExpired(5)) + == "psutil.TimeoutExpired(seconds=5, msg='timeout after 5" + " seconds')" + ) + assert ( + repr(psutil.TimeoutExpired(5, pid=321, name="name")) + == "psutil.TimeoutExpired(pid=321, name='name', seconds=5, " + "msg='timeout after 5 seconds')" + ) + + def test_timeout_expired__str__(self): + assert str(psutil.TimeoutExpired(5)) == "timeout after 5 seconds" + assert ( + str(psutil.TimeoutExpired(5, pid=321, name="name")) + == "timeout after 5 seconds (pid=321, name='name')" + ) + + def test_process__eq__(self): + p1 = psutil.Process() + p2 = psutil.Process() + assert p1 == p2 + p2._ident = (0, 0) + assert p1 != p2 + assert p1 != 'foo' + + def test_process__hash__(self): + s = {psutil.Process(), psutil.Process()} + assert len(s) == 1 + + +# =================================================================== +# --- Misc, generic, corner cases +# =================================================================== + + +class TestMisc(PsutilTestCase): + def test__all__(self): + dir_psutil = dir(psutil) + for name in dir_psutil: + if name in { + 'debug', + 'tests', + 'test', + 'PermissionError', + 'ProcessLookupError', + }: + continue + if not name.startswith('_'): + try: + __import__(name) + except ImportError: + if name not in psutil.__all__: + fun = getattr(psutil, name) + if fun is None: + continue + if ( + fun.__doc__ is not None + and 'deprecated' not in fun.__doc__.lower() + ): + raise self.fail(f"{name!r} not in psutil.__all__") + + # Import 'star' will break if __all__ is inconsistent, see: + # https://github.com/giampaolo/psutil/issues/656 + # Can't do `from psutil import *` as it won't work + # so we simply iterate over __all__. + for name in psutil.__all__: + assert name in dir_psutil + + def test_version(self): + assert ( + '.'.join([str(x) for x in psutil.version_info]) + == psutil.__version__ + ) + + def test_process_as_dict_no_new_names(self): + # See https://github.com/giampaolo/psutil/issues/813 + p = psutil.Process() + p.foo = '1' + assert 'foo' not in p.as_dict() + + def test_serialization(self): + def check(ret): + json.loads(json.dumps(ret)) + + a = pickle.dumps(ret) + b = pickle.loads(a) + assert ret == b + + # --- process APIs + + proc = psutil.Process() + check(psutil.Process().as_dict()) + + ns = process_namespace(proc) + for fun, name in ns.iter(ns.getters, clear_cache=True): + with self.subTest(proc=proc, name=name): + try: + ret = fun() + except psutil.Error: + pass + else: + check(ret) + + # --- system APIs + + ns = system_namespace() + for fun, name in ns.iter(ns.getters): + if name in {"win_service_iter", "win_service_get"}: + continue + with self.subTest(name=name): + try: + ret = fun() + except psutil.AccessDenied: + pass + else: + check(ret) + + # --- exception classes + + b = pickle.loads( + pickle.dumps( + psutil.NoSuchProcess(pid=4567, name='name', msg='msg') + ) + ) + assert isinstance(b, psutil.NoSuchProcess) + assert b.pid == 4567 + assert b.name == 'name' + assert b.msg == 'msg' + + b = pickle.loads( + pickle.dumps( + psutil.ZombieProcess(pid=4567, name='name', ppid=42, msg='msg') + ) + ) + assert isinstance(b, psutil.ZombieProcess) + assert b.pid == 4567 + assert b.ppid == 42 + assert b.name == 'name' + assert b.msg == 'msg' + + b = pickle.loads( + pickle.dumps(psutil.AccessDenied(pid=123, name='name', msg='msg')) + ) + assert isinstance(b, psutil.AccessDenied) + assert b.pid == 123 + assert b.name == 'name' + assert b.msg == 'msg' + + b = pickle.loads( + pickle.dumps( + psutil.TimeoutExpired(seconds=33, pid=4567, name='name') + ) + ) + assert isinstance(b, psutil.TimeoutExpired) + assert b.seconds == 33 + assert b.pid == 4567 + assert b.name == 'name' + + def test_ad_on_process_creation(self): + # We are supposed to be able to instantiate Process also in case + # of zombie processes or access denied. + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=psutil.AccessDenied + ) as meth: + psutil.Process() + assert meth.called + + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=psutil.ZombieProcess(1) + ) as meth: + psutil.Process() + assert meth.called + + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=ValueError + ) as meth: + with pytest.raises(ValueError): + psutil.Process() + assert meth.called + + with mock.patch.object( + psutil.Process, '_get_ident', side_effect=psutil.NoSuchProcess(1) + ) as meth: + with self.assertRaises(psutil.NoSuchProcess): + psutil.Process() + assert meth.called + + def test_sanity_version_check(self): + # see: https://github.com/giampaolo/psutil/issues/564 + with mock.patch( + "psutil._psplatform.cext.version", return_value="0.0.0" + ): + with pytest.raises(ImportError) as cm: + reload_module(psutil) + assert "version conflict" in str(cm.value).lower() + + +# =================================================================== +# --- psutil/_common.py utils +# =================================================================== + + +class TestMemoizeDecorator(PsutilTestCase): + def setUp(self): + self.calls = [] + + tearDown = setUp + + def run_against(self, obj, expected_retval=None): + # no args + for _ in range(2): + ret = obj() + assert self.calls == [((), {})] + if expected_retval is not None: + assert ret == expected_retval + # with args + for _ in range(2): + ret = obj(1) + assert self.calls == [((), {}), ((1,), {})] + if expected_retval is not None: + assert ret == expected_retval + # with args + kwargs + for _ in range(2): + ret = obj(1, bar=2) + assert self.calls == [((), {}), ((1,), {}), ((1,), {'bar': 2})] + if expected_retval is not None: + assert ret == expected_retval + # clear cache + assert len(self.calls) == 3 + obj.cache_clear() + ret = obj() + if expected_retval is not None: + assert ret == expected_retval + assert len(self.calls) == 4 + # docstring + assert obj.__doc__ == "My docstring." + + def test_function(self): + @memoize + def foo(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(foo, expected_retval=22) + + def test_class(self): + @memoize + class Foo: + """My docstring.""" + + def __init__(self, *args, **kwargs): + baseclass.calls.append((args, kwargs)) + + def bar(self): + return 22 + + baseclass = self + self.run_against(Foo, expected_retval=None) + assert Foo().bar() == 22 + + def test_class_singleton(self): + # @memoize can be used against classes to create singletons + @memoize + class Bar: + def __init__(self, *args, **kwargs): + pass + + assert Bar() is Bar() + assert id(Bar()) == id(Bar()) + assert id(Bar(1)) == id(Bar(1)) + assert id(Bar(1, foo=3)) == id(Bar(1, foo=3)) + assert id(Bar(1)) != id(Bar(2)) + + def test_staticmethod(self): + class Foo: + @staticmethod + @memoize + def bar(*args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_classmethod(self): + class Foo: + @classmethod + @memoize + def bar(cls, *args, **kwargs): + """My docstring.""" + baseclass.calls.append((args, kwargs)) + return 22 + + baseclass = self + self.run_against(Foo().bar, expected_retval=22) + + def test_original(self): + # This was the original test before I made it dynamic to test it + # against different types. Keeping it anyway. + @memoize + def foo(*args, **kwargs): + """Foo docstring.""" + calls.append(None) + return (args, kwargs) + + calls = [] + # no args + for _ in range(2): + ret = foo() + expected = ((), {}) + assert ret == expected + assert len(calls) == 1 + # with args + for _ in range(2): + ret = foo(1) + expected = ((1,), {}) + assert ret == expected + assert len(calls) == 2 + # with args + kwargs + for _ in range(2): + ret = foo(1, bar=2) + expected = ((1,), {'bar': 2}) + assert ret == expected + assert len(calls) == 3 + # clear cache + foo.cache_clear() + ret = foo() + expected = ((), {}) + assert ret == expected + assert len(calls) == 4 + # docstring + assert foo.__doc__ == "Foo docstring." + + +class TestCommonModule(PsutilTestCase): + def test_memoize_when_activated(self): + class Foo: + @memoize_when_activated + def foo(self): + calls.append(None) + + f = Foo() + calls = [] + f.foo() + f.foo() + assert len(calls) == 2 + + # activate + calls = [] + f.foo.cache_activate(f) + f.foo() + f.foo() + assert len(calls) == 1 + + # deactivate + calls = [] + f.foo.cache_deactivate(f) + f.foo() + f.foo() + assert len(calls) == 2 + + def test_parse_environ_block(self): + def k(s): + return s.upper() if WINDOWS else s + + assert parse_environ_block("a=1\0") == {k("a"): "1"} + assert parse_environ_block("a=1\0b=2\0\0") == { + k("a"): "1", + k("b"): "2", + } + assert parse_environ_block("a=1\0b=\0\0") == {k("a"): "1", k("b"): ""} + # ignore everything after \0\0 + assert parse_environ_block("a=1\0b=2\0\0c=3\0") == { + k("a"): "1", + k("b"): "2", + } + # ignore everything that is not an assignment + assert parse_environ_block("xxx\0a=1\0") == {k("a"): "1"} + assert parse_environ_block("a=1\0=b=2\0") == {k("a"): "1"} + # do not fail if the block is incomplete + assert parse_environ_block("a=1\0b=2") == {k("a"): "1"} + + def test_supports_ipv6(self): + self.addCleanup(supports_ipv6.cache_clear) + if supports_ipv6(): + with mock.patch('psutil._common.socket') as s: + s.has_ipv6 = False + supports_ipv6.cache_clear() + assert not supports_ipv6() + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=OSError + ) as s: + assert not supports_ipv6() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket', side_effect=socket.gaierror + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + + supports_ipv6.cache_clear() + with mock.patch( + 'psutil._common.socket.socket.bind', + side_effect=socket.gaierror, + ) as s: + assert not supports_ipv6() + supports_ipv6.cache_clear() + assert s.called + else: + with pytest.raises(OSError): + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + try: + sock.bind(("::1", 0)) + finally: + sock.close() + + def test_isfile_strict(self): + this_file = os.path.abspath(__file__) + assert isfile_strict(this_file) + assert not isfile_strict(os.path.dirname(this_file)) + with mock.patch('psutil._common.os.stat', side_effect=PermissionError): + with pytest.raises(OSError): + isfile_strict(this_file) + with mock.patch( + 'psutil._common.os.stat', side_effect=FileNotFoundError + ): + assert not isfile_strict(this_file) + with mock.patch('psutil._common.stat.S_ISREG', return_value=False): + assert not isfile_strict(this_file) + + def test_debug(self): + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.StringIO()) as f: + debug("hello") + sys.stderr.flush() + msg = f.getvalue() + assert msg.startswith("psutil-debug"), msg + assert "hello" in msg + assert __file__.replace('.pyc', '.py') in msg + + # supposed to use repr(exc) + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.StringIO()) as f: + debug(ValueError("this is an error")) + msg = f.getvalue() + assert "ignoring ValueError" in msg + assert "'this is an error'" in msg + + # supposed to use str(exc), because of extra info about file name + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.StringIO()) as f: + exc = OSError(2, "no such file") + exc.filename = "/foo" + debug(exc) + msg = f.getvalue() + assert "no such file" in msg + assert "/foo" in msg + + def test_cat_bcat(self): + testfn = self.get_testfn() + with open(testfn, "w") as f: + f.write("foo") + assert cat(testfn) == "foo" + assert bcat(testfn) == b"foo" + with pytest.raises(FileNotFoundError): + cat(testfn + '-invalid') + with pytest.raises(FileNotFoundError): + bcat(testfn + '-invalid') + assert cat(testfn + '-invalid', fallback="bar") == "bar" + assert bcat(testfn + '-invalid', fallback="bar") == "bar" + + +# =================================================================== +# --- Tests for wrap_numbers() function. +# =================================================================== + + +nt = collections.namedtuple('foo', 'a b c') + + +class TestWrapNumbers(PsutilTestCase): + def setUp(self): + wrap_numbers.cache_clear() + + tearDown = setUp + + def test_first_call(self): + input = {'disk1': nt(5, 5, 5)} + assert wrap_numbers(input, 'disk_io') == input + + def test_input_hasnt_changed(self): + input = {'disk1': nt(5, 5, 5)} + assert wrap_numbers(input, 'disk_io') == input + assert wrap_numbers(input, 'disk_io') == input + + def test_increase_but_no_wrap(self): + input = {'disk1': nt(5, 5, 5)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(10, 15, 20)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(20, 25, 30)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(20, 25, 30)} + assert wrap_numbers(input, 'disk_io') == input + + def test_wrap(self): + # let's say 100 is the threshold + input = {'disk1': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 110)} + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 110)} + # then it goes up + input = {'disk1': nt(100, 100, 90)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 190)} + # then it wraps again + input = {'disk1': nt(100, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 210)} + # and remains the same + input = {'disk1': nt(100, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(100, 100, 210)} + # now wrap another num + input = {'disk1': nt(50, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(150, 100, 210)} + # and again + input = {'disk1': nt(40, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(190, 100, 210)} + # keep it the same + input = {'disk1': nt(40, 100, 20)} + assert wrap_numbers(input, 'disk_io') == {'disk1': nt(190, 100, 210)} + + def test_changing_keys(self): + # Emulate a case where the second call to disk_io() + # (or whatever) provides a new disk, then the new disk + # disappears on the third call. + input = {'disk1': nt(5, 5, 5)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + assert wrap_numbers(input, 'disk_io') == input + input = {'disk1': nt(8, 8, 8)} + assert wrap_numbers(input, 'disk_io') == input + + def test_changing_keys_w_wrap(self): + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # disk 2 wraps + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == { + 'disk1': nt(50, 50, 50), + 'disk2': nt(100, 100, 110), + } + # disk 2 disappears + input = {'disk1': nt(50, 50, 50)} + assert wrap_numbers(input, 'disk_io') == input + + # then it appears again; the old wrap is supposed to be + # gone. + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # remains the same + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 100)} + assert wrap_numbers(input, 'disk_io') == input + # and then wraps again + input = {'disk1': nt(50, 50, 50), 'disk2': nt(100, 100, 10)} + assert wrap_numbers(input, 'disk_io') == { + 'disk1': nt(50, 50, 50), + 'disk2': nt(100, 100, 110), + } + + def test_real_data(self): + d = { + 'nvme0n1': (300, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), + 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), + 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), + 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348), + } + assert wrap_numbers(d, 'disk_io') == d + assert wrap_numbers(d, 'disk_io') == d + # decrease this ↓ + d = { + 'nvme0n1': (100, 508, 640, 1571, 5970, 1987, 2049, 451751, 47048), + 'nvme0n1p1': (1171, 2, 5600256, 1024, 516, 0, 0, 0, 8), + 'nvme0n1p2': (54, 54, 2396160, 5165056, 4, 24, 30, 1207, 28), + 'nvme0n1p3': (2389, 4539, 5154, 150, 4828, 1844, 2019, 398, 348), + } + out = wrap_numbers(d, 'disk_io') + assert out['nvme0n1'][0] == 400 + + # --- cache tests + + def test_cache_first_call(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + assert cache[1] == {'disk_io': {}} + assert cache[2] == {'disk_io': {}} + + def test_cache_call_twice(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(10, 10, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0} + } + assert cache[2] == {'disk_io': {}} + + def test_cache_wrap(self): + # let's say 100 is the threshold + input = {'disk1': nt(100, 100, 100)} + wrap_numbers(input, 'disk_io') + + # first wrap restarts from 10 + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 100} + } + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} + + def check_cache_info(): + cache = wrap_numbers.cache_info() + assert cache[1] == { + 'disk_io': { + ('disk1', 0): 0, + ('disk1', 1): 0, + ('disk1', 2): 100, + } + } + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} + + # then it remains the same + input = {'disk1': nt(100, 100, 10)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + check_cache_info() + + # then it goes up + input = {'disk1': nt(100, 100, 90)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + check_cache_info() + + # then it wraps again + input = {'disk1': nt(100, 100, 20)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 190} + } + assert cache[2] == {'disk_io': {'disk1': {('disk1', 2)}}} + + def test_cache_changing_keys(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + input = {'disk1': nt(5, 5, 5), 'disk2': nt(7, 7, 7)} + wrap_numbers(input, 'disk_io') + cache = wrap_numbers.cache_info() + assert cache[0] == {'disk_io': input} + assert cache[1] == { + 'disk_io': {('disk1', 0): 0, ('disk1', 1): 0, ('disk1', 2): 0} + } + assert cache[2] == {'disk_io': {}} + + def test_cache_clear(self): + input = {'disk1': nt(5, 5, 5)} + wrap_numbers(input, 'disk_io') + wrap_numbers(input, 'disk_io') + wrap_numbers.cache_clear('disk_io') + assert wrap_numbers.cache_info() == ({}, {}, {}) + wrap_numbers.cache_clear('disk_io') + wrap_numbers.cache_clear('?!?') + + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_cache_clear_public_apis(self): + if not psutil.disk_io_counters() or not psutil.net_io_counters(): + raise pytest.skip("no disks or NICs available") + psutil.disk_io_counters() + psutil.net_io_counters() + caches = wrap_numbers.cache_info() + for cache in caches: + assert 'psutil.disk_io_counters' in cache + assert 'psutil.net_io_counters' in cache + + psutil.disk_io_counters.cache_clear() + caches = wrap_numbers.cache_info() + for cache in caches: + assert 'psutil.net_io_counters' in cache + assert 'psutil.disk_io_counters' not in cache + + psutil.net_io_counters.cache_clear() + caches = wrap_numbers.cache_info() + assert caches == ({}, {}, {}) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_osx.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_osx.py new file mode 100644 index 0000000000000000000000000000000000000000..050418c5f9f107243cef4403d05cc27f9406b339 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_osx.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS specific tests.""" + +import platform +import re +import time + +import psutil +from psutil import MACOS +from psutil import POSIX +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if POSIX: + from psutil._psutil_posix import getpagesize + + +def sysctl(cmdline): + """Expects a sysctl command with an argument and parse the result + returning only the value of interest. + """ + out = sh(cmdline) + result = out.split()[1] + try: + return int(result) + except ValueError: + return result + + +def vm_stat(field): + """Wrapper around 'vm_stat' cmdline utility.""" + out = sh('vm_stat') + for line in out.split('\n'): + if field in line: + break + else: + raise ValueError("line not found") + return int(re.search(r'\d+', line).group(0)) * getpagesize() + + +@pytest.mark.skipif(not MACOS, reason="MACOS only") +class TestProcess(PsutilTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_process_create_time(self): + output = sh(f"ps -o lstart -p {self.pid}") + start_ps = output.replace('STARTED', '').strip() + hhmmss = start_ps.split(' ')[-2] + year = start_ps.split(' ')[-1] + start_psutil = psutil.Process(self.pid).create_time() + assert hhmmss == time.strftime( + "%H:%M:%S", time.localtime(start_psutil) + ) + assert year == time.strftime("%Y", time.localtime(start_psutil)) + + +@pytest.mark.skipif(not MACOS, reason="MACOS only") +class TestSystemAPIs(PsutilTestCase): + + # --- disk + + @retry_on_failure() + def test_disks(self): + # test psutil.disk_usage() and psutil.disk_partitions() + # against "df -a" + def df(path): + out = sh(f'df -k "{path}"').strip() + lines = out.split('\n') + lines.pop(0) + line = lines.pop(0) + dev, total, used, free = line.split()[:4] + if dev == 'none': + dev = '' + total = int(total) * 1024 + used = int(used) * 1024 + free = int(free) * 1024 + return dev, total, used, free + + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + dev, total, used, free = df(part.mountpoint) + assert part.device == dev + assert usage.total == total + assert abs(usage.free - free) < TOLERANCE_DISK_USAGE + assert abs(usage.used - used) < TOLERANCE_DISK_USAGE + + # --- cpu + + def test_cpu_count_logical(self): + num = sysctl("sysctl hw.logicalcpu") + assert num == psutil.cpu_count(logical=True) + + def test_cpu_count_cores(self): + num = sysctl("sysctl hw.physicalcpu") + assert num == psutil.cpu_count(logical=False) + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + def test_cpu_freq(self): + freq = psutil.cpu_freq() + assert freq.current * 1000 * 1000 == sysctl("sysctl hw.cpufrequency") + assert freq.min * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_min") + assert freq.max * 1000 * 1000 == sysctl("sysctl hw.cpufrequency_max") + + # --- virtual mem + + def test_vmem_total(self): + sysctl_hwphymem = sysctl('sysctl hw.memsize') + assert sysctl_hwphymem == psutil.virtual_memory().total + + @pytest.mark.skipif( + CI_TESTING and MACOS and platform.machine() == 'arm64', + reason="skipped on MACOS + ARM64 + CI_TESTING", + ) + @retry_on_failure() + def test_vmem_free(self): + vmstat_val = vm_stat("free") + psutil_val = psutil.virtual_memory().free + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_active(self): + vmstat_val = vm_stat("active") + psutil_val = psutil.virtual_memory().active + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_inactive(self): + vmstat_val = vm_stat("inactive") + psutil_val = psutil.virtual_memory().inactive + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_vmem_wired(self): + vmstat_val = vm_stat("wired") + psutil_val = psutil.virtual_memory().wired + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + # --- swap mem + + @retry_on_failure() + def test_swapmem_sin(self): + vmstat_val = vm_stat("Pageins") + psutil_val = psutil.swap_memory().sin + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + @retry_on_failure() + def test_swapmem_sout(self): + vmstat_val = vm_stat("Pageout") + psutil_val = psutil.swap_memory().sout + assert abs(psutil_val - vmstat_val) < TOLERANCE_SYS_MEM + + # --- network + + def test_net_if_stats(self): + for name, stats in psutil.net_if_stats().items(): + try: + out = sh(f"ifconfig {name}") + except RuntimeError: + pass + else: + assert stats.isup == ('RUNNING' in out), out + assert stats.mtu == int(re.findall(r'mtu (\d+)', out)[0]) + + # --- sensors_battery + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + out = sh("pmset -g batt") + percent = re.search(r"(\d+)%", out).group(1) + drawing_from = re.search(r"Now drawing from '([^']+)'", out).group(1) + power_plugged = drawing_from == "AC Power" + psutil_result = psutil.sensors_battery() + assert psutil_result.power_plugged == power_plugged + assert psutil_result.percent == int(percent) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_posix.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_posix.py new file mode 100644 index 0000000000000000000000000000000000000000..a7844929e767638086da0ebd85c27ab393f5c7ba --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_posix.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""POSIX specific tests.""" + +import datetime +import errno +import os +import re +import shutil +import subprocess +import time +from unittest import mock + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil.tests import AARCH64 +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import PYTHON_EXE +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if POSIX: + import mmap + import resource + + from psutil._psutil_posix import getpagesize + + +def ps(fmt, pid=None): + """Wrapper for calling the ps command with a little bit of cross-platform + support for a narrow range of features. + """ + + cmd = ['ps'] + + if LINUX: + cmd.append('--no-headers') + + if pid is not None: + cmd.extend(['-p', str(pid)]) + elif SUNOS or AIX: + cmd.append('-A') + else: + cmd.append('ax') + + if SUNOS: + fmt = fmt.replace("start", "stime") + + cmd.extend(['-o', fmt]) + + output = sh(cmd) + + output = output.splitlines() if LINUX else output.splitlines()[1:] + + all_output = [] + for line in output: + line = line.strip() + + try: + line = int(line) + except ValueError: + pass + + all_output.append(line) + + if pid is None: + return all_output + else: + return all_output[0] + + +# ps "-o" field names differ wildly between platforms. +# "comm" means "only executable name" but is not available on BSD platforms. +# "args" means "command with all its arguments", and is also not available +# on BSD platforms. +# "command" is like "args" on most platforms, but like "comm" on AIX, +# and not available on SUNOS. +# so for the executable name we can use "comm" on Solaris and split "command" +# on other platforms. +# to get the cmdline (with args) we have to use "args" on AIX and +# Solaris, and can use "command" on all others. + + +def ps_name(pid): + field = "command" + if SUNOS: + field = "comm" + command = ps(field, pid).split() + return command[0] + + +def ps_args(pid): + field = "command" + if AIX or SUNOS: + field = "args" + out = ps(field, pid) + # observed on BSD + Github CI: '/usr/local/bin/python3 -E -O (python3.9)' + out = re.sub(r"\(python.*?\)$", "", out) + return out.strip() + + +def ps_rss(pid): + field = "rss" + if AIX: + field = "rssize" + return ps(field, pid) + + +def ps_vsz(pid): + field = "vsz" + if AIX: + field = "vsize" + return ps(field, pid) + + +def df(device): + try: + out = sh(f"df -k {device}").strip() + except RuntimeError as err: + if "device busy" in str(err).lower(): + raise pytest.skip("df returned EBUSY") + raise + line = out.split('\n')[1] + fields = line.split() + sys_total = int(fields[1]) * 1024 + sys_used = int(fields[2]) * 1024 + sys_free = int(fields[3]) * 1024 + sys_percent = float(fields[4].replace('%', '')) + return (sys_total, sys_used, sys_free, sys_percent) + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestProcess(PsutilTestCase): + """Compare psutil results against 'ps' command line utility (mainly).""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc( + [PYTHON_EXE, "-E", "-O"], stdin=subprocess.PIPE + ).pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_ppid(self): + ppid_ps = ps('ppid', self.pid) + ppid_psutil = psutil.Process(self.pid).ppid() + assert ppid_ps == ppid_psutil + + def test_uid(self): + uid_ps = ps('uid', self.pid) + uid_psutil = psutil.Process(self.pid).uids().real + assert uid_ps == uid_psutil + + def test_gid(self): + gid_ps = ps('rgid', self.pid) + gid_psutil = psutil.Process(self.pid).gids().real + assert gid_ps == gid_psutil + + def test_username(self): + username_ps = ps('user', self.pid) + username_psutil = psutil.Process(self.pid).username() + assert username_ps == username_psutil + + def test_username_no_resolution(self): + # Emulate a case where the system can't resolve the uid to + # a username in which case psutil is supposed to return + # the stringified uid. + p = psutil.Process() + with mock.patch("psutil.pwd.getpwuid", side_effect=KeyError) as fun: + assert p.username() == str(p.uids().real) + assert fun.called + + @skip_on_access_denied() + @retry_on_failure() + def test_rss_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + rss_ps = ps_rss(self.pid) + rss_psutil = psutil.Process(self.pid).memory_info()[0] / 1024 + assert rss_ps == rss_psutil + + @skip_on_access_denied() + @retry_on_failure() + def test_vsz_memory(self): + # give python interpreter some time to properly initialize + # so that the results are the same + time.sleep(0.1) + vsz_ps = ps_vsz(self.pid) + vsz_psutil = psutil.Process(self.pid).memory_info()[1] / 1024 + assert vsz_ps == vsz_psutil + + def test_name(self): + name_ps = ps_name(self.pid) + # remove path if there is any, from the command + name_ps = os.path.basename(name_ps).lower() + name_psutil = psutil.Process(self.pid).name().lower() + # ...because of how we calculate PYTHON_EXE; on MACOS this may + # be "pythonX.Y". + name_ps = re.sub(r"\d.\d", "", name_ps) + name_psutil = re.sub(r"\d.\d", "", name_psutil) + # ...may also be "python.X" + name_ps = re.sub(r"\d", "", name_ps) + name_psutil = re.sub(r"\d", "", name_psutil) + assert name_ps == name_psutil + + def test_name_long(self): + # On UNIX the kernel truncates the name to the first 15 + # characters. In such a case psutil tries to determine the + # full name from the cmdline. + name = "long-program-name" + cmdline = ["long-program-name-extended", "foo", "bar"] + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", return_value=cmdline + ): + p = psutil.Process() + assert p.name() == "long-program-name-extended" + + def test_name_long_cmdline_ad_exc(self): + # Same as above but emulates a case where cmdline() raises + # AccessDenied in which case psutil is supposed to return + # the truncated name instead of crashing. + name = "long-program-name" + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", + side_effect=psutil.AccessDenied(0, ""), + ): + p = psutil.Process() + assert p.name() == "long-program-name" + + def test_name_long_cmdline_nsp_exc(self): + # Same as above but emulates a case where cmdline() raises NSP + # which is supposed to propagate. + name = "long-program-name" + with mock.patch("psutil._psplatform.Process.name", return_value=name): + with mock.patch( + "psutil._psplatform.Process.cmdline", + side_effect=psutil.NoSuchProcess(0, ""), + ): + p = psutil.Process() + with pytest.raises(psutil.NoSuchProcess): + p.name() + + @pytest.mark.skipif(MACOS or BSD, reason="ps -o start not available") + def test_create_time(self): + time_ps = ps('start', self.pid) + time_psutil = psutil.Process(self.pid).create_time() + time_psutil_tstamp = datetime.datetime.fromtimestamp( + time_psutil + ).strftime("%H:%M:%S") + # sometimes ps shows the time rounded up instead of down, so we check + # for both possible values + round_time_psutil = round(time_psutil) + round_time_psutil_tstamp = datetime.datetime.fromtimestamp( + round_time_psutil + ).strftime("%H:%M:%S") + assert time_ps in {time_psutil_tstamp, round_time_psutil_tstamp} + + def test_exe(self): + ps_pathname = ps_name(self.pid) + psutil_pathname = psutil.Process(self.pid).exe() + try: + assert ps_pathname == psutil_pathname + except AssertionError: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python3.7" + # ...instead of: + # "/usr/local/bin/python" + # We do not want to consider this difference in accuracy + # an error. + adjusted_ps_pathname = ps_pathname[: len(ps_pathname)] + assert ps_pathname == adjusted_ps_pathname + + # On macOS the official python installer exposes a python wrapper that + # executes a python executable hidden inside an application bundle inside + # the Python framework. + # There's a race condition between the ps call & the psutil call below + # depending on the completion of the execve call so let's retry on failure + @retry_on_failure() + def test_cmdline(self): + ps_cmdline = ps_args(self.pid) + psutil_cmdline = " ".join(psutil.Process(self.pid).cmdline()) + if AARCH64 and len(ps_cmdline) < len(psutil_cmdline): + assert psutil_cmdline.startswith(ps_cmdline) + else: + assert ps_cmdline == psutil_cmdline + + # On SUNOS "ps" reads niceness /proc/pid/psinfo which returns an + # incorrect value (20); the real deal is getpriority(2) which + # returns 0; psutil relies on it, see: + # https://github.com/giampaolo/psutil/issues/1082 + # AIX has the same issue + @pytest.mark.skipif(SUNOS, reason="not reliable on SUNOS") + @pytest.mark.skipif(AIX, reason="not reliable on AIX") + def test_nice(self): + ps_nice = ps('nice', self.pid) + psutil_nice = psutil.Process().nice() + assert ps_nice == psutil_nice + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestSystemAPIs(PsutilTestCase): + """Test some system APIs.""" + + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + pids_ps = sorted(ps("pid")) + pids_psutil = psutil.pids() + + # on MACOS and OPENBSD ps doesn't show pid 0 + if MACOS or (OPENBSD and 0 not in pids_ps): + pids_ps.insert(0, 0) + + # There will often be one more process in pids_ps for ps itself + if len(pids_ps) - len(pids_psutil) > 1: + difference = [x for x in pids_psutil if x not in pids_ps] + [ + x for x in pids_ps if x not in pids_psutil + ] + raise self.fail("difference: " + str(difference)) + + # for some reason ifconfig -a does not report all interfaces + # returned by psutil + @pytest.mark.skipif(SUNOS, reason="unreliable on SUNOS") + @pytest.mark.skipif(not shutil.which("ifconfig"), reason="no ifconfig cmd") + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_nic_names(self): + output = sh("ifconfig -a") + for nic in psutil.net_io_counters(pernic=True): + for line in output.split(): + if line.startswith(nic): + break + else: + raise self.fail( + f"couldn't find {nic} nic in 'ifconfig -a'" + f" output\n{output}" + ) + + # @pytest.mark.skipif(CI_TESTING and not psutil.users(), + # reason="unreliable on CI") + @retry_on_failure() + def test_users(self): + out = sh("who -u") + if not out.strip(): + raise pytest.skip("no users on this system") + lines = out.split('\n') + users = [x.split()[0] for x in lines] + terminals = [x.split()[1] for x in lines] + assert len(users) == len(psutil.users()) + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + assert u.name == users[idx] + assert u.terminal == terminals[idx] + if u.pid is not None: # None on OpenBSD + psutil.Process(u.pid) + + @retry_on_failure() + def test_users_started(self): + out = sh("who -u") + if not out.strip(): + raise pytest.skip("no users on this system") + tstamp = None + # '2023-04-11 09:31' (Linux) + started = re.findall(r"\d\d\d\d-\d\d-\d\d \d\d:\d\d", out) + if started: + tstamp = "%Y-%m-%d %H:%M" + else: + # 'Apr 10 22:27' (macOS) + started = re.findall(r"[A-Z][a-z][a-z] \d\d \d\d:\d\d", out) + if started: + tstamp = "%b %d %H:%M" + else: + # 'Apr 10' + started = re.findall(r"[A-Z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + else: + # 'apr 10' (sunOS) + started = re.findall(r"[a-z][a-z][a-z] \d\d", out) + if started: + tstamp = "%b %d" + started = [x.capitalize() for x in started] + + if not tstamp: + raise pytest.skip(f"cannot interpret tstamp in who output\n{out}") + + with self.subTest(psutil=psutil.users(), who=out): + for idx, u in enumerate(psutil.users()): + psutil_value = datetime.datetime.fromtimestamp( + u.started + ).strftime(tstamp) + assert psutil_value == started[idx] + + def test_pid_exists_let_raise(self): + # According to "man 2 kill" possible error values for kill + # are (EINVAL, EPERM, ESRCH). Test that any other errno + # results in an exception. + with mock.patch( + "psutil._psposix.os.kill", side_effect=OSError(errno.EBADF, "") + ) as m: + with pytest.raises(OSError): + psutil._psposix.pid_exists(os.getpid()) + assert m.called + + def test_os_waitpid_let_raise(self): + # os.waitpid() is supposed to catch EINTR and ECHILD only. + # Test that any other errno results in an exception. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EBADF, "") + ) as m: + with pytest.raises(OSError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + def test_os_waitpid_eintr(self): + # os.waitpid() is supposed to "retry" on EINTR. + with mock.patch( + "psutil._psposix.os.waitpid", side_effect=OSError(errno.EINTR, "") + ) as m: + with pytest.raises(psutil._psposix.TimeoutExpired): + psutil._psposix.wait_pid(os.getpid(), timeout=0.01) + assert m.called + + def test_os_waitpid_bad_ret_status(self): + # Simulate os.waitpid() returning a bad status. + with mock.patch( + "psutil._psposix.os.waitpid", return_value=(1, -1) + ) as m: + with pytest.raises(ValueError): + psutil._psposix.wait_pid(os.getpid()) + assert m.called + + # AIX can return '-' in df output instead of numbers, e.g. for /proc + @pytest.mark.skipif(AIX, reason="unreliable on AIX") + @retry_on_failure() + def test_disk_usage(self): + tolerance = 4 * 1024 * 1024 # 4MB + for part in psutil.disk_partitions(all=False): + usage = psutil.disk_usage(part.mountpoint) + try: + sys_total, sys_used, sys_free, sys_percent = df(part.device) + except RuntimeError as err: + # see: + # https://travis-ci.org/giampaolo/psutil/jobs/138338464 + # https://travis-ci.org/giampaolo/psutil/jobs/138343361 + err = str(err).lower() + if ( + "no such file or directory" in err + or "raw devices not supported" in err + or "permission denied" in err + ): + continue + raise + else: + assert abs(usage.total - sys_total) < tolerance + assert abs(usage.used - sys_used) < tolerance + assert abs(usage.free - sys_free) < tolerance + assert abs(usage.percent - sys_percent) <= 1 + + +@pytest.mark.skipif(not POSIX, reason="POSIX only") +class TestMisc(PsutilTestCase): + def test_getpagesize(self): + pagesize = getpagesize() + assert pagesize > 0 + assert pagesize == resource.getpagesize() + assert pagesize == mmap.PAGESIZE diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process.py new file mode 100644 index 0000000000000000000000000000000000000000..9ba1ba0e3bc7934a62011fcb89f880020ff984a0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process.py @@ -0,0 +1,1667 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for psutil.Process class.""" + +import collections +import contextlib +import errno +import getpass +import io +import itertools +import os +import signal +import socket +import stat +import string +import subprocess +import sys +import textwrap +import time +from unittest import mock + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import WINDOWS +from psutil._common import open_text +from psutil.tests import CI_TESTING +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_CPU_AFFINITY +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_IONICE +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_PROC_CPU_NUM +from psutil.tests import HAS_PROC_IO_COUNTERS +from psutil.tests import HAS_RLIMIT +from psutil.tests import HAS_THREADS +from psutil.tests import MACOS_11PLUS +from psutil.tests import PYPY +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import ThreadTask +from psutil.tests import call_until +from psutil.tests import copyload_shared_lib +from psutil.tests import create_c_exe +from psutil.tests import create_py_exe +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import skip_on_access_denied +from psutil.tests import skip_on_not_implemented +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- psutil.Process class tests +# =================================================================== + + +class TestProcess(PsutilTestCase): + """Tests for psutil.Process class.""" + + def spawn_psproc(self, *args, **kwargs): + sproc = self.spawn_testproc(*args, **kwargs) + try: + return psutil.Process(sproc.pid) + except psutil.NoSuchProcess: + self.assertPidGone(sproc.pid) + raise + + # --- + + def test_pid(self): + p = psutil.Process() + assert p.pid == os.getpid() + with pytest.raises(AttributeError): + p.pid = 33 + + def test_kill(self): + p = self.spawn_psproc() + p.kill() + code = p.wait() + if WINDOWS: + assert code == signal.SIGTERM + else: + assert code == -signal.SIGKILL + self.assertProcessGone(p) + + def test_terminate(self): + p = self.spawn_psproc() + p.terminate() + code = p.wait() + if WINDOWS: + assert code == signal.SIGTERM + else: + assert code == -signal.SIGTERM + self.assertProcessGone(p) + + def test_send_signal(self): + sig = signal.SIGKILL if POSIX else signal.SIGTERM + p = self.spawn_psproc() + p.send_signal(sig) + code = p.wait() + if WINDOWS: + assert code == sig + else: + assert code == -sig + self.assertProcessGone(p) + + @pytest.mark.skipif(not POSIX, reason="not POSIX") + def test_send_signal_mocked(self): + sig = signal.SIGTERM + p = self.spawn_psproc() + with mock.patch('psutil.os.kill', side_effect=ProcessLookupError): + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(sig) + + p = self.spawn_psproc() + with mock.patch('psutil.os.kill', side_effect=PermissionError): + with pytest.raises(psutil.AccessDenied): + p.send_signal(sig) + + def test_wait_exited(self): + # Test waitpid() + WIFEXITED -> WEXITSTATUS. + # normal return, same as exit(0) + cmd = [PYTHON_EXE, "-c", "pass"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 0 + self.assertProcessGone(p) + # exit(1), implicit in case of error + cmd = [PYTHON_EXE, "-c", "1 / 0"] + p = self.spawn_psproc(cmd, stderr=subprocess.PIPE) + code = p.wait() + assert code == 1 + self.assertProcessGone(p) + # via sys.exit() + cmd = [PYTHON_EXE, "-c", "import sys; sys.exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assertProcessGone(p) + # via os._exit() + cmd = [PYTHON_EXE, "-c", "import os; os._exit(5);"] + p = self.spawn_psproc(cmd) + code = p.wait() + assert code == 5 + self.assertProcessGone(p) + + @pytest.mark.skipif(NETBSD, reason="fails on NETBSD") + def test_wait_stopped(self): + p = self.spawn_psproc() + if POSIX: + # Test waitpid() + WIFSTOPPED and WIFCONTINUED. + # Note: if a process is stopped it ignores SIGTERM. + p.send_signal(signal.SIGSTOP) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGCONT) + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.send_signal(signal.SIGTERM) + assert p.wait() == -signal.SIGTERM + assert p.wait() == -signal.SIGTERM + else: + p.suspend() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.resume() + with pytest.raises(psutil.TimeoutExpired): + p.wait(timeout=0.001) + p.terminate() + assert p.wait() == signal.SIGTERM + assert p.wait() == signal.SIGTERM + + def test_wait_non_children(self): + # Test wait() against a process which is not our direct + # child. + child, grandchild = self.spawn_children_pair() + with pytest.raises(psutil.TimeoutExpired): + child.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + grandchild.wait(0.01) + # We also terminate the direct child otherwise the + # grandchild will hang until the parent is gone. + child.terminate() + grandchild.terminate() + child_ret = child.wait() + grandchild_ret = grandchild.wait() + if POSIX: + assert child_ret == -signal.SIGTERM + # For processes which are not our children we're supposed + # to get None. + assert grandchild_ret is None + else: + assert child_ret == signal.SIGTERM + assert child_ret == signal.SIGTERM + + def test_wait_timeout(self): + p = self.spawn_psproc() + p.name() + with pytest.raises(psutil.TimeoutExpired): + p.wait(0.01) + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + with pytest.raises(ValueError): + p.wait(-1) + + def test_wait_timeout_nonblocking(self): + p = self.spawn_psproc() + with pytest.raises(psutil.TimeoutExpired): + p.wait(0) + p.kill() + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + try: + code = p.wait(0) + break + except psutil.TimeoutExpired: + pass + else: + raise self.fail('timeout') + if POSIX: + assert code == -signal.SIGKILL + else: + assert code == signal.SIGTERM + self.assertProcessGone(p) + + def test_cpu_percent(self): + p = psutil.Process() + p.cpu_percent(interval=0.001) + p.cpu_percent(interval=0.001) + for _ in range(100): + percent = p.cpu_percent(interval=None) + assert isinstance(percent, float) + assert percent >= 0.0 + with pytest.raises(ValueError): + p.cpu_percent(interval=-1) + + def test_cpu_percent_numcpus_none(self): + # See: https://github.com/giampaolo/psutil/issues/1087 + with mock.patch('psutil.cpu_count', return_value=None) as m: + psutil.Process().cpu_percent() + assert m.called + + def test_cpu_times(self): + times = psutil.Process().cpu_times() + assert times.user >= 0.0, times + assert times.system >= 0.0, times + assert times.children_user >= 0.0, times + assert times.children_system >= 0.0, times + if LINUX: + assert times.iowait >= 0.0, times + # make sure returned values can be pretty printed with strftime + for name in times._fields: + time.strftime("%H:%M:%S", time.localtime(getattr(times, name))) + + def test_cpu_times_2(self): + def waste_cpu(): + stop_at = os.times().user + 0.2 + while os.times().user < stop_at: + for x in range(100000): + x **= 2 + + waste_cpu() + a = psutil.Process().cpu_times() + b = os.times() + self.assertAlmostEqual(a.user, b.user, delta=0.1) + self.assertAlmostEqual(a.system, b.system, delta=0.1) + + @pytest.mark.skipif(not HAS_PROC_CPU_NUM, reason="not supported") + def test_cpu_num(self): + p = psutil.Process() + num = p.cpu_num() + assert num >= 0 + if psutil.cpu_count() == 1: + assert num == 0 + assert p.cpu_num() in range(psutil.cpu_count()) + + def test_create_time(self): + p = self.spawn_psproc() + now = time.time() + create_time = p.create_time() + + # Use time.time() as base value to compare our result using a + # tolerance of +/- 1 second. + # It will fail if the difference between the values is > 2s. + difference = abs(create_time - now) + if difference > 2: + raise self.fail( + f"expected: {now}, found: {create_time}, difference:" + f" {difference}" + ) + + # make sure returned value can be pretty printed with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(p.create_time())) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_terminal(self): + terminal = psutil.Process().terminal() + if terminal is not None: + try: + tty = os.path.realpath(sh('tty')) + except RuntimeError: + # Note: happens if pytest is run without the `-s` opt. + raise pytest.skip("can't rely on `tty` CLI") + else: + assert terminal == tty + + @pytest.mark.skipif(not HAS_PROC_IO_COUNTERS, reason="not supported") + @skip_on_not_implemented(only_if=LINUX) + def test_io_counters(self): + p = psutil.Process() + # test reads + io1 = p.io_counters() + with open(PYTHON_EXE, 'rb') as f: + f.read() + io2 = p.io_counters() + if not BSD and not AIX: + assert io2.read_count > io1.read_count + assert io2.write_count == io1.write_count + if LINUX: + assert io2.read_chars > io1.read_chars + assert io2.write_chars == io1.write_chars + else: + assert io2.read_bytes >= io1.read_bytes + assert io2.write_bytes >= io1.write_bytes + + # test writes + io1 = p.io_counters() + with open(self.get_testfn(), 'wb') as f: + f.write(bytes("x" * 1000000, 'ascii')) + io2 = p.io_counters() + assert io2.write_count >= io1.write_count + assert io2.write_bytes >= io1.write_bytes + assert io2.read_count >= io1.read_count + assert io2.read_bytes >= io1.read_bytes + if LINUX: + assert io2.write_chars > io1.write_chars + assert io2.read_chars >= io1.read_chars + + # sanity check + for i in range(len(io2)): + if BSD and i >= 2: + # On BSD read_bytes and write_bytes are always set to -1. + continue + assert io2[i] >= 0 + assert io2[i] >= 0 + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif(not LINUX, reason="linux only") + def test_ionice_linux(self): + def cleanup(init): + ioclass, value = init + if ioclass == psutil.IOPRIO_CLASS_NONE: + value = 0 + p.ionice(ioclass, value) + + p = psutil.Process() + if not CI_TESTING: + assert p.ionice()[0] == psutil.IOPRIO_CLASS_NONE + assert psutil.IOPRIO_CLASS_NONE == 0 + assert psutil.IOPRIO_CLASS_RT == 1 # high + assert psutil.IOPRIO_CLASS_BE == 2 # normal + assert psutil.IOPRIO_CLASS_IDLE == 3 # low + init = p.ionice() + self.addCleanup(cleanup, init) + + # low + p.ionice(psutil.IOPRIO_CLASS_IDLE) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_IDLE, 0) + with pytest.raises(ValueError): # accepts no value + p.ionice(psutil.IOPRIO_CLASS_IDLE, value=7) + # normal + p.ionice(psutil.IOPRIO_CLASS_BE) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_BE, 0) + p.ionice(psutil.IOPRIO_CLASS_BE, value=7) + assert tuple(p.ionice()) == (psutil.IOPRIO_CLASS_BE, 7) + with pytest.raises(ValueError): + p.ionice(psutil.IOPRIO_CLASS_BE, value=8) + try: + p.ionice(psutil.IOPRIO_CLASS_RT, value=7) + except psutil.AccessDenied: + pass + # errs + with pytest.raises(ValueError, match="ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_NONE, 1) + with pytest.raises(ValueError, match="ioclass accepts no value"): + p.ionice(psutil.IOPRIO_CLASS_IDLE, 1) + with pytest.raises( + ValueError, match="'ioclass' argument must be specified" + ): + p.ionice(value=1) + + @pytest.mark.skipif(not HAS_IONICE, reason="not supported") + @pytest.mark.skipif( + not WINDOWS, reason="not supported on this win version" + ) + def test_ionice_win(self): + p = psutil.Process() + if not CI_TESTING: + assert p.ionice() == psutil.IOPRIO_NORMAL + init = p.ionice() + self.addCleanup(p.ionice, init) + + # base + p.ionice(psutil.IOPRIO_VERYLOW) + assert p.ionice() == psutil.IOPRIO_VERYLOW + p.ionice(psutil.IOPRIO_LOW) + assert p.ionice() == psutil.IOPRIO_LOW + try: + p.ionice(psutil.IOPRIO_HIGH) + except psutil.AccessDenied: + pass + else: + assert p.ionice() == psutil.IOPRIO_HIGH + # errs + with pytest.raises( + TypeError, match="value argument not accepted on Windows" + ): + p.ionice(psutil.IOPRIO_NORMAL, value=1) + with pytest.raises(ValueError, match="is not a valid priority"): + p.ionice(psutil.IOPRIO_HIGH + 1) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_get(self): + import resource + + p = psutil.Process(os.getpid()) + names = [x for x in dir(psutil) if x.startswith('RLIMIT')] + assert names, names + for name in names: + value = getattr(psutil, name) + assert value >= 0 + if name in dir(resource): + assert value == getattr(resource, name) + # XXX - On PyPy RLIMIT_INFINITY returned by + # resource.getrlimit() is reported as a very big long + # number instead of -1. It looks like a bug with PyPy. + if PYPY: + continue + assert p.rlimit(value) == resource.getrlimit(value) + else: + ret = p.rlimit(value) + assert len(ret) == 2 + assert ret[0] >= -1 + assert ret[1] >= -1 + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_set(self): + p = self.spawn_psproc() + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5)) + assert p.rlimit(psutil.RLIMIT_NOFILE) == (5, 5) + # If pid is 0 prlimit() applies to the calling process and + # we don't want that. + if LINUX: + with pytest.raises(ValueError, match="can't use prlimit"): + psutil._psplatform.Process(0).rlimit(0) + with pytest.raises(ValueError): + p.rlimit(psutil.RLIMIT_NOFILE, (5, 5, 5)) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit(self): + p = psutil.Process() + testfn = self.get_testfn() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + with open(testfn, "wb") as f: + f.write(b"X" * 1024) + # write() or flush() doesn't always cause the exception + # but close() will. + with pytest.raises(OSError) as exc: + with open(testfn, "wb") as f: + f.write(b"X" * 1025) + assert exc.value.errno == errno.EFBIG + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_infinity(self): + # First set a limit, then re-set it by specifying INFINITY + # and assume we overridden the previous limit. + p = psutil.Process() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + try: + p.rlimit(psutil.RLIMIT_FSIZE, (1024, hard)) + p.rlimit(psutil.RLIMIT_FSIZE, (psutil.RLIM_INFINITY, hard)) + with open(self.get_testfn(), "wb") as f: + f.write(b"X" * 2048) + finally: + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + assert p.rlimit(psutil.RLIMIT_FSIZE) == (soft, hard) + + @pytest.mark.skipif(not HAS_RLIMIT, reason="not supported") + def test_rlimit_infinity_value(self): + # RLIMIT_FSIZE should be RLIM_INFINITY, which will be a really + # big number on a platform with large file support. On these + # platforms we need to test that the get/setrlimit functions + # properly convert the number to a C long long and that the + # conversion doesn't raise an error. + p = psutil.Process() + soft, hard = p.rlimit(psutil.RLIMIT_FSIZE) + assert hard == psutil.RLIM_INFINITY + p.rlimit(psutil.RLIMIT_FSIZE, (soft, hard)) + + def test_num_threads(self): + # on certain platforms such as Linux we might test for exact + # thread number, since we always have with 1 thread per process, + # but this does not apply across all platforms (MACOS, Windows) + p = psutil.Process() + if OPENBSD: + try: + step1 = p.num_threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + else: + step1 = p.num_threads() + + with ThreadTask(): + step2 = p.num_threads() + assert step2 == step1 + 1 + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_num_handles(self): + # a better test is done later into test/_windows.py + p = psutil.Process() + assert p.num_handles() > 0 + + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + def test_threads(self): + p = psutil.Process() + if OPENBSD: + try: + step1 = p.threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + else: + step1 = p.threads() + + with ThreadTask(): + step2 = p.threads() + assert len(step2) == len(step1) + 1 + athread = step2[0] + # test named tuple + assert athread.id == athread[0] + assert athread.user_time == athread[1] + assert athread.system_time == athread[2] + + @retry_on_failure() + @skip_on_access_denied(only_if=MACOS) + @pytest.mark.skipif(not HAS_THREADS, reason="not supported") + def test_threads_2(self): + p = self.spawn_psproc() + if OPENBSD: + try: + p.threads() + except psutil.AccessDenied: + raise pytest.skip("on OpenBSD this requires root access") + assert ( + abs(p.cpu_times().user - sum(x.user_time for x in p.threads())) + < 0.1 + ) + assert ( + abs(p.cpu_times().system - sum(x.system_time for x in p.threads())) + < 0.1 + ) + + @retry_on_failure() + def test_memory_info(self): + p = psutil.Process() + + # step 1 - get a base value to compare our results + rss1, vms1 = p.memory_info()[:2] + percent1 = p.memory_percent() + assert rss1 > 0 + assert vms1 > 0 + + # step 2 - allocate some memory + memarr = [None] * 1500000 + + rss2, vms2 = p.memory_info()[:2] + percent2 = p.memory_percent() + + # step 3 - make sure that the memory usage bumped up + assert rss2 > rss1 + assert vms2 >= vms1 # vms might be equal + assert percent2 > percent1 + del memarr + + if WINDOWS: + mem = p.memory_info() + assert mem.rss == mem.wset + assert mem.vms == mem.pagefile + + mem = p.memory_info() + for name in mem._fields: + assert getattr(mem, name) >= 0 + + def test_memory_full_info(self): + p = psutil.Process() + total = psutil.virtual_memory().total + mem = p.memory_full_info() + for name in mem._fields: + value = getattr(mem, name) + assert value >= 0 + if (name == "vms" and OSX) or LINUX: + continue + assert value <= total + if LINUX or WINDOWS or MACOS: + assert mem.uss >= 0 + if LINUX: + assert mem.pss >= 0 + assert mem.swap >= 0 + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_memory_maps(self): + p = psutil.Process() + maps = p.memory_maps() + assert len(maps) == len(set(maps)) + ext_maps = p.memory_maps(grouped=False) + + for nt in maps: + if nt.path.startswith('['): + continue + if BSD and nt.path == "pvclock": + continue + assert os.path.isabs(nt.path), nt.path + + if POSIX: + try: + assert os.path.exists(nt.path) or os.path.islink( + nt.path + ), nt.path + except AssertionError: + if not LINUX: + raise + # https://github.com/giampaolo/psutil/issues/759 + with open_text('/proc/self/smaps') as f: + data = f.read() + if f"{nt.path} (deleted)" not in data: + raise + elif '64' not in os.path.basename(nt.path): + # XXX - On Windows we have this strange behavior with + # 64 bit dlls: they are visible via explorer but cannot + # be accessed via os.stat() (wtf?). + try: + st = os.stat(nt.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), nt.path + + for nt in ext_maps: + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + continue + if fname in {'addr', 'perms'}: + assert value, value + else: + assert isinstance(value, int) + assert value >= 0, value + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_memory_maps_lists_lib(self): + # Make sure a newly loaded shared lib is listed. + p = psutil.Process() + with copyload_shared_lib() as path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [normpath(x.path) for x in p.memory_maps()] + assert normpath(path) in libpaths + + def test_memory_percent(self): + p = psutil.Process() + p.memory_percent() + with pytest.raises(ValueError): + p.memory_percent(memtype="?!?") + if LINUX or MACOS or WINDOWS: + p.memory_percent(memtype='uss') + + def test_is_running(self): + p = self.spawn_psproc() + assert p.is_running() + assert p.is_running() + p.kill() + p.wait() + assert not p.is_running() + assert not p.is_running() + + def test_exe(self): + p = self.spawn_psproc() + exe = p.exe() + try: + assert exe == PYTHON_EXE + except AssertionError: + if WINDOWS and len(exe) == len(PYTHON_EXE): + # on Windows we don't care about case sensitivity + normcase = os.path.normcase + assert normcase(exe) == normcase(PYTHON_EXE) + else: + # certain platforms such as BSD are more accurate returning: + # "/usr/local/bin/python3.7" + # ...instead of: + # "/usr/local/bin/python" + # We do not want to consider this difference in accuracy + # an error. + ver = f"{sys.version_info[0]}.{sys.version_info[1]}" + try: + assert exe.replace(ver, '') == PYTHON_EXE.replace(ver, '') + except AssertionError: + # Typically MACOS. Really not sure what to do here. + pass + + out = sh([exe, "-c", "import os; print('hey')"]) + assert out == 'hey' + + def test_cmdline(self): + cmdline = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + p = self.spawn_psproc(cmdline) + + if NETBSD and p.cmdline() == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise pytest.skip("OPENBSD: returned EBUSY") + + # XXX - most of the times the underlying sysctl() call on Net + # and Open BSD returns a truncated string. + # Also /proc/pid/cmdline behaves the same so it looks + # like this is a kernel bug. + # XXX - AIX truncates long arguments in /proc/pid/cmdline + if NETBSD or OPENBSD or AIX: + assert p.cmdline()[0] == PYTHON_EXE + else: + if MACOS and CI_TESTING: + pyexe = p.cmdline()[0] + if pyexe != PYTHON_EXE: + assert ' '.join(p.cmdline()[1:]) == ' '.join(cmdline[1:]) + return + assert ' '.join(p.cmdline()) == ' '.join(cmdline) + + @pytest.mark.skipif(PYPY, reason="broken on PYPY") + def test_long_cmdline(self): + cmdline = [PYTHON_EXE] + cmdline.extend(["-v"] * 50) + cmdline.extend( + ["-c", "import time; [time.sleep(0.1) for x in range(100)]"] + ) + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). + try: + assert p.cmdline() == cmdline + except psutil.ZombieProcess: + raise pytest.skip("OPENBSD: process turned into zombie") + else: + ret = p.cmdline() + if NETBSD and ret == []: + # https://github.com/giampaolo/psutil/issues/2250 + raise pytest.skip("OPENBSD: returned EBUSY") + assert ret == cmdline + + def test_name(self): + p = self.spawn_psproc() + name = p.name().lower() + pyexe = os.path.basename(os.path.realpath(sys.executable)).lower() + assert pyexe.startswith(name), (pyexe, name) + + @pytest.mark.skipif(PYPY, reason="unreliable on PYPY") + def test_long_name(self): + pyexe = create_py_exe(self.get_testfn(suffix=string.digits * 2)) + cmdline = [ + pyexe, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + p = self.spawn_psproc(cmdline) + if OPENBSD: + # XXX: for some reason the test process may turn into a + # zombie (don't know why). Because the name() is long, all + # UNIX kernels truncate it to 15 chars, so internally psutil + # tries to guess the full name() from the cmdline(). But the + # cmdline() of a zombie on OpenBSD fails (internally), so we + # just compare the first 15 chars. Full explanation: + # https://github.com/giampaolo/psutil/issues/2239 + try: + assert p.name() == os.path.basename(pyexe) + except AssertionError: + if p.status() == psutil.STATUS_ZOMBIE: + assert os.path.basename(pyexe).startswith(p.name()) + else: + raise + else: + assert p.name() == os.path.basename(pyexe) + + # XXX: fails too often + # @pytest.mark.skipif(SUNOS, reason="broken on SUNOS") + # @pytest.mark.skipif(AIX, reason="broken on AIX") + # @pytest.mark.skipif(PYPY, reason="broken on PYPY") + # def test_prog_w_funky_name(self): + # # Test that name(), exe() and cmdline() correctly handle programs + # # with funky chars such as spaces and ")", see: + # # https://github.com/giampaolo/psutil/issues/628 + # pyexe = create_py_exe(self.get_testfn(suffix='foo bar )')) + # cmdline = [ + # pyexe, + # "-c", + # "import time; [time.sleep(0.1) for x in range(100)]", + # ] + # p = self.spawn_psproc(cmdline) + # assert p.cmdline() == cmdline + # assert p.name() == os.path.basename(pyexe) + # assert os.path.normcase(p.exe()) == os.path.normcase(pyexe) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_uids(self): + p = psutil.Process() + real, effective, _saved = p.uids() + # os.getuid() refers to "real" uid + assert real == os.getuid() + # os.geteuid() refers to "effective" uid + assert effective == os.geteuid() + # No such thing as os.getsuid() ("saved" uid), but we have + # os.getresuid() which returns all of them. + if hasattr(os, "getresuid"): + assert os.getresuid() == p.uids() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_gids(self): + p = psutil.Process() + real, effective, _saved = p.gids() + # os.getuid() refers to "real" uid + assert real == os.getgid() + # os.geteuid() refers to "effective" uid + assert effective == os.getegid() + # No such thing as os.getsgid() ("saved" gid), but we have + # os.getresgid() which returns all of them. + if hasattr(os, "getresuid"): + assert os.getresgid() == p.gids() + + def test_nice(self): + def cleanup(init): + try: + p.nice(init) + except psutil.AccessDenied: + pass + + p = psutil.Process() + with pytest.raises(TypeError): + p.nice("str") + init = p.nice() + self.addCleanup(cleanup, init) + + if WINDOWS: + highest_prio = None + for prio in [ + psutil.IDLE_PRIORITY_CLASS, + psutil.BELOW_NORMAL_PRIORITY_CLASS, + psutil.NORMAL_PRIORITY_CLASS, + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + ]: + with self.subTest(prio=prio): + try: + p.nice(prio) + except psutil.AccessDenied: + pass + else: + new_prio = p.nice() + # The OS may limit our maximum priority, + # even if the function succeeds. For higher + # priorities, we match either the expected + # value or the highest so far. + if prio in { + psutil.ABOVE_NORMAL_PRIORITY_CLASS, + psutil.HIGH_PRIORITY_CLASS, + psutil.REALTIME_PRIORITY_CLASS, + }: + if new_prio == prio or highest_prio is None: + highest_prio = prio + assert new_prio == highest_prio + else: + assert new_prio == prio + else: + try: + if hasattr(os, "getpriority"): + assert ( + os.getpriority(os.PRIO_PROCESS, os.getpid()) + == p.nice() + ) + p.nice(1) + assert p.nice() == 1 + if hasattr(os, "getpriority"): + assert ( + os.getpriority(os.PRIO_PROCESS, os.getpid()) + == p.nice() + ) + # XXX - going back to previous nice value raises + # AccessDenied on MACOS + if not MACOS: + p.nice(0) + assert p.nice() == 0 + except psutil.AccessDenied: + pass + + def test_status(self): + p = psutil.Process() + assert p.status() == psutil.STATUS_RUNNING + + def test_username(self): + p = self.spawn_psproc() + username = p.username() + if WINDOWS: + domain, username = username.split('\\') + getpass_user = getpass.getuser() + if getpass_user.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce + # the same result, causing the test to fail. + raise pytest.skip('running as service account') + assert username == getpass_user + if 'USERDOMAIN' in os.environ: + assert domain == os.environ['USERDOMAIN'] + else: + assert username == getpass.getuser() + + def test_cwd(self): + p = self.spawn_psproc() + assert p.cwd() == os.getcwd() + + def test_cwd_2(self): + cmd = [ + PYTHON_EXE, + "-c", + ( + "import os, time; os.chdir('..'); [time.sleep(0.1) for x in" + " range(100)]" + ), + ] + p = self.spawn_psproc(cmd) + call_until(lambda: p.cwd() == os.path.dirname(os.getcwd())) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity(self): + p = psutil.Process() + initial = p.cpu_affinity() + assert initial, initial + self.addCleanup(p.cpu_affinity, initial) + + if hasattr(os, "sched_getaffinity"): + assert initial == list(os.sched_getaffinity(p.pid)) + assert len(initial) == len(set(initial)) + + all_cpus = list(range(len(psutil.cpu_percent(percpu=True)))) + for n in all_cpus: + p.cpu_affinity([n]) + assert p.cpu_affinity() == [n] + if hasattr(os, "sched_getaffinity"): + assert p.cpu_affinity() == list(os.sched_getaffinity(p.pid)) + # also test num_cpu() + if hasattr(p, "num_cpu"): + assert p.cpu_affinity()[0] == p.num_cpu() + + # [] is an alias for "all eligible CPUs"; on Linux this may + # not be equal to all available CPUs, see: + # https://github.com/giampaolo/psutil/issues/956 + p.cpu_affinity([]) + if LINUX: + assert p.cpu_affinity() == p._proc._get_eligible_cpus() + else: + assert p.cpu_affinity() == all_cpus + if hasattr(os, "sched_getaffinity"): + assert p.cpu_affinity() == list(os.sched_getaffinity(p.pid)) + + with pytest.raises(TypeError): + p.cpu_affinity(1) + p.cpu_affinity(initial) + # it should work with all iterables, not only lists + p.cpu_affinity(set(all_cpus)) + p.cpu_affinity(tuple(all_cpus)) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_errs(self): + p = self.spawn_psproc() + invalid_cpu = [len(psutil.cpu_times(percpu=True)) + 10] + with pytest.raises(ValueError): + p.cpu_affinity(invalid_cpu) + with pytest.raises(ValueError): + p.cpu_affinity(range(10000, 11000)) + with pytest.raises((TypeError, ValueError)): + p.cpu_affinity([0, "1"]) + with pytest.raises(ValueError): + p.cpu_affinity([0, -1]) + + @pytest.mark.skipif(not HAS_CPU_AFFINITY, reason="not supported") + def test_cpu_affinity_all_combinations(self): + p = psutil.Process() + initial = p.cpu_affinity() + assert initial, initial + self.addCleanup(p.cpu_affinity, initial) + + # All possible CPU set combinations. + if len(initial) > 12: + initial = initial[:12] # ...otherwise it will take forever + combos = [] + for i in range(len(initial) + 1): + combos.extend( + list(subset) + for subset in itertools.combinations(initial, i) + if subset + ) + + for combo in combos: + p.cpu_affinity(combo) + assert sorted(p.cpu_affinity()) == sorted(combo) + + # TODO: #595 + @pytest.mark.skipif(BSD, reason="broken on BSD") + def test_open_files(self): + p = psutil.Process() + testfn = self.get_testfn() + files = p.open_files() + assert testfn not in files + with open(testfn, 'wb') as f: + f.write(b'x' * 1024) + f.flush() + # give the kernel some time to see the new file + call_until(lambda: len(p.open_files()) != len(files)) + files = p.open_files() + filenames = [os.path.normcase(x.path) for x in files] + assert os.path.normcase(testfn) in filenames + if LINUX: + for file in files: + if file.path == testfn: + assert file.position == 1024 + for file in files: + assert os.path.isfile(file.path), file + + # another process + cmdline = ( + f"import time; f = open(r'{testfn}', 'r'); [time.sleep(0.1) for x" + " in range(100)];" + ) + p = self.spawn_psproc([PYTHON_EXE, "-c", cmdline]) + + for x in range(100): + filenames = [os.path.normcase(x.path) for x in p.open_files()] + if testfn in filenames: + break + time.sleep(0.01) + else: + assert os.path.normcase(testfn) in filenames + for file in filenames: + assert os.path.isfile(file), file + + # TODO: #595 + @pytest.mark.skipif(BSD, reason="broken on BSD") + def test_open_files_2(self): + # test fd and path fields + p = psutil.Process() + normcase = os.path.normcase + testfn = self.get_testfn() + with open(testfn, 'w') as fileobj: + for file in p.open_files(): + if ( + normcase(file.path) == normcase(fileobj.name) + or file.fd == fileobj.fileno() + ): + break + else: + raise self.fail(f"no file found; files={p.open_files()!r}") + assert normcase(file.path) == normcase(fileobj.name) + if WINDOWS: + assert file.fd == -1 + else: + assert file.fd == fileobj.fileno() + # test positions + ntuple = p.open_files()[0] + assert ntuple[0] == ntuple.path + assert ntuple[1] == ntuple.fd + # test file is gone + assert fileobj.name not in p.open_files() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_num_fds(self): + p = psutil.Process() + testfn = self.get_testfn() + start = p.num_fds() + file = open(testfn, 'w') # noqa: SIM115 + self.addCleanup(file.close) + assert p.num_fds() == start + 1 + sock = socket.socket() + self.addCleanup(sock.close) + assert p.num_fds() == start + 2 + file.close() + sock.close() + assert p.num_fds() == start + + @skip_on_not_implemented(only_if=LINUX) + @pytest.mark.skipif( + OPENBSD or NETBSD, reason="not reliable on OPENBSD & NETBSD" + ) + def test_num_ctx_switches(self): + p = psutil.Process() + before = sum(p.num_ctx_switches()) + for _ in range(2): + time.sleep(0.05) # this shall ensure a context switch happens + after = sum(p.num_ctx_switches()) + if after > before: + return + raise self.fail("num ctx switches still the same after 2 iterations") + + def test_ppid(self): + p = psutil.Process() + if hasattr(os, 'getppid'): + assert p.ppid() == os.getppid() + p = self.spawn_psproc() + assert p.ppid() == os.getpid() + + def test_parent(self): + p = self.spawn_psproc() + assert p.parent().pid == os.getpid() + + lowest_pid = psutil.pids()[0] + assert psutil.Process(lowest_pid).parent() is None + + def test_parent_multi(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + assert grandchild.parent() == child + assert child.parent() == parent + + @retry_on_failure() + def test_parents(self): + parent = psutil.Process() + assert parent.parents() + child, grandchild = self.spawn_children_pair() + assert child.parents()[0] == parent + assert grandchild.parents()[0] == child + assert grandchild.parents()[1] == parent + + def test_children(self): + parent = psutil.Process() + assert not parent.children() + assert not parent.children(recursive=True) + # On Windows we set the flag to 0 in order to cancel out the + # CREATE_NO_WINDOW flag (enabled by default) which creates + # an extra "conhost.exe" child. + child = self.spawn_psproc(creationflags=0) + children1 = parent.children() + children2 = parent.children(recursive=True) + for children in (children1, children2): + assert len(children) == 1 + assert children[0].pid == child.pid + assert children[0].ppid() == parent.pid + + def test_children_recursive(self): + # Test children() against two sub processes, p1 and p2, where + # p1 (our child) spawned p2 (our grandchild). + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + assert parent.children() == [child] + assert parent.children(recursive=True) == [child, grandchild] + # If the intermediate process is gone there's no way for + # children() to recursively find it. + child.terminate() + child.wait() + assert not parent.children(recursive=True) + + def test_children_duplicates(self): + # find the process which has the highest number of children + table = collections.defaultdict(int) + for p in psutil.process_iter(): + try: + table[p.ppid()] += 1 + except psutil.Error: + pass + # this is the one, now let's make sure there are no duplicates + pid = max(table.items(), key=lambda x: x[1])[0] + if LINUX and pid == 0: + raise pytest.skip("PID 0") + p = psutil.Process(pid) + try: + c = p.children(recursive=True) + except psutil.AccessDenied: # windows + pass + else: + assert len(c) == len(set(c)) + + def test_parents_and_children(self): + parent = psutil.Process() + child, grandchild = self.spawn_children_pair() + # forward + children = parent.children(recursive=True) + assert len(children) == 2 + assert children[0] == child + assert children[1] == grandchild + # backward + parents = grandchild.parents() + assert parents[0] == child + assert parents[1] == parent + + def test_suspend_resume(self): + p = self.spawn_psproc() + p.suspend() + for _ in range(100): + if p.status() == psutil.STATUS_STOPPED: + break + time.sleep(0.01) + p.resume() + assert p.status() != psutil.STATUS_STOPPED + + def test_invalid_pid(self): + with pytest.raises(TypeError): + psutil.Process("1") + with pytest.raises(ValueError): + psutil.Process(-1) + + def test_as_dict(self): + p = psutil.Process() + d = p.as_dict(attrs=['exe', 'name']) + assert sorted(d.keys()) == ['exe', 'name'] + + p = psutil.Process(min(psutil.pids())) + d = p.as_dict(attrs=['net_connections'], ad_value='foo') + if not isinstance(d['net_connections'], list): + assert d['net_connections'] == 'foo' + + # Test ad_value is set on AccessDenied. + with mock.patch( + 'psutil.Process.nice', create=True, side_effect=psutil.AccessDenied + ): + assert p.as_dict(attrs=["nice"], ad_value=1) == {"nice": 1} + + # Test that NoSuchProcess bubbles up. + with mock.patch( + 'psutil.Process.nice', + create=True, + side_effect=psutil.NoSuchProcess(p.pid, "name"), + ): + with pytest.raises(psutil.NoSuchProcess): + p.as_dict(attrs=["nice"]) + + # Test that ZombieProcess is swallowed. + with mock.patch( + 'psutil.Process.nice', + create=True, + side_effect=psutil.ZombieProcess(p.pid, "name"), + ): + assert p.as_dict(attrs=["nice"], ad_value="foo") == {"nice": "foo"} + + # By default APIs raising NotImplementedError are + # supposed to be skipped. + with mock.patch( + 'psutil.Process.nice', create=True, side_effect=NotImplementedError + ): + d = p.as_dict() + assert 'nice' not in list(d.keys()) + # ...unless the user explicitly asked for some attr. + with pytest.raises(NotImplementedError): + p.as_dict(attrs=["nice"]) + + # errors + with pytest.raises(TypeError): + p.as_dict('name') + with pytest.raises(ValueError): + p.as_dict(['foo']) + with pytest.raises(ValueError): + p.as_dict(['foo', 'bar']) + + def test_oneshot(self): + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + assert m.call_count == 1 + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + assert m.call_count == 2 + + def test_oneshot_twice(self): + # Test the case where the ctx manager is __enter__ed twice. + # The second __enter__ is supposed to resut in a NOOP. + p = psutil.Process() + with mock.patch("psutil._psplatform.Process.cpu_times") as m1: + with mock.patch("psutil._psplatform.Process.oneshot_enter") as m2: + with p.oneshot(): + p.cpu_times() + p.cpu_times() + with p.oneshot(): + p.cpu_times() + p.cpu_times() + assert m1.call_count == 1 + assert m2.call_count == 1 + + with mock.patch("psutil._psplatform.Process.cpu_times") as m: + p.cpu_times() + p.cpu_times() + assert m.call_count == 2 + + def test_oneshot_cache(self): + # Make sure oneshot() cache is nonglobal. Instead it's + # supposed to be bound to the Process instance, see: + # https://github.com/giampaolo/psutil/issues/1373 + p1, p2 = self.spawn_children_pair() + p1_ppid = p1.ppid() + p2_ppid = p2.ppid() + assert p1_ppid != p2_ppid + with p1.oneshot(): + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid + with p2.oneshot(): + assert p1.ppid() == p1_ppid + assert p2.ppid() == p2_ppid + + def test_halfway_terminated_process(self): + # Test that NoSuchProcess exception gets raised in case the + # process dies after we create the Process object. + # Example: + # >>> proc = Process(1234) + # >>> time.sleep(2) # time-consuming task, process dies in meantime + # >>> proc.name() + # Refers to Issue #15 + def assert_raises_nsp(fun, fun_name): + try: + ret = fun() + except psutil.ZombieProcess: # differentiate from NSP + raise + except psutil.NoSuchProcess: + pass + except psutil.AccessDenied: + if OPENBSD and fun_name in {'threads', 'num_threads'}: + return + raise + else: + # NtQuerySystemInformation succeeds even if process is gone. + if WINDOWS and fun_name in {'exe', 'name'}: + return + raise self.fail( + f"{fun!r} didn't raise NSP and returned {ret!r} instead" + ) + + p = self.spawn_psproc() + p.terminate() + p.wait() + if WINDOWS: # XXX + call_until(lambda: p.pid not in psutil.pids()) + self.assertProcessGone(p) + + ns = process_namespace(p) + for fun, name in ns.iter(ns.all): + assert_raises_nsp(fun, name) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_zombie_process(self): + _parent, zombie = self.spawn_zombie() + self.assertProcessZombie(zombie) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_zombie_process_is_running_w_exc(self): + # Emulate a case where internally is_running() raises + # ZombieProcess. + p = psutil.Process() + with mock.patch( + "psutil.Process", side_effect=psutil.ZombieProcess(0) + ) as m: + assert p.is_running() + assert m.called + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_zombie_process_status_w_exc(self): + # Emulate a case where internally status() raises + # ZombieProcess. + p = psutil.Process() + with mock.patch( + "psutil._psplatform.Process.status", + side_effect=psutil.ZombieProcess(0), + ) as m: + assert p.status() == psutil.STATUS_ZOMBIE + assert m.called + + def test_reused_pid(self): + # Emulate a case where PID has been reused by another process. + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + p._ident = (p.pid, p.create_time() + 100) + + list(psutil.process_iter()) + assert p.pid in psutil._pmap + assert not p.is_running() + + # make sure is_running() removed PID from process_iter() + # internal cache + with mock.patch.object(psutil._common, "PSUTIL_DEBUG", True): + with contextlib.redirect_stderr(io.StringIO()) as f: + list(psutil.process_iter()) + assert ( + f"refreshing Process instance for reused PID {p.pid}" + in f.getvalue() + ) + assert p.pid not in psutil._pmap + + assert p != psutil.Process(subp.pid) + msg = "process no longer exists and its PID has been reused" + ns = process_namespace(p) + for fun, name in ns.iter(ns.setters + ns.killers, clear_cache=False): + with self.subTest(name=name): + with pytest.raises(psutil.NoSuchProcess, match=msg): + fun() + + assert "terminated + PID reused" in str(p) + assert "terminated + PID reused" in repr(p) + + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.ppid() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parent() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.parents() + with pytest.raises(psutil.NoSuchProcess, match=msg): + p.children() + + def test_pid_0(self): + # Process(0) is supposed to work on all platforms except Linux + if 0 not in psutil.pids(): + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(0) + # These 2 are a contradiction, but "ps" says PID 1's parent + # is PID 0. + assert not psutil.pid_exists(0) + assert psutil.Process(1).ppid() == 0 + return + + p = psutil.Process(0) + exc = psutil.AccessDenied if WINDOWS else ValueError + with pytest.raises(exc): + p.wait() + with pytest.raises(exc): + p.terminate() + with pytest.raises(exc): + p.suspend() + with pytest.raises(exc): + p.resume() + with pytest.raises(exc): + p.kill() + with pytest.raises(exc): + p.send_signal(signal.SIGTERM) + + # test all methods + ns = process_namespace(p) + for fun, name in ns.iter(ns.getters + ns.setters): + try: + ret = fun() + except psutil.AccessDenied: + pass + else: + if name in {"uids", "gids"}: + assert ret.real == 0 + elif name == "username": + user = 'NT AUTHORITY\\SYSTEM' if WINDOWS else 'root' + assert p.username() == user + elif name == "name": + assert name, name + + if not OPENBSD: + assert 0 in psutil.pids() + assert psutil.pid_exists(0) + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + def test_environ(self): + def clean_dict(d): + exclude = ["PLAT", "HOME", "PYTEST_CURRENT_TEST", "PYTEST_VERSION"] + if MACOS: + exclude.extend([ + "__CF_USER_TEXT_ENCODING", + "VERSIONER_PYTHON_PREFER_32_BIT", + "VERSIONER_PYTHON_VERSION", + "VERSIONER_PYTHON_VERSION", + ]) + for name in exclude: + d.pop(name, None) + return { + k.replace("\r", "").replace("\n", ""): v.replace( + "\r", "" + ).replace("\n", "") + for k, v in d.items() + } + + self.maxDiff = None + p = psutil.Process() + d1 = clean_dict(p.environ()) + d2 = clean_dict(os.environ.copy()) + if not OSX and GITHUB_ACTIONS: + assert d1 == d2 + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + MACOS_11PLUS, + reason="macOS 11+ can't get another process environment, issue #2084", + ) + @pytest.mark.skipif( + NETBSD, reason="sometimes fails on `assert is_running()`" + ) + def test_weird_environ(self): + # environment variables can contain values without an equals sign + code = textwrap.dedent(""" + #include + #include + + char * const argv[] = {"cat", 0}; + char * const envp[] = {"A=1", "X", "C=3", 0}; + + int main(void) { + // Close stderr on exec so parent can wait for the + // execve to finish. + if (fcntl(2, F_SETFD, FD_CLOEXEC) != 0) + return 0; + return execve("/bin/cat", argv, envp); + } + """) + cexe = create_c_exe(self.get_testfn(), c_code=code) + sproc = self.spawn_testproc( + [cexe], stdin=subprocess.PIPE, stderr=subprocess.PIPE + ) + p = psutil.Process(sproc.pid) + wait_for_pid(p.pid) + assert p.is_running() + # Wait for process to exec or exit. + assert sproc.stderr.read() == b"" + if MACOS and CI_TESTING: + try: + env = p.environ() + except psutil.AccessDenied: + # XXX: fails sometimes with: + # PermissionError from 'sysctl(KERN_PROCARGS2) -> EIO' + return + else: + env = p.environ() + assert env == {"A": "1", "C": "3"} + sproc.communicate() + assert sproc.returncode == 0 + + +# =================================================================== +# --- psutil.Popen tests +# =================================================================== + + +class TestPopen(PsutilTestCase): + """Tests for psutil.Popen class.""" + + @classmethod + def tearDownClass(cls): + reap_children() + + def test_misc(self): + # XXX this test causes a ResourceWarning because + # psutil.__subproc instance doesn't get properly freed. + # Not sure what to do though. + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.name() + proc.cpu_times() + proc.stdin # noqa: B018 + assert dir(proc) + with pytest.raises(AttributeError): + proc.foo # noqa: B018 + proc.terminate() + if POSIX: + assert proc.wait(5) == -signal.SIGTERM + else: + assert proc.wait(5) == signal.SIGTERM + + def test_ctx_manager(self): + with psutil.Popen( + [PYTHON_EXE, "-V"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.communicate() + assert proc.stdout.closed + assert proc.stderr.closed + assert proc.stdin.closed + assert proc.returncode == 0 + + def test_kill_terminate(self): + # subprocess.Popen()'s terminate(), kill() and send_signal() do + # not raise exception after the process is gone. psutil.Popen + # diverges from that. + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + with pytest.raises(psutil.NoSuchProcess): + proc.terminate() + with pytest.raises(psutil.NoSuchProcess): + proc.kill() + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.SIGTERM) + if WINDOWS: + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + proc.send_signal(signal.CTRL_BREAK_EVENT) + + def test__getattribute__(self): + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + with psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) as proc: + proc.terminate() + proc.wait() + with pytest.raises(AttributeError): + proc.foo # noqa: B018 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process_all.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process_all.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa3fa01dfd774f163ce0cbc9d358fef2b3b3bd1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_process_all.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Iterate over all process PIDs and for each one of them invoke and +test all psutil.Process() methods. +""" + +import enum +import errno +import multiprocessing +import os +import stat +import time +import traceback + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import OSX +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import CI_TESTING +from psutil.tests import PYTEST_PARALLEL +from psutil.tests import VALID_PROC_STATUSES +from psutil.tests import PsutilTestCase +from psutil.tests import check_connection_ntuple +from psutil.tests import create_sockets +from psutil.tests import is_namedtuple +from psutil.tests import is_win_secure_system_proc +from psutil.tests import process_namespace +from psutil.tests import pytest + + +# Cuts the time in half, but (e.g.) on macOS the process pool stays +# alive after join() (multiprocessing bug?), messing up other tests. +USE_PROC_POOL = LINUX and not CI_TESTING and not PYTEST_PARALLEL + + +def proc_info(pid): + tcase = PsutilTestCase() + + def check_exception(exc, proc, name, ppid): + tcase.assertEqual(exc.pid, pid) + if exc.name is not None: + tcase.assertEqual(exc.name, name) + if isinstance(exc, psutil.ZombieProcess): + tcase.assertProcessZombie(proc) + if exc.ppid is not None: + tcase.assertGreaterEqual(exc.ppid, 0) + tcase.assertEqual(exc.ppid, ppid) + elif isinstance(exc, psutil.NoSuchProcess): + tcase.assertProcessGone(proc) + str(exc) + repr(exc) + + def do_wait(): + if pid != 0: + try: + proc.wait(0) + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + + try: + proc = psutil.Process(pid) + except psutil.NoSuchProcess: + tcase.assertPidGone(pid) + return {} + try: + d = proc.as_dict(['ppid', 'name']) + except psutil.NoSuchProcess: + tcase.assertProcessGone(proc) + else: + name, ppid = d['name'], d['ppid'] + info = {'pid': proc.pid} + ns = process_namespace(proc) + # We don't use oneshot() because in order not to fool + # check_exception() in case of NSP. + for fun, fun_name in ns.iter(ns.getters, clear_cache=False): + try: + info[fun_name] = fun() + except psutil.Error as exc: + check_exception(exc, proc, name, ppid) + continue + do_wait() + return info + + +class TestFetchAllProcesses(PsutilTestCase): + """Test which iterates over all running processes and performs + some sanity checks against Process API's returned values. + Uses a process pool to get info about all processes. + """ + + def setUp(self): + psutil._set_debug(False) + # Using a pool in a CI env may result in deadlock, see: + # https://github.com/giampaolo/psutil/issues/2104 + if USE_PROC_POOL: + self.pool = multiprocessing.Pool() + + def tearDown(self): + psutil._set_debug(True) + if USE_PROC_POOL: + self.pool.terminate() + self.pool.join() + + def iter_proc_info(self): + # Fixes "can't pickle : it's not the + # same object as test_process_all.proc_info". + from psutil.tests.test_process_all import proc_info + + if USE_PROC_POOL: + return self.pool.imap_unordered(proc_info, psutil.pids()) + else: + ls = [proc_info(pid) for pid in psutil.pids()] + return ls + + def test_all(self): + failures = [] + for info in self.iter_proc_info(): + for name, value in info.items(): + meth = getattr(self, name) + try: + meth(value, info) + except Exception: # noqa: BLE001 + s = '\n' + '=' * 70 + '\n' + s += ( + "FAIL: name=test_{}, pid={}, ret={}\ninfo={}\n".format( + name, + info['pid'], + repr(value), + info, + ) + ) + s += '-' * 70 + s += f"\n{traceback.format_exc()}" + s = "\n".join((" " * 4) + i for i in s.splitlines()) + "\n" + failures.append(s) + else: + if value not in (0, 0.0, [], None, '', {}): + assert value, value + if failures: + raise self.fail(''.join(failures)) + + def cmdline(self, ret, info): + assert isinstance(ret, list) + for part in ret: + assert isinstance(part, str) + + def exe(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + if ret: + if WINDOWS and not ret.endswith('.exe'): + return # May be "Registry", "MemCompression", ... + assert os.path.isabs(ret), ret + # Note: os.stat() may return False even if the file is there + # hence we skip the test, see: + # http://stackoverflow.com/questions/3112546/os-path-exists-lies + if POSIX and os.path.isfile(ret): + if hasattr(os, 'access') and hasattr(os, "X_OK"): + # XXX: may fail on MACOS + try: + assert os.access(ret, os.X_OK) + except AssertionError: + if os.path.exists(ret) and not CI_TESTING: + raise + + def pid(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def ppid(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + proc_info(ret) + + def name(self, ret, info): + assert isinstance(ret, str) + if WINDOWS and not ret and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + # on AIX, "" processes don't have names + if not AIX: + assert ret, repr(ret) + + def create_time(self, ret, info): + assert isinstance(ret, float) + try: + assert ret >= 0 + except AssertionError: + # XXX + if OPENBSD and info['status'] == psutil.STATUS_ZOMBIE: + pass + else: + raise + # this can't be taken for granted on all platforms + # self.assertGreaterEqual(ret, psutil.boot_time()) + # make sure returned value can be pretty printed + # with strftime + time.strftime("%Y %m %d %H:%M:%S", time.localtime(ret)) + + def uids(self, ret, info): + assert is_namedtuple(ret) + for uid in ret: + assert isinstance(uid, int) + assert uid >= 0 + + def gids(self, ret, info): + assert is_namedtuple(ret) + # note: testing all gids as above seems not to be reliable for + # gid == 30 (nodoby); not sure why. + for gid in ret: + assert isinstance(gid, int) + if not MACOS and not NETBSD: + assert gid >= 0 + + def username(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + assert ret.strip() + + def status(self, ret, info): + assert isinstance(ret, str) + assert ret, ret + assert ret != '?' # XXX + assert ret in VALID_PROC_STATUSES + + def io_counters(self, ret, info): + assert is_namedtuple(ret) + for field in ret: + assert isinstance(field, int) + if field != -1: + assert field >= 0 + + def ionice(self, ret, info): + if LINUX: + assert isinstance(ret.ioclass, int) + assert isinstance(ret.value, int) + assert ret.ioclass >= 0 + assert ret.value >= 0 + else: # Windows, Cygwin + choices = [ + psutil.IOPRIO_VERYLOW, + psutil.IOPRIO_LOW, + psutil.IOPRIO_NORMAL, + psutil.IOPRIO_HIGH, + ] + assert isinstance(ret, int) + assert ret >= 0 + assert ret in choices + + def num_threads(self, ret, info): + assert isinstance(ret, int) + if WINDOWS and ret == 0 and is_win_secure_system_proc(info['pid']): + # https://github.com/giampaolo/psutil/issues/2338 + return + assert ret >= 1 + + def threads(self, ret, info): + assert isinstance(ret, list) + for t in ret: + assert is_namedtuple(t) + assert t.id >= 0 + assert t.user_time >= 0 + assert t.system_time >= 0 + for field in t: + assert isinstance(field, (int, float)) + + def cpu_times(self, ret, info): + assert is_namedtuple(ret) + for n in ret: + assert isinstance(n, float) + assert n >= 0 + # TODO: check ntuple fields + + def cpu_percent(self, ret, info): + assert isinstance(ret, float) + assert 0.0 <= ret <= 100.0, ret + + def cpu_num(self, ret, info): + assert isinstance(ret, int) + if FREEBSD and ret == -1: + return + assert ret >= 0 + if psutil.cpu_count() == 1: + assert ret == 0 + assert ret in list(range(psutil.cpu_count())) + + def memory_info(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + assert isinstance(value, int) + assert value >= 0 + if WINDOWS: + assert ret.peak_wset >= ret.wset + assert ret.peak_paged_pool >= ret.paged_pool + assert ret.peak_nonpaged_pool >= ret.nonpaged_pool + assert ret.peak_pagefile >= ret.pagefile + + def memory_full_info(self, ret, info): + assert is_namedtuple(ret) + total = psutil.virtual_memory().total + for name in ret._fields: + value = getattr(ret, name) + assert isinstance(value, int) + assert value >= 0 + if LINUX or (OSX and name in {'vms', 'data'}): + # On Linux there are processes (e.g. 'goa-daemon') whose + # VMS is incredibly high for some reason. + continue + assert value <= total, name + + if LINUX: + assert ret.pss >= ret.uss + + def open_files(self, ret, info): + assert isinstance(ret, list) + for f in ret: + assert isinstance(f.fd, int) + assert isinstance(f.path, str) + assert f.path.strip() == f.path + if WINDOWS: + assert f.fd == -1 + elif LINUX: + assert isinstance(f.position, int) + assert isinstance(f.mode, str) + assert isinstance(f.flags, int) + assert f.position >= 0 + assert f.mode in {'r', 'w', 'a', 'r+', 'a+'} + assert f.flags > 0 + elif BSD and not f.path: + # XXX see: https://github.com/giampaolo/psutil/issues/595 + continue + assert os.path.isabs(f.path), f + try: + st = os.stat(f.path) + except FileNotFoundError: + pass + else: + assert stat.S_ISREG(st.st_mode), f + + def num_fds(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def net_connections(self, ret, info): + with create_sockets(): + assert len(ret) == len(set(ret)) + for conn in ret: + assert is_namedtuple(conn) + check_connection_ntuple(conn) + + def cwd(self, ret, info): + assert isinstance(ret, str) + assert ret.strip() == ret + if ret: + assert os.path.isabs(ret), ret + try: + st = os.stat(ret) + except OSError as err: + if WINDOWS and psutil._psplatform.is_permission_err(err): + pass + # directory has been removed in mean time + elif err.errno != errno.ENOENT: + raise + else: + assert stat.S_ISDIR(st.st_mode) + + def memory_percent(self, ret, info): + assert isinstance(ret, float) + assert 0 <= ret <= 100, ret + + def is_running(self, ret, info): + assert isinstance(ret, bool) + + def cpu_affinity(self, ret, info): + assert isinstance(ret, list) + assert ret != [] + cpus = list(range(psutil.cpu_count())) + for n in ret: + assert isinstance(n, int) + assert n in cpus + + def terminal(self, ret, info): + assert isinstance(ret, (str, type(None))) + if ret is not None: + assert os.path.isabs(ret), ret + assert os.path.exists(ret), ret + + def memory_maps(self, ret, info): + for nt in ret: + assert isinstance(nt.addr, str) + assert isinstance(nt.perms, str) + assert isinstance(nt.path, str) + for fname in nt._fields: + value = getattr(nt, fname) + if fname == 'path': + if value.startswith(("[", "anon_inode:")): # linux + continue + if BSD and value == "pvclock": # seen on FreeBSD + continue + assert os.path.isabs(nt.path), nt.path + # commented as on Linux we might get + # '/foo/bar (deleted)' + # assert os.path.exists(nt.path), nt.path + elif fname == 'addr': + assert value, repr(value) + elif fname == 'perms': + if not WINDOWS: + assert value, repr(value) + else: + assert isinstance(value, int) + assert value >= 0 + + def num_handles(self, ret, info): + assert isinstance(ret, int) + assert ret >= 0 + + def nice(self, ret, info): + assert isinstance(ret, int) + if POSIX: + assert -20 <= ret <= 20, ret + else: + priorities = [ + getattr(psutil, x) + for x in dir(psutil) + if x.endswith('_PRIORITY_CLASS') + ] + assert ret in priorities + assert isinstance(ret, enum.IntEnum) + + def num_ctx_switches(self, ret, info): + assert is_namedtuple(ret) + for value in ret: + assert isinstance(value, int) + assert value >= 0 + + def rlimit(self, ret, info): + assert isinstance(ret, tuple) + assert len(ret) == 2 + assert ret[0] >= -1 + assert ret[1] >= -1 + + def environ(self, ret, info): + assert isinstance(ret, dict) + for k, v in ret.items(): + assert isinstance(k, str) + assert isinstance(v, str) + + +class TestPidsRange(PsutilTestCase): + """Given pid_exists() return value for a range of PIDs which may or + may not exist, make sure that psutil.Process() and psutil.pids() + agree with pid_exists(). This guarantees that the 3 APIs are all + consistent with each other. See: + https://github.com/giampaolo/psutil/issues/2359 + + XXX - Note about Windows: it turns out there are some "hidden" PIDs + which are not returned by psutil.pids() and are also not revealed + by taskmgr.exe and ProcessHacker, still they can be instantiated by + psutil.Process() and queried. One of such PIDs is "conhost.exe". + Running as_dict() for it reveals that some Process() APIs + erroneously raise NoSuchProcess, so we know we have problem there. + Let's ignore this for now, since it's quite a corner case (who even + imagined hidden PIDs existed on Windows?). + """ + + def setUp(self): + psutil._set_debug(False) + + def tearDown(self): + psutil._set_debug(True) + + def test_it(self): + def is_linux_tid(pid): + try: + f = open(f"/proc/{pid}/status", "rb") # noqa: SIM115 + except FileNotFoundError: + return False + else: + with f: + for line in f: + if line.startswith(b"Tgid:"): + tgid = int(line.split()[1]) + # If tgid and pid are different then we're + # dealing with a process TID. + return tgid != pid + raise ValueError("'Tgid' line not found") + + def check(pid): + # In case of failure retry up to 3 times in order to avoid + # race conditions, especially when running in a CI + # environment where PIDs may appear and disappear at any + # time. + x = 3 + while True: + exists = psutil.pid_exists(pid) + try: + if exists: + psutil.Process(pid) + if not WINDOWS: # see docstring + assert pid in psutil.pids() + else: + # On OpenBSD thread IDs can be instantiated, + # and oneshot() succeeds, but other APIs fail + # with EINVAL. + if not OPENBSD: + with pytest.raises(psutil.NoSuchProcess): + psutil.Process(pid) + if not WINDOWS: # see docstring + assert pid not in psutil.pids() + except (psutil.Error, AssertionError): + x -= 1 + if x == 0: + raise + else: + return + + for pid in range(1, 3000): + if LINUX and is_linux_tid(pid): + # On Linux a TID (thread ID) can be passed to the + # Process class and is querable like a PID (process + # ID). Skip it. + continue + with self.subTest(pid=pid): + check(pid) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_scripts.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_scripts.py new file mode 100644 index 0000000000000000000000000000000000000000..de0ad2af747cd1daec63f4cc5e1320e855cfcfad --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_scripts.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Test various scripts.""" + +import ast +import os +import shutil +import stat +import subprocess + +import pytest + +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import ROOT_DIR +from psutil.tests import SCRIPTS_DIR +from psutil.tests import PsutilTestCase +from psutil.tests import import_module_by_path +from psutil.tests import psutil +from psutil.tests import sh + + +INTERNAL_SCRIPTS_DIR = os.path.join(SCRIPTS_DIR, "internal") +SETUP_PY = os.path.join(ROOT_DIR, 'setup.py') + + +# =================================================================== +# --- Tests scripts in scripts/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SCRIPTS_DIR), + reason="can't find scripts/ directory", +) +class TestExampleScripts(PsutilTestCase): + @staticmethod + def assert_stdout(exe, *args, **kwargs): + kwargs.setdefault("env", PYTHON_EXE_ENV) + exe = os.path.join(SCRIPTS_DIR, exe) + cmd = [PYTHON_EXE, exe] + for arg in args: + cmd.append(arg) + try: + out = sh(cmd, **kwargs).strip() + except RuntimeError as err: + if 'AccessDenied' in str(err): + return str(err) + else: + raise + assert out, out + return out + + @staticmethod + def assert_syntax(exe): + exe = os.path.join(SCRIPTS_DIR, exe) + with open(exe, encoding="utf8") as f: + src = f.read() + ast.parse(src) + + def test_coverage(self): + # make sure all example scripts have a test method defined + meths = dir(self) + for name in os.listdir(SCRIPTS_DIR): + if name.endswith('.py'): + if 'test_' + os.path.splitext(name)[0] not in meths: + # self.assert_stdout(name) + raise self.fail( + "no test defined for" + f" {os.path.join(SCRIPTS_DIR, name)!r} script" + ) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_executable(self): + for root, dirs, files in os.walk(SCRIPTS_DIR): + for file in files: + if file.endswith('.py'): + path = os.path.join(root, file) + if not stat.S_IXUSR & os.stat(path)[stat.ST_MODE]: + raise self.fail(f"{path!r} is not executable") + + def test_disk_usage(self): + self.assert_stdout('disk_usage.py') + + def test_free(self): + self.assert_stdout('free.py') + + def test_meminfo(self): + self.assert_stdout('meminfo.py') + + def test_procinfo(self): + self.assert_stdout('procinfo.py', str(os.getpid())) + + @pytest.mark.skipif(CI_TESTING and not psutil.users(), reason="no users") + def test_who(self): + self.assert_stdout('who.py') + + def test_ps(self): + self.assert_stdout('ps.py') + + def test_pstree(self): + self.assert_stdout('pstree.py') + + def test_netstat(self): + self.assert_stdout('netstat.py') + + def test_ifconfig(self): + self.assert_stdout('ifconfig.py') + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + def test_pmap(self): + self.assert_stdout('pmap.py', str(os.getpid())) + + def test_procsmem(self): + if 'uss' not in psutil.Process().memory_full_info()._fields: + raise pytest.skip("not supported") + self.assert_stdout('procsmem.py') + + def test_killall(self): + self.assert_syntax('killall.py') + + def test_nettop(self): + self.assert_syntax('nettop.py') + + def test_top(self): + self.assert_syntax('top.py') + + def test_iotop(self): + self.assert_syntax('iotop.py') + + def test_pidof(self): + output = self.assert_stdout('pidof.py', psutil.Process().name()) + assert str(os.getpid()) in output + + @pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") + def test_winservices(self): + self.assert_stdout('winservices.py') + + def test_cpu_distribution(self): + self.assert_syntax('cpu_distribution.py') + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_temperatures(self): + if not psutil.sensors_temperatures(): + raise pytest.skip("no temperatures") + self.assert_stdout('temperatures.py') + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_fans(self): + if not psutil.sensors_fans(): + raise pytest.skip("no fans") + self.assert_stdout('fans.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_battery(self): + self.assert_stdout('battery.py') + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors(self): + self.assert_stdout('sensors.py') + + +# =================================================================== +# --- Tests scripts in scripts/internal/ directory +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(INTERNAL_SCRIPTS_DIR), + reason="can't find scripts/internal/ directory", +) +class TestInternalScripts(PsutilTestCase): + @staticmethod + def ls(): + for name in os.listdir(INTERNAL_SCRIPTS_DIR): + if name.endswith(".py"): + yield os.path.join(INTERNAL_SCRIPTS_DIR, name) + + def test_syntax_all(self): + for path in self.ls(): + with open(path, encoding="utf8") as f: + data = f.read() + ast.parse(data) + + @pytest.mark.skipif(CI_TESTING, reason="not on CI") + def test_import_all(self): + for path in self.ls(): + try: + import_module_by_path(path) + except SystemExit: + pass + + +# =================================================================== +# --- Tests for setup.py script +# =================================================================== + + +@pytest.mark.skipif( + CI_TESTING and not os.path.exists(SETUP_PY), reason="can't find setup.py" +) +class TestSetupScript(PsutilTestCase): + def test_invocation(self): + module = import_module_by_path(SETUP_PY) + with pytest.raises(SystemExit): + module.setup() + assert module.get_version() == psutil.__version__ + + @pytest.mark.skipif( + not shutil.which("python2.7"), reason="python2.7 not installed" + ) + def test_python2(self): + # There's a duplicate of this test in scripts/internal + # directory, which is only executed by CI. We replicate it here + # to run it when developing locally. + p = subprocess.Popen( + [shutil.which("python2.7"), SETUP_PY], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = p.communicate() + assert p.wait() == 1 + assert not stdout + assert "psutil no longer supports Python 2.7" in stderr + assert "Latest version supporting Python 2.7 is" in stderr diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_sunos.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_sunos.py new file mode 100644 index 0000000000000000000000000000000000000000..b5d9d353b94e60f537b967b7a9fbcfc13cf5bf65 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_sunos.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Sun OS specific tests.""" + +import os + +import psutil +from psutil import SUNOS +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import sh + + +@pytest.mark.skipif(not SUNOS, reason="SUNOS only") +class SunOSSpecificTestCase(PsutilTestCase): + def test_swap_memory(self): + out = sh(f"env PATH=/usr/sbin:/sbin:{os.environ['PATH']} swap -l") + lines = out.strip().split('\n')[1:] + if not lines: + raise ValueError('no swap device(s) configured') + total = free = 0 + for line in lines: + fields = line.split() + total = int(fields[3]) * 512 + free = int(fields[4]) * 512 + used = total - free + + psutil_swap = psutil.swap_memory() + assert psutil_swap.total == total + assert psutil_swap.used == used + assert psutil_swap.free == free + + def test_cpu_count(self): + out = sh("/usr/sbin/psrinfo") + assert psutil.cpu_count() == len(out.split('\n')) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_system.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_system.py new file mode 100644 index 0000000000000000000000000000000000000000..b961e1f89178932ce99ea3a6cc1acfa745bfa280 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_system.py @@ -0,0 +1,979 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for system APIS.""" + +import datetime +import enum +import errno +import os +import platform +import pprint +import shutil +import signal +import socket +import sys +import time +from unittest import mock + +import psutil +from psutil import AIX +from psutil import BSD +from psutil import FREEBSD +from psutil import LINUX +from psutil import MACOS +from psutil import NETBSD +from psutil import OPENBSD +from psutil import POSIX +from psutil import SUNOS +from psutil import WINDOWS +from psutil._common import broadcast_addr +from psutil.tests import AARCH64 +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import GLOBAL_TIMEOUT +from psutil.tests import HAS_BATTERY +from psutil.tests import HAS_CPU_FREQ +from psutil.tests import HAS_GETLOADAVG +from psutil.tests import HAS_NET_IO_COUNTERS +from psutil.tests import HAS_SENSORS_BATTERY +from psutil.tests import HAS_SENSORS_FANS +from psutil.tests import HAS_SENSORS_TEMPERATURES +from psutil.tests import IS_64BIT +from psutil.tests import MACOS_12PLUS +from psutil.tests import PYPY +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import check_net_address +from psutil.tests import pytest +from psutil.tests import retry_on_failure + + +# =================================================================== +# --- System-related API tests +# =================================================================== + + +class TestProcessIter(PsutilTestCase): + def test_pid_presence(self): + assert os.getpid() in [x.pid for x in psutil.process_iter()] + sproc = self.spawn_testproc() + assert sproc.pid in [x.pid for x in psutil.process_iter()] + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + assert sproc.pid not in [x.pid for x in psutil.process_iter()] + + def test_no_duplicates(self): + ls = list(psutil.process_iter()) + assert sorted(ls, key=lambda x: x.pid) == sorted( + set(ls), key=lambda x: x.pid + ) + + def test_emulate_nsp(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.NoSuchProcess(os.getpid()), + ): + assert not list(psutil.process_iter(attrs=["cpu_times"])) + psutil.process_iter.cache_clear() # repeat test without cache + + def test_emulate_access_denied(self): + list(psutil.process_iter()) # populate cache + for x in range(2): + with mock.patch( + 'psutil.Process.as_dict', + side_effect=psutil.AccessDenied(os.getpid()), + ): + with pytest.raises(psutil.AccessDenied): + list(psutil.process_iter(attrs=["cpu_times"])) + psutil.process_iter.cache_clear() # repeat test without cache + + def test_attrs(self): + for p in psutil.process_iter(attrs=['pid']): + assert list(p.info.keys()) == ['pid'] + # yield again + for p in psutil.process_iter(attrs=['pid']): + assert list(p.info.keys()) == ['pid'] + with pytest.raises(ValueError): + list(psutil.process_iter(attrs=['foo'])) + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ) as m: + for p in psutil.process_iter(attrs=["pid", "cpu_times"]): + assert p.info['cpu_times'] is None + assert p.info['pid'] >= 0 + assert m.called + with mock.patch( + "psutil._psplatform.Process.cpu_times", + side_effect=psutil.AccessDenied(0, ""), + ) as m: + flag = object() + for p in psutil.process_iter( + attrs=["pid", "cpu_times"], ad_value=flag + ): + assert p.info['cpu_times'] is flag + assert p.info['pid'] >= 0 + assert m.called + + def test_cache_clear(self): + list(psutil.process_iter()) # populate cache + assert psutil._pmap + psutil.process_iter.cache_clear() + assert not psutil._pmap + + +class TestProcessAPIs(PsutilTestCase): + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", + ) + def test_wait_procs(self): + def callback(p): + pids.append(p.pid) + + pids = [] + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + with pytest.raises(ValueError): + psutil.wait_procs(procs, timeout=-1) + with pytest.raises(TypeError): + psutil.wait_procs(procs, callback=1) + t = time.time() + gone, alive = psutil.wait_procs(procs, timeout=0.01, callback=callback) + + assert time.time() - t < 0.5 + assert not gone + assert len(alive) == 3 + assert not pids + for p in alive: + assert not hasattr(p, 'returncode') + + @retry_on_failure(30) + def test_1(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + assert len(gone) == 1 + assert len(alive) == 2 + return gone, alive + + sproc3.terminate() + gone, alive = test_1(procs, callback) + assert sproc3.pid in [x.pid for x in gone] + if POSIX: + assert gone.pop().returncode == -signal.SIGTERM + else: + assert gone.pop().returncode == 1 + assert pids == [sproc3.pid] + for p in alive: + assert not hasattr(p, 'returncode') + + @retry_on_failure(30) + def test_2(procs, callback): + gone, alive = psutil.wait_procs( + procs, timeout=0.03, callback=callback + ) + assert len(gone) == 3 + assert len(alive) == 0 + return gone, alive + + sproc1.terminate() + sproc2.terminate() + gone, alive = test_2(procs, callback) + assert set(pids) == {sproc1.pid, sproc2.pid, sproc3.pid} + for p in gone: + assert hasattr(p, 'returncode') + + @pytest.mark.skipif( + PYPY and WINDOWS, + reason="spawn_testproc() unreliable on PYPY + WINDOWS", + ) + def test_wait_procs_no_timeout(self): + sproc1 = self.spawn_testproc() + sproc2 = self.spawn_testproc() + sproc3 = self.spawn_testproc() + procs = [psutil.Process(x.pid) for x in (sproc1, sproc2, sproc3)] + for p in procs: + p.terminate() + psutil.wait_procs(procs) + + def test_pid_exists(self): + sproc = self.spawn_testproc() + assert psutil.pid_exists(sproc.pid) + p = psutil.Process(sproc.pid) + p.kill() + p.wait() + assert not psutil.pid_exists(sproc.pid) + assert not psutil.pid_exists(-1) + assert psutil.pid_exists(0) == (0 in psutil.pids()) + + def test_pid_exists_2(self): + pids = psutil.pids() + for pid in pids: + try: + assert psutil.pid_exists(pid) + except AssertionError: + # in case the process disappeared in meantime fail only + # if it is no longer in psutil.pids() + time.sleep(0.1) + assert pid not in psutil.pids() + pids = range(max(pids) + 15000, max(pids) + 16000) + for pid in pids: + assert not psutil.pid_exists(pid) + + +class TestMiscAPIs(PsutilTestCase): + def test_boot_time(self): + bt = psutil.boot_time() + assert isinstance(bt, float) + assert bt > 0 + assert bt < time.time() + + @pytest.mark.skipif( + CI_TESTING and not psutil.users(), reason="unreliable on CI" + ) + def test_users(self): + users = psutil.users() + assert users + for user in users: + with self.subTest(user=user): + assert user.name + assert isinstance(user.name, str) + assert isinstance(user.terminal, (str, type(None))) + if user.host is not None: + assert isinstance(user.host, (str, type(None))) + user.terminal # noqa: B018 + user.host # noqa: B018 + assert user.started > 0.0 + datetime.datetime.fromtimestamp(user.started) + if WINDOWS or OPENBSD: + assert user.pid is None + else: + psutil.Process(user.pid) + + def test_os_constants(self): + names = [ + "POSIX", + "WINDOWS", + "LINUX", + "MACOS", + "FREEBSD", + "OPENBSD", + "NETBSD", + "BSD", + "SUNOS", + ] + for name in names: + assert isinstance(getattr(psutil, name), bool), name + + if os.name == 'posix': + assert psutil.POSIX + assert not psutil.WINDOWS + names.remove("POSIX") + if "linux" in sys.platform.lower(): + assert psutil.LINUX + names.remove("LINUX") + elif "bsd" in sys.platform.lower(): + assert psutil.BSD + assert [psutil.FREEBSD, psutil.OPENBSD, psutil.NETBSD].count( + True + ) == 1 + names.remove("BSD") + names.remove("FREEBSD") + names.remove("OPENBSD") + names.remove("NETBSD") + elif ( + "sunos" in sys.platform.lower() + or "solaris" in sys.platform.lower() + ): + assert psutil.SUNOS + names.remove("SUNOS") + elif "darwin" in sys.platform.lower(): + assert psutil.MACOS + names.remove("MACOS") + else: + assert psutil.WINDOWS + assert not psutil.POSIX + names.remove("WINDOWS") + + # assert all other constants are set to False + for name in names: + assert not getattr(psutil, name), name + + +class TestMemoryAPIs(PsutilTestCase): + def test_virtual_memory(self): + mem = psutil.virtual_memory() + assert mem.total > 0, mem + assert mem.available > 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.used > 0, mem + assert mem.free >= 0, mem + for name in mem._fields: + value = getattr(mem, name) + if name != 'percent': + assert isinstance(value, int) + if name != 'total': + if not value >= 0: + raise self.fail(f"{name!r} < 0 ({value})") + if value > mem.total: + raise self.fail( + f"{name!r} > total (total={mem.total}, {name}={value})" + ) + + def test_swap_memory(self): + mem = psutil.swap_memory() + assert mem._fields == ( + 'total', + 'used', + 'free', + 'percent', + 'sin', + 'sout', + ) + + assert mem.total >= 0, mem + assert mem.used >= 0, mem + if mem.total > 0: + # likely a system with no swap partition + assert mem.free > 0, mem + else: + assert mem.free == 0, mem + assert 0 <= mem.percent <= 100, mem + assert mem.sin >= 0, mem + assert mem.sout >= 0, mem + + +class TestCpuAPIs(PsutilTestCase): + def test_cpu_count_logical(self): + logical = psutil.cpu_count() + assert logical is not None + assert logical == len(psutil.cpu_times(percpu=True)) + assert logical >= 1 + + if os.path.exists("/proc/cpuinfo"): + with open("/proc/cpuinfo") as fd: + cpuinfo_data = fd.read() + if "physical id" not in cpuinfo_data: + raise pytest.skip("cpuinfo doesn't include physical id") + + def test_cpu_count_cores(self): + logical = psutil.cpu_count() + cores = psutil.cpu_count(logical=False) + if cores is None: + raise pytest.skip("cpu_count_cores() is None") + if WINDOWS and sys.getwindowsversion()[:2] <= (6, 1): # <= Vista + assert cores is None + else: + assert cores >= 1 + assert logical >= cores + + def test_cpu_count_none(self): + # https://github.com/giampaolo/psutil/issues/1085 + for val in (-1, 0, None): + with mock.patch( + 'psutil._psplatform.cpu_count_logical', return_value=val + ) as m: + assert psutil.cpu_count() is None + assert m.called + with mock.patch( + 'psutil._psplatform.cpu_count_cores', return_value=val + ) as m: + assert psutil.cpu_count(logical=False) is None + assert m.called + + def test_cpu_times(self): + # Check type, value >= 0, str(). + total = 0 + times = psutil.cpu_times() + sum(times) + for cp_time in times: + assert isinstance(cp_time, float) + assert cp_time >= 0.0 + total += cp_time + assert round(abs(total - sum(times)), 6) == 0 + str(times) + # CPU times are always supposed to increase over time + # or at least remain the same and that's because time + # cannot go backwards. + # Surprisingly sometimes this might not be the case (at + # least on Windows and Linux), see: + # https://github.com/giampaolo/psutil/issues/392 + # https://github.com/giampaolo/psutil/issues/645 + # if not WINDOWS: + # last = psutil.cpu_times() + # for x in range(100): + # new = psutil.cpu_times() + # for field in new._fields: + # new_t = getattr(new, field) + # last_t = getattr(last, field) + # self.assertGreaterEqual( + # new_t, last_t, + # msg="{} {}".format(new_t, last_t)) + # last = new + + def test_cpu_times_time_increases(self): + # Make sure time increases between calls. + t1 = sum(psutil.cpu_times()) + stop_at = time.time() + GLOBAL_TIMEOUT + while time.time() < stop_at: + t2 = sum(psutil.cpu_times()) + if t2 > t1: + return + raise self.fail("time remained the same") + + def test_per_cpu_times(self): + # Check type, value >= 0, str(). + for times in psutil.cpu_times(percpu=True): + total = 0 + sum(times) + for cp_time in times: + assert isinstance(cp_time, float) + assert cp_time >= 0.0 + total += cp_time + assert round(abs(total - sum(times)), 6) == 0 + str(times) + assert len(psutil.cpu_times(percpu=True)[0]) == len( + psutil.cpu_times(percpu=False) + ) + + # Note: in theory CPU times are always supposed to increase over + # time or remain the same but never go backwards. In practice + # sometimes this is not the case. + # This issue seemd to be afflict Windows: + # https://github.com/giampaolo/psutil/issues/392 + # ...but it turns out also Linux (rarely) behaves the same. + # last = psutil.cpu_times(percpu=True) + # for x in range(100): + # new = psutil.cpu_times(percpu=True) + # for index in range(len(new)): + # newcpu = new[index] + # lastcpu = last[index] + # for field in newcpu._fields: + # new_t = getattr(newcpu, field) + # last_t = getattr(lastcpu, field) + # self.assertGreaterEqual( + # new_t, last_t, msg="{} {}".format(lastcpu, newcpu)) + # last = new + + def test_per_cpu_times_2(self): + # Simulate some work load then make sure time have increased + # between calls. + tot1 = psutil.cpu_times(percpu=True) + giveup_at = time.time() + GLOBAL_TIMEOUT + while True: + if time.time() >= giveup_at: + return self.fail("timeout") + tot2 = psutil.cpu_times(percpu=True) + for t1, t2 in zip(tot1, tot2): + t1, t2 = psutil._cpu_busy_time(t1), psutil._cpu_busy_time(t2) + difference = t2 - t1 + if difference >= 0.05: + return None + + @pytest.mark.skipif( + CI_TESTING and OPENBSD, reason="unreliable on OPENBSD + CI" + ) + @retry_on_failure(30) + def test_cpu_times_comparison(self): + # Make sure the sum of all per cpu times is almost equal to + # base "one cpu" times. On OpenBSD the sum of per-CPUs is + # higher for some reason. + base = psutil.cpu_times() + per_cpu = psutil.cpu_times(percpu=True) + summed_values = base._make([sum(num) for num in zip(*per_cpu)]) + for field in base._fields: + with self.subTest(field=field, base=base, per_cpu=per_cpu): + assert ( + abs(getattr(base, field) - getattr(summed_values, field)) + < 2 + ) + + def _test_cpu_percent(self, percent, last_ret, new_ret): + try: + assert isinstance(percent, float) + assert percent >= 0.0 + assert percent <= 100.0 * psutil.cpu_count() + except AssertionError as err: + raise AssertionError( + "\n{}\nlast={}\nnew={}".format( + err, pprint.pformat(last_ret), pprint.pformat(new_ret) + ) + ) + + def test_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_percent(interval=None) + self._test_cpu_percent(new, last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_percent(interval=-1) + + def test_per_cpu_percent(self): + last = psutil.cpu_percent(interval=0.001, percpu=True) + assert len(last) == psutil.cpu_count() + for _ in range(100): + new = psutil.cpu_percent(interval=None, percpu=True) + for percent in new: + self._test_cpu_percent(percent, last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_percent(interval=-1, percpu=True) + + def test_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001) + for _ in range(100): + new = psutil.cpu_times_percent(interval=None) + for percent in new: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(new), last, new) + last = new + with pytest.raises(ValueError): + psutil.cpu_times_percent(interval=-1) + + def test_per_cpu_times_percent(self): + last = psutil.cpu_times_percent(interval=0.001, percpu=True) + assert len(last) == psutil.cpu_count() + for _ in range(100): + new = psutil.cpu_times_percent(interval=None, percpu=True) + for cpu in new: + for percent in cpu: + self._test_cpu_percent(percent, last, new) + self._test_cpu_percent(sum(cpu), last, new) + last = new + + def test_per_cpu_times_percent_negative(self): + # see: https://github.com/giampaolo/psutil/issues/645 + psutil.cpu_times_percent(percpu=True) + zero_times = [ + x._make([0 for x in range(len(x._fields))]) + for x in psutil.cpu_times(percpu=True) + ] + with mock.patch('psutil.cpu_times', return_value=zero_times): + for cpu in psutil.cpu_times_percent(percpu=True): + for percent in cpu: + self._test_cpu_percent(percent, None, None) + + def test_cpu_stats(self): + # Tested more extensively in per-platform test modules. + infos = psutil.cpu_stats() + assert infos._fields == ( + 'ctx_switches', + 'interrupts', + 'soft_interrupts', + 'syscalls', + ) + for name in infos._fields: + value = getattr(infos, name) + assert value >= 0 + # on AIX, ctx_switches is always 0 + if not AIX and name in {'ctx_switches', 'interrupts'}: + assert value > 0 + + # TODO: remove this once 1892 is fixed + @pytest.mark.skipif( + MACOS and platform.machine() == 'arm64', reason="skipped due to #1892" + ) + @pytest.mark.skipif(not HAS_CPU_FREQ, reason="not supported") + def test_cpu_freq(self): + def check_ls(ls): + for nt in ls: + assert nt._fields == ('current', 'min', 'max') + if nt.max != 0.0: + assert nt.current <= nt.max + for name in nt._fields: + value = getattr(nt, name) + assert isinstance(value, (int, float)) + assert value >= 0 + + ls = psutil.cpu_freq(percpu=True) + if (FREEBSD or AARCH64) and not ls: + raise pytest.skip( + "returns empty list on FreeBSD and Linux aarch64" + ) + + assert ls, ls + check_ls([psutil.cpu_freq(percpu=False)]) + + if LINUX: + assert len(ls) == psutil.cpu_count() + + @pytest.mark.skipif(not HAS_GETLOADAVG, reason="not supported") + def test_getloadavg(self): + loadavg = psutil.getloadavg() + assert len(loadavg) == 3 + for load in loadavg: + assert isinstance(load, float) + assert load >= 0.0 + + +class TestDiskAPIs(PsutilTestCase): + @pytest.mark.skipif( + PYPY and not IS_64BIT, reason="unreliable on PYPY32 + 32BIT" + ) + def test_disk_usage(self): + usage = psutil.disk_usage(os.getcwd()) + assert usage._fields == ('total', 'used', 'free', 'percent') + + assert usage.total > 0, usage + assert usage.used > 0, usage + assert usage.free > 0, usage + assert usage.total > usage.used, usage + assert usage.total > usage.free, usage + assert 0 <= usage.percent <= 100, usage.percent + if hasattr(shutil, 'disk_usage'): + # py >= 3.3, see: http://bugs.python.org/issue12442 + shutil_usage = shutil.disk_usage(os.getcwd()) + tolerance = 5 * 1024 * 1024 # 5MB + assert usage.total == shutil_usage.total + assert abs(usage.free - shutil_usage.free) < tolerance + if not MACOS_12PLUS: + # see https://github.com/giampaolo/psutil/issues/2147 + assert abs(usage.used - shutil_usage.used) < tolerance + + # if path does not exist OSError ENOENT is expected across + # all platforms + fname = self.get_testfn() + with pytest.raises(FileNotFoundError): + psutil.disk_usage(fname) + + @pytest.mark.skipif(not ASCII_FS, reason="not an ASCII fs") + def test_disk_usage_unicode(self): + # See: https://github.com/giampaolo/psutil/issues/416 + with pytest.raises(UnicodeEncodeError): + psutil.disk_usage(UNICODE_SUFFIX) + + def test_disk_usage_bytes(self): + psutil.disk_usage(b'.') + + def test_disk_partitions(self): + def check_ntuple(nt): + assert isinstance(nt.device, str) + assert isinstance(nt.mountpoint, str) + assert isinstance(nt.fstype, str) + assert isinstance(nt.opts, str) + + # all = False + ls = psutil.disk_partitions(all=False) + assert ls + for disk in ls: + check_ntuple(disk) + if WINDOWS and 'cdrom' in disk.opts: + continue + if not POSIX: + assert os.path.exists(disk.device), disk + else: + # we cannot make any assumption about this, see: + # http://goo.gl/p9c43 + disk.device # noqa: B018 + # on modern systems mount points can also be files + assert os.path.exists(disk.mountpoint), disk + assert disk.fstype, disk + + # all = True + ls = psutil.disk_partitions(all=True) + assert ls + for disk in psutil.disk_partitions(all=True): + check_ntuple(disk) + if not WINDOWS and disk.mountpoint: + try: + os.stat(disk.mountpoint) + except OSError as err: + if GITHUB_ACTIONS and MACOS and err.errno == errno.EIO: + continue + # http://mail.python.org/pipermail/python-dev/ + # 2012-June/120787.html + if err.errno not in {errno.EPERM, errno.EACCES}: + raise + else: + assert os.path.exists(disk.mountpoint), disk + + # --- + + def find_mount_point(path): + path = os.path.abspath(path) + while not os.path.ismount(path): + path = os.path.dirname(path) + return path.lower() + + mount = find_mount_point(__file__) + mounts = [ + x.mountpoint.lower() + for x in psutil.disk_partitions(all=True) + if x.mountpoint + ] + assert mount in mounts + + @pytest.mark.skipif( + LINUX and not os.path.exists('/proc/diskstats'), + reason="/proc/diskstats not available on this linux version", + ) + @pytest.mark.skipif( + CI_TESTING and not psutil.disk_io_counters(), reason="unreliable on CI" + ) # no visible disks + def test_disk_io_counters(self): + def check_ntuple(nt): + assert nt[0] == nt.read_count + assert nt[1] == nt.write_count + assert nt[2] == nt.read_bytes + assert nt[3] == nt.write_bytes + if not (OPENBSD or NETBSD): + assert nt[4] == nt.read_time + assert nt[5] == nt.write_time + if LINUX: + assert nt[6] == nt.read_merged_count + assert nt[7] == nt.write_merged_count + assert nt[8] == nt.busy_time + elif FREEBSD: + assert nt[6] == nt.busy_time + for name in nt._fields: + assert getattr(nt, name) >= 0, nt + + ret = psutil.disk_io_counters(perdisk=False) + assert ret is not None, "no disks on this system?" + check_ntuple(ret) + ret = psutil.disk_io_counters(perdisk=True) + # make sure there are no duplicates + assert len(ret) == len(set(ret)) + for key in ret: + assert key, key + check_ntuple(ret[key]) + + def test_disk_io_counters_no_disks(self): + # Emulate a case where no disks are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.disk_io_counters', return_value={} + ) as m: + assert psutil.disk_io_counters(perdisk=False) is None + assert psutil.disk_io_counters(perdisk=True) == {} + assert m.called + + +class TestNetAPIs(PsutilTestCase): + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters(self): + def check_ntuple(nt): + assert nt[0] == nt.bytes_sent + assert nt[1] == nt.bytes_recv + assert nt[2] == nt.packets_sent + assert nt[3] == nt.packets_recv + assert nt[4] == nt.errin + assert nt[5] == nt.errout + assert nt[6] == nt.dropin + assert nt[7] == nt.dropout + assert nt.bytes_sent >= 0, nt + assert nt.bytes_recv >= 0, nt + assert nt.packets_sent >= 0, nt + assert nt.packets_recv >= 0, nt + assert nt.errin >= 0, nt + assert nt.errout >= 0, nt + assert nt.dropin >= 0, nt + assert nt.dropout >= 0, nt + + ret = psutil.net_io_counters(pernic=False) + check_ntuple(ret) + ret = psutil.net_io_counters(pernic=True) + assert ret != [] + for key in ret: + assert key + assert isinstance(key, str) + check_ntuple(ret[key]) + + @pytest.mark.skipif(not HAS_NET_IO_COUNTERS, reason="not supported") + def test_net_io_counters_no_nics(self): + # Emulate a case where no NICs are installed, see: + # https://github.com/giampaolo/psutil/issues/1062 + with mock.patch( + 'psutil._psplatform.net_io_counters', return_value={} + ) as m: + assert psutil.net_io_counters(pernic=False) is None + assert psutil.net_io_counters(pernic=True) == {} + assert m.called + + def test_net_if_addrs(self): + nics = psutil.net_if_addrs() + assert nics, nics + + nic_stats = psutil.net_if_stats() + + # Not reliable on all platforms (net_if_addrs() reports more + # interfaces). + # self.assertEqual(sorted(nics.keys()), + # sorted(psutil.net_io_counters(pernic=True).keys())) + + families = {socket.AF_INET, socket.AF_INET6, psutil.AF_LINK} + for nic, addrs in nics.items(): + assert isinstance(nic, str) + assert len(set(addrs)) == len(addrs) + for addr in addrs: + assert isinstance(addr.family, int) + assert isinstance(addr.address, str) + assert isinstance(addr.netmask, (str, type(None))) + assert isinstance(addr.broadcast, (str, type(None))) + assert addr.family in families + assert isinstance(addr.family, enum.IntEnum) + if nic_stats[nic].isup: + # Do not test binding to addresses of interfaces + # that are down + if addr.family == socket.AF_INET: + with socket.socket(addr.family) as s: + s.bind((addr.address, 0)) + elif addr.family == socket.AF_INET6: + info = socket.getaddrinfo( + addr.address, + 0, + socket.AF_INET6, + socket.SOCK_STREAM, + 0, + socket.AI_PASSIVE, + )[0] + af, socktype, proto, _canonname, sa = info + with socket.socket(af, socktype, proto) as s: + s.bind(sa) + for ip in ( + addr.address, + addr.netmask, + addr.broadcast, + addr.ptp, + ): + if ip is not None: + # TODO: skip AF_INET6 for now because I get: + # AddressValueError: Only hex digits permitted in + # u'c6f3%lxcbr0' in u'fe80::c8e0:fff:fe54:c6f3%lxcbr0' + if addr.family != socket.AF_INET6: + check_net_address(ip, addr.family) + # broadcast and ptp addresses are mutually exclusive + if addr.broadcast: + assert addr.ptp is None + elif addr.ptp: + assert addr.broadcast is None + + # check broadcast address + if ( + addr.broadcast + and addr.netmask + and addr.family in {socket.AF_INET, socket.AF_INET6} + ): + assert addr.broadcast == broadcast_addr(addr) + + if BSD or MACOS or SUNOS: + if hasattr(socket, "AF_LINK"): + assert psutil.AF_LINK == socket.AF_LINK + elif LINUX: + assert psutil.AF_LINK == socket.AF_PACKET + elif WINDOWS: + assert psutil.AF_LINK == -1 + + def test_net_if_addrs_mac_null_bytes(self): + # Simulate that the underlying C function returns an incomplete + # MAC address. psutil is supposed to fill it with null bytes. + # https://github.com/giampaolo/psutil/issues/786 + if POSIX: + ret = [('em1', psutil.AF_LINK, '06:3d:29', None, None, None)] + else: + ret = [('em1', -1, '06-3d-29', None, None, None)] + with mock.patch( + 'psutil._psplatform.net_if_addrs', return_value=ret + ) as m: + addr = psutil.net_if_addrs()['em1'][0] + assert m.called + if POSIX: + assert addr.address == '06:3d:29:00:00:00' + else: + assert addr.address == '06-3d-29-00-00-00' + + def test_net_if_stats(self): + nics = psutil.net_if_stats() + assert nics, nics + all_duplexes = ( + psutil.NIC_DUPLEX_FULL, + psutil.NIC_DUPLEX_HALF, + psutil.NIC_DUPLEX_UNKNOWN, + ) + for name, stats in nics.items(): + assert isinstance(name, str) + isup, duplex, speed, mtu, flags = stats + assert isinstance(isup, bool) + assert duplex in all_duplexes + assert duplex in all_duplexes + assert speed >= 0 + assert mtu >= 0 + assert isinstance(flags, str) + + @pytest.mark.skipif( + not (LINUX or BSD or MACOS), reason="LINUX or BSD or MACOS specific" + ) + def test_net_if_stats_enodev(self): + # See: https://github.com/giampaolo/psutil/issues/1279 + with mock.patch( + 'psutil._psutil_posix.net_if_mtu', + side_effect=OSError(errno.ENODEV, ""), + ) as m: + ret = psutil.net_if_stats() + assert ret == {} + assert m.called + + +class TestSensorsAPIs(PsutilTestCase): + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures(self): + temps = psutil.sensors_temperatures() + for name, entries in temps.items(): + assert isinstance(name, str) + for entry in entries: + assert isinstance(entry.label, str) + if entry.current is not None: + assert entry.current >= 0 + if entry.high is not None: + assert entry.high >= 0 + if entry.critical is not None: + assert entry.critical >= 0 + + @pytest.mark.skipif(not HAS_SENSORS_TEMPERATURES, reason="not supported") + def test_sensors_temperatures_fahreneit(self): + d = {'coretemp': [('label', 50.0, 60.0, 70.0)]} + with mock.patch( + "psutil._psplatform.sensors_temperatures", return_value=d + ) as m: + temps = psutil.sensors_temperatures(fahrenheit=True)['coretemp'][0] + assert m.called + assert temps.current == 122.0 + assert temps.high == 140.0 + assert temps.critical == 158.0 + + @pytest.mark.skipif(not HAS_SENSORS_BATTERY, reason="not supported") + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_sensors_battery(self): + ret = psutil.sensors_battery() + assert ret.percent >= 0 + assert ret.percent <= 100 + if ret.secsleft not in { + psutil.POWER_TIME_UNKNOWN, + psutil.POWER_TIME_UNLIMITED, + }: + assert ret.secsleft >= 0 + elif ret.secsleft == psutil.POWER_TIME_UNLIMITED: + assert ret.power_plugged + assert isinstance(ret.power_plugged, bool) + + @pytest.mark.skipif(not HAS_SENSORS_FANS, reason="not supported") + def test_sensors_fans(self): + fans = psutil.sensors_fans() + for name, entries in fans.items(): + assert isinstance(name, str) + for entry in entries: + assert isinstance(entry.label, str) + assert isinstance(entry.current, int) + assert entry.current >= 0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_testutils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_testutils.py new file mode 100644 index 0000000000000000000000000000000000000000..6db66e50eb54c8b77222785fb537923ba05f46d3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_testutils.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for testing utils (psutil.tests namespace).""" + +import collections +import errno +import os +import socket +import stat +import subprocess +import textwrap +import unittest +import warnings +from unittest import mock + +import psutil +import psutil.tests +from psutil import FREEBSD +from psutil import NETBSD +from psutil import POSIX +from psutil._common import open_binary +from psutil._common import open_text +from psutil._common import supports_ipv6 +from psutil.tests import CI_TESTING +from psutil.tests import COVERAGE +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import HERE +from psutil.tests import PYTHON_EXE +from psutil.tests import PYTHON_EXE_ENV +from psutil.tests import PsutilTestCase +from psutil.tests import TestMemoryLeak +from psutil.tests import bind_socket +from psutil.tests import bind_unix_socket +from psutil.tests import call_until +from psutil.tests import chdir +from psutil.tests import create_sockets +from psutil.tests import fake_pytest +from psutil.tests import filter_proc_net_connections +from psutil.tests import get_free_port +from psutil.tests import is_namedtuple +from psutil.tests import process_namespace +from psutil.tests import pytest +from psutil.tests import reap_children +from psutil.tests import retry +from psutil.tests import retry_on_failure +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import system_namespace +from psutil.tests import tcp_socketpair +from psutil.tests import terminate +from psutil.tests import unix_socketpair +from psutil.tests import wait_for_file +from psutil.tests import wait_for_pid + + +# =================================================================== +# --- Unit tests for test utilities. +# =================================================================== + + +class TestRetryDecorator(PsutilTestCase): + @mock.patch('time.sleep') + def test_retry_success(self, sleep): + # Fail 3 times out of 5; make sure the decorated fun returns. + + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa: B018 + return 1 + + queue = list(range(3)) + assert foo() == 1 + assert sleep.call_count == 3 + + @mock.patch('time.sleep') + def test_retry_failure(self, sleep): + # Fail 6 times out of 5; th function is supposed to raise exc. + @retry(retries=5, interval=1, logfun=None) + def foo(): + while queue: + queue.pop() + 1 / 0 # noqa: B018 + return 1 + + queue = list(range(6)) + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 + + @mock.patch('time.sleep') + def test_exception_arg(self, sleep): + @retry(exception=ValueError, interval=1) + def foo(): + raise TypeError + + with pytest.raises(TypeError): + foo() + assert sleep.call_count == 0 + + @mock.patch('time.sleep') + def test_no_interval_arg(self, sleep): + # if interval is not specified sleep is not supposed to be called + + @retry(retries=5, interval=None, logfun=None) + def foo(): + 1 / 0 # noqa: B018 + + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 0 + + @mock.patch('time.sleep') + def test_retries_arg(self, sleep): + @retry(retries=5, interval=1, logfun=None) + def foo(): + 1 / 0 # noqa: B018 + + with pytest.raises(ZeroDivisionError): + foo() + assert sleep.call_count == 5 + + @mock.patch('time.sleep') + def test_retries_and_timeout_args(self, sleep): + with pytest.raises(ValueError): + retry(retries=5, timeout=1) + + +class TestSyncTestUtils(PsutilTestCase): + def test_wait_for_pid(self): + wait_for_pid(os.getpid()) + nopid = max(psutil.pids()) + 99999 + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + with pytest.raises(psutil.NoSuchProcess): + wait_for_pid(nopid) + + def test_wait_for_file(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn) + assert not os.path.exists(testfn) + + def test_wait_for_file_empty(self): + testfn = self.get_testfn() + with open(testfn, 'w'): + pass + wait_for_file(testfn, empty=True) + assert not os.path.exists(testfn) + + def test_wait_for_file_no_file(self): + testfn = self.get_testfn() + with mock.patch('psutil.tests.retry.__iter__', return_value=iter([0])): + with pytest.raises(OSError): + wait_for_file(testfn) + + def test_wait_for_file_no_delete(self): + testfn = self.get_testfn() + with open(testfn, 'w') as f: + f.write('foo') + wait_for_file(testfn, delete=False) + assert os.path.exists(testfn) + + def test_call_until(self): + call_until(lambda: 1) + # TODO: test for timeout + + +class TestFSTestUtils(PsutilTestCase): + def test_open_text(self): + with open_text(__file__) as f: + assert f.mode == 'r' + + def test_open_binary(self): + with open_binary(__file__) as f: + assert f.mode == 'rb' + + def test_safe_mkdir(self): + testfn = self.get_testfn() + safe_mkdir(testfn) + assert os.path.isdir(testfn) + safe_mkdir(testfn) + assert os.path.isdir(testfn) + + def test_safe_rmpath(self): + # test file is removed + testfn = self.get_testfn() + open(testfn, 'w').close() + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test no exception if path does not exist + safe_rmpath(testfn) + # test dir is removed + os.mkdir(testfn) + safe_rmpath(testfn) + assert not os.path.exists(testfn) + # test other exceptions are raised + with mock.patch( + 'psutil.tests.os.stat', side_effect=OSError(errno.EINVAL, "") + ) as m: + with pytest.raises(OSError): + safe_rmpath(testfn) + assert m.called + + def test_chdir(self): + testfn = self.get_testfn() + base = os.getcwd() + os.mkdir(testfn) + with chdir(testfn): + assert os.getcwd() == os.path.join(base, testfn) + assert os.getcwd() == base + + +class TestProcessUtils(PsutilTestCase): + def test_reap_children(self): + subp = self.spawn_testproc() + p = psutil.Process(subp.pid) + assert p.is_running() + reap_children() + assert not p.is_running() + assert not psutil.tests._pids_started + assert not psutil.tests._subprocesses_started + + def test_spawn_children_pair(self): + child, grandchild = self.spawn_children_pair() + assert child.pid != grandchild.pid + assert child.is_running() + assert grandchild.is_running() + children = psutil.Process().children() + assert children == [child] + children = psutil.Process().children(recursive=True) + assert len(children) == 2 + assert child in children + assert grandchild in children + assert child.ppid() == os.getpid() + assert grandchild.ppid() == child.pid + + terminate(child) + assert not child.is_running() + assert grandchild.is_running() + + terminate(grandchild) + assert not grandchild.is_running() + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_spawn_zombie(self): + _parent, zombie = self.spawn_zombie() + assert zombie.status() == psutil.STATUS_ZOMBIE + + def test_terminate(self): + # by subprocess.Popen + p = self.spawn_testproc() + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Process + p = psutil.Process(self.spawn_testproc().pid) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by psutil.Popen + cmd = [ + PYTHON_EXE, + "-c", + "import time; [time.sleep(0.1) for x in range(100)];", + ] + p = psutil.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=PYTHON_EXE_ENV, + ) + terminate(p) + self.assertPidGone(p.pid) + terminate(p) + # by PID + pid = self.spawn_testproc().pid + terminate(pid) + self.assertPidGone(p.pid) + terminate(pid) + # zombie + if POSIX: + parent, zombie = self.spawn_zombie() + terminate(parent) + terminate(zombie) + self.assertPidGone(parent.pid) + self.assertPidGone(zombie.pid) + + +class TestNetUtils(PsutilTestCase): + def bind_socket(self): + port = get_free_port() + with bind_socket(addr=('', port)) as s: + assert s.getsockname()[1] == port + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_bind_unix_socket(self): + name = self.get_testfn() + with bind_unix_socket(name) as sock: + assert sock.family == socket.AF_UNIX + assert sock.type == socket.SOCK_STREAM + assert sock.getsockname() == name + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + # UDP + name = self.get_testfn() + with bind_unix_socket(name, type=socket.SOCK_DGRAM) as sock: + assert sock.type == socket.SOCK_DGRAM + + def test_tcp_socketpair(self): + addr = ("127.0.0.1", get_free_port()) + server, client = tcp_socketpair(socket.AF_INET, addr=addr) + with server, client: + # Ensure they are connected and the positions are correct. + assert server.getsockname() == addr + assert client.getpeername() == addr + assert client.getsockname() != addr + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + NETBSD or FREEBSD, reason="/var/run/log UNIX socket opened by default" + ) + def test_unix_socketpair(self): + p = psutil.Process() + num_fds = p.num_fds() + assert not filter_proc_net_connections(p.net_connections(kind='unix')) + name = self.get_testfn() + server, client = unix_socketpair(name) + try: + assert os.path.exists(name) + assert stat.S_ISSOCK(os.stat(name).st_mode) + assert p.num_fds() - num_fds == 2 + assert ( + len( + filter_proc_net_connections(p.net_connections(kind='unix')) + ) + == 2 + ) + assert server.getsockname() == name + assert client.getpeername() == name + finally: + client.close() + server.close() + + def test_create_sockets(self): + with create_sockets() as socks: + fams = collections.defaultdict(int) + types = collections.defaultdict(int) + for s in socks: + fams[s.family] += 1 + # work around http://bugs.python.org/issue30204 + types[s.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE)] += 1 + assert fams[socket.AF_INET] >= 2 + if supports_ipv6(): + assert fams[socket.AF_INET6] >= 2 + if POSIX and HAS_NET_CONNECTIONS_UNIX: + assert fams[socket.AF_UNIX] >= 2 + assert types[socket.SOCK_STREAM] >= 2 + assert types[socket.SOCK_DGRAM] >= 2 + + +@pytest.mark.xdist_group(name="serial") +class TestMemLeakClass(TestMemoryLeak): + @retry_on_failure() + def test_times(self): + def fun(): + cnt['cnt'] += 1 + + cnt = {'cnt': 0} + self.execute(fun, times=10, warmup_times=15) + assert cnt['cnt'] == 26 + + def test_param_err(self): + with pytest.raises(ValueError): + self.execute(lambda: 0, times=0) + with pytest.raises(ValueError): + self.execute(lambda: 0, times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, warmup_times=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, tolerance=-1) + with pytest.raises(ValueError): + self.execute(lambda: 0, retries=-1) + + @retry_on_failure() + @pytest.mark.skipif(CI_TESTING, reason="skipped on CI") + @pytest.mark.skipif(COVERAGE, reason="skipped during test coverage") + def test_leak_mem(self): + ls = [] + + def fun(ls=ls): + ls.append("x" * 248 * 1024) + + try: + # will consume around 60M in total + with pytest.raises(AssertionError, match="extra-mem"): + self.execute(fun, times=100) + finally: + del ls + + def test_unclosed_files(self): + def fun(): + f = open(__file__) # noqa: SIM115 + self.addCleanup(f.close) + box.append(f) + + box = [] + kind = "fd" if POSIX else "handle" + with pytest.raises(AssertionError, match="unclosed " + kind): + self.execute(fun) + + def test_tolerance(self): + def fun(): + ls.append("x" * 24 * 1024) + + ls = [] + times = 100 + self.execute( + fun, times=times, warmup_times=0, tolerance=200 * 1024 * 1024 + ) + assert len(ls) == times + 1 + + def test_execute_w_exc(self): + def fun_1(): + 1 / 0 # noqa: B018 + + self.execute_w_exc(ZeroDivisionError, fun_1) + with pytest.raises(ZeroDivisionError): + self.execute_w_exc(OSError, fun_1) + + def fun_2(): + pass + + with pytest.raises(AssertionError): + self.execute_w_exc(ZeroDivisionError, fun_2) + + +class TestFakePytest(PsutilTestCase): + def run_test_class(self, klass): + suite = unittest.TestSuite() + suite.addTest(klass) + runner = unittest.TextTestRunner() + result = runner.run(suite) + return result + + def test_raises(self): + with fake_pytest.raises(ZeroDivisionError) as cm: + 1 / 0 # noqa: B018 + assert isinstance(cm.value, ZeroDivisionError) + + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("foo") + + try: + with fake_pytest.raises(ValueError, match="foo") as cm: + raise ValueError("bar") + except AssertionError as err: + assert str(err) == '"foo" does not match "bar"' + else: + raise self.fail("exception not raised") + + def test_mark(self): + @fake_pytest.mark.xdist_group(name="serial") + def foo(): + return 1 + + assert foo() == 1 + + @fake_pytest.mark.xdist_group(name="serial") + class Foo: + def bar(self): + return 1 + + assert Foo().bar() == 1 + + def test_skipif(self): + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(True, reason="reason") + def foo(self): + assert 1 == 1 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + + class TestCase(unittest.TestCase): + @fake_pytest.mark.skipif(False, reason="reason") + def foo(self): + assert 1 == 1 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 0 + + def test_skip(self): + class TestCase(unittest.TestCase): + def foo(self): + fake_pytest.skip("reason") + assert 1 == 0 # noqa: PLR0133 + + result = self.run_test_class(TestCase("foo")) + assert result.wasSuccessful() + assert len(result.skipped) == 1 + assert result.skipped[0][1] == "reason" + + def test_main(self): + tmpdir = self.get_testfn(dir=HERE) + os.mkdir(tmpdir) + with open(os.path.join(tmpdir, "__init__.py"), "w"): + pass + with open(os.path.join(tmpdir, "test_file.py"), "w") as f: + f.write(textwrap.dedent("""\ + import unittest + + class TestCase(unittest.TestCase): + def test_passed(self): + pass + """).lstrip()) + with mock.patch.object(psutil.tests, "HERE", tmpdir): + with self.assertWarnsRegex( + UserWarning, "Fake pytest module was used" + ): + suite = fake_pytest.main() + assert suite.countTestCases() == 1 + + def test_warns(self): + # success + with fake_pytest.warns(UserWarning): + warnings.warn("foo", UserWarning, stacklevel=1) + + # failure + try: + with fake_pytest.warns(UserWarning): + warnings.warn("foo", DeprecationWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + # match success + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("foo", UserWarning, stacklevel=1) + + # match failure + try: + with fake_pytest.warns(UserWarning, match="foo"): + warnings.warn("bar", UserWarning, stacklevel=1) + except AssertionError: + pass + else: + raise self.fail("exception not raised") + + +class TestTestingUtils(PsutilTestCase): + def test_process_namespace(self): + p = psutil.Process() + ns = process_namespace(p) + ns.test() + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'ppid')[0] + assert fun() == p.ppid() + + def test_system_namespace(self): + ns = system_namespace() + fun = next(x for x in ns.iter(ns.getters) if x[1] == 'net_if_addrs')[0] + assert fun() == psutil.net_if_addrs() + + +class TestOtherUtils(PsutilTestCase): + def test_is_namedtuple(self): + assert is_namedtuple(collections.namedtuple('foo', 'a b c')(1, 2, 3)) + assert not is_namedtuple(tuple()) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_unicode.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_unicode.py new file mode 100644 index 0000000000000000000000000000000000000000..d8a8c4bfc55e7f077a6f27ca2b42dab183ba88ed --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_unicode.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Notes about unicode handling in psutil +======================================. + +Starting from version 5.3.0 psutil adds unicode support, see: +https://github.com/giampaolo/psutil/issues/1040 +The notes below apply to *any* API returning a string such as +process exe(), cwd() or username(): + +* all strings are encoded by using the OS filesystem encoding + (sys.getfilesystemencoding()) which varies depending on the platform + (e.g. "UTF-8" on macOS, "mbcs" on Win) +* no API call is supposed to crash with UnicodeDecodeError +* instead, in case of badly encoded data returned by the OS, the + following error handlers are used to replace the corrupted characters in + the string: + * sys.getfilesystemencodeerrors() or "surrogatescape" on POSIX and + "replace" on Windows. + +For a detailed explanation of how psutil handles unicode see #1040. + +Tests +===== + +List of APIs returning or dealing with a string: +('not tested' means they are not tested to deal with non-ASCII strings): + +* Process.cmdline() +* Process.cwd() +* Process.environ() +* Process.exe() +* Process.memory_maps() +* Process.name() +* Process.net_connections('unix') +* Process.open_files() +* Process.username() (not tested) + +* disk_io_counters() (not tested) +* disk_partitions() (not tested) +* disk_usage(str) +* net_connections('unix') +* net_if_addrs() (not tested) +* net_if_stats() (not tested) +* net_io_counters() (not tested) +* sensors_fans() (not tested) +* sensors_temperatures() (not tested) +* users() (not tested) + +* WindowsService.binpath() (not tested) +* WindowsService.description() (not tested) +* WindowsService.display_name() (not tested) +* WindowsService.name() (not tested) +* WindowsService.status() (not tested) +* WindowsService.username() (not tested) + +In here we create a unicode path with a funky non-ASCII name and (where +possible) make psutil return it back (e.g. on name(), exe(), open_files(), +etc.) and make sure that: + +* psutil never crashes with UnicodeDecodeError +* the returned path matches +""" + +import os +import shutil +import warnings +from contextlib import closing + +import psutil +from psutil import BSD +from psutil import POSIX +from psutil import WINDOWS +from psutil.tests import ASCII_FS +from psutil.tests import CI_TESTING +from psutil.tests import HAS_ENVIRON +from psutil.tests import HAS_MEMORY_MAPS +from psutil.tests import HAS_NET_CONNECTIONS_UNIX +from psutil.tests import INVALID_UNICODE_SUFFIX +from psutil.tests import PYPY +from psutil.tests import TESTFN_PREFIX +from psutil.tests import UNICODE_SUFFIX +from psutil.tests import PsutilTestCase +from psutil.tests import bind_unix_socket +from psutil.tests import chdir +from psutil.tests import copyload_shared_lib +from psutil.tests import create_py_exe +from psutil.tests import get_testfn +from psutil.tests import pytest +from psutil.tests import safe_mkdir +from psutil.tests import safe_rmpath +from psutil.tests import skip_on_access_denied +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +def try_unicode(suffix): + """Return True if both the fs and the subprocess module can + deal with a unicode file name. + """ + sproc = None + testfn = get_testfn(suffix=suffix) + try: + safe_rmpath(testfn) + create_py_exe(testfn) + sproc = spawn_testproc(cmd=[testfn]) + shutil.copyfile(testfn, testfn + '-2') + safe_rmpath(testfn + '-2') + except (UnicodeEncodeError, OSError): + return False + else: + return True + finally: + if sproc is not None: + terminate(sproc) + safe_rmpath(testfn) + + +# =================================================================== +# FS APIs +# =================================================================== + + +class BaseUnicodeTest(PsutilTestCase): + funky_suffix = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.skip_tests = False + cls.funky_name = None + if cls.funky_suffix is not None: + if not try_unicode(cls.funky_suffix): + cls.skip_tests = True + else: + cls.funky_name = get_testfn(suffix=cls.funky_suffix) + create_py_exe(cls.funky_name) + + def setUp(self): + super().setUp() + if self.skip_tests: + raise pytest.skip("can't handle unicode str") + + +@pytest.mark.xdist_group(name="serial") +@pytest.mark.skipif(ASCII_FS, reason="ASCII fs") +class TestFSAPIs(BaseUnicodeTest): + """Test FS APIs with a funky, valid, UTF8 path name.""" + + funky_suffix = UNICODE_SUFFIX + + def expect_exact_path_match(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return self.funky_name in os.listdir(".") + + # --- + + def test_proc_exe(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + exe = p.exe() + assert isinstance(exe, str) + if self.expect_exact_path_match(): + assert os.path.normcase(exe) == os.path.normcase(self.funky_name) + + def test_proc_name(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + name = psutil.Process(subp.pid).name() + assert isinstance(name, str) + if self.expect_exact_path_match(): + assert name == os.path.basename(self.funky_name) + + def test_proc_cmdline(self): + cmd = [ + self.funky_name, + "-c", + "import time; [time.sleep(0.1) for x in range(100)]", + ] + subp = self.spawn_testproc(cmd) + p = psutil.Process(subp.pid) + cmdline = p.cmdline() + for part in cmdline: + assert isinstance(part, str) + if self.expect_exact_path_match(): + assert cmdline == cmd + + def test_proc_cwd(self): + dname = self.funky_name + "2" + self.addCleanup(safe_rmpath, dname) + safe_mkdir(dname) + with chdir(dname): + p = psutil.Process() + cwd = p.cwd() + assert isinstance(p.cwd(), str) + if self.expect_exact_path_match(): + assert cwd == dname + + @pytest.mark.skipif(PYPY and WINDOWS, reason="fails on PYPY + WINDOWS") + def test_proc_open_files(self): + p = psutil.Process() + start = set(p.open_files()) + with open(self.funky_name, 'rb'): + new = set(p.open_files()) + path = (new - start).pop().path + assert isinstance(path, str) + if BSD and not path: + # XXX - see https://github.com/giampaolo/psutil/issues/595 + raise pytest.skip("open_files on BSD is broken") + if self.expect_exact_path_match(): + assert os.path.normcase(path) == os.path.normcase(self.funky_name) + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + def test_proc_net_connections(self): + name = self.get_testfn(suffix=self.funky_suffix) + sock = bind_unix_socket(name) + with closing(sock): + conn = psutil.Process().net_connections('unix')[0] + assert isinstance(conn.laddr, str) + assert conn.laddr == name + + @pytest.mark.skipif(not POSIX, reason="POSIX only") + @pytest.mark.skipif( + not HAS_NET_CONNECTIONS_UNIX, reason="can't list UNIX sockets" + ) + @skip_on_access_denied() + def test_net_connections(self): + def find_sock(cons): + for conn in cons: + if os.path.basename(conn.laddr).startswith(TESTFN_PREFIX): + return conn + raise ValueError("connection not found") + + name = self.get_testfn(suffix=self.funky_suffix) + sock = bind_unix_socket(name) + with closing(sock): + cons = psutil.net_connections(kind='unix') + conn = find_sock(cons) + assert isinstance(conn.laddr, str) + assert conn.laddr == name + + def test_disk_usage(self): + dname = self.funky_name + "2" + self.addCleanup(safe_rmpath, dname) + safe_mkdir(dname) + psutil.disk_usage(dname) + + @pytest.mark.skipif(not HAS_MEMORY_MAPS, reason="not supported") + @pytest.mark.skipif(PYPY, reason="unstable on PYPY") + def test_memory_maps(self): + with copyload_shared_lib(suffix=self.funky_suffix) as funky_path: + + def normpath(p): + return os.path.realpath(os.path.normcase(p)) + + libpaths = [ + normpath(x.path) for x in psutil.Process().memory_maps() + ] + # ...just to have a clearer msg in case of failure + libpaths = [x for x in libpaths if TESTFN_PREFIX in x] + assert normpath(funky_path) in libpaths + for path in libpaths: + assert isinstance(path, str) + + +@pytest.mark.skipif(CI_TESTING, reason="unreliable on CI") +class TestFSAPIsWithInvalidPath(TestFSAPIs): + """Test FS APIs with a funky, invalid path name.""" + + funky_suffix = INVALID_UNICODE_SUFFIX + + def expect_exact_path_match(self): + return True + + +# =================================================================== +# Non fs APIs +# =================================================================== + + +class TestNonFSAPIS(BaseUnicodeTest): + """Unicode tests for non fs-related APIs.""" + + funky_suffix = UNICODE_SUFFIX + + @pytest.mark.skipif(not HAS_ENVIRON, reason="not supported") + @pytest.mark.skipif(PYPY and WINDOWS, reason="segfaults on PYPY + WINDOWS") + def test_proc_environ(self): + # Note: differently from others, this test does not deal + # with fs paths. + env = os.environ.copy() + env['FUNNY_ARG'] = self.funky_suffix + sproc = self.spawn_testproc(env=env) + p = psutil.Process(sproc.pid) + env = p.environ() + for k, v in env.items(): + assert isinstance(k, str) + assert isinstance(v, str) + assert env['FUNNY_ARG'] == self.funky_suffix diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_windows.py b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_windows.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c536b468d44dfbbe44f4d5cfa6f443913cca57 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/psutil/tests/test_windows.py @@ -0,0 +1,914 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Windows specific tests.""" + +import datetime +import glob +import os +import platform +import re +import shutil +import signal +import subprocess +import sys +import time +import warnings +from unittest import mock + +import psutil +from psutil import WINDOWS +from psutil.tests import GITHUB_ACTIONS +from psutil.tests import HAS_BATTERY +from psutil.tests import IS_64BIT +from psutil.tests import PYPY +from psutil.tests import TOLERANCE_DISK_USAGE +from psutil.tests import TOLERANCE_SYS_MEM +from psutil.tests import PsutilTestCase +from psutil.tests import pytest +from psutil.tests import retry_on_failure +from psutil.tests import sh +from psutil.tests import spawn_testproc +from psutil.tests import terminate + + +if WINDOWS and not PYPY: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + import win32api # requires "pip install pywin32" + import win32con + import win32process + import wmi # requires "pip install wmi" / "make install-pydeps-test" + +if WINDOWS: + from psutil._pswindows import convert_oserror + + +cext = psutil._psplatform.cext + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +@pytest.mark.skipif(PYPY, reason="pywin32 not available on PYPY") +class WindowsTestCase(PsutilTestCase): + pass + + +def powershell(cmd): + """Currently not used, but available just in case. Usage: + + >>> powershell( + "Get-CIMInstance Win32_PageFileUsage | Select AllocatedBaseSize") + """ + if not shutil.which("powershell.exe"): + raise pytest.skip("powershell.exe not available") + cmdline = ( + "powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive " + f"-NoProfile -WindowStyle Hidden -Command \"{cmd}\"" # noqa: Q003 + ) + return sh(cmdline) + + +def wmic(path, what, converter=int): + """Currently not used, but available just in case. Usage: + + >>> wmic("Win32_OperatingSystem", "FreePhysicalMemory") + 2134124534 + """ + out = sh(f"wmic path {path} get {what}").strip() + data = "".join(out.splitlines()[1:]).strip() # get rid of the header + if converter is not None: + if "," in what: + return tuple(converter(x) for x in data.split()) + else: + return converter(data) + else: + return data + + +# =================================================================== +# System APIs +# =================================================================== + + +class TestCpuAPIs(WindowsTestCase): + @pytest.mark.skipif( + 'NUMBER_OF_PROCESSORS' not in os.environ, + reason="NUMBER_OF_PROCESSORS env var is not available", + ) + def test_cpu_count_vs_NUMBER_OF_PROCESSORS(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + num_cpus = int(os.environ['NUMBER_OF_PROCESSORS']) + assert num_cpus == psutil.cpu_count() + + def test_cpu_count_vs_GetSystemInfo(self): + # Will likely fail on many-cores systems: + # https://stackoverflow.com/questions/31209256 + sys_value = win32api.GetSystemInfo()[5] + psutil_value = psutil.cpu_count() + assert sys_value == psutil_value + + def test_cpu_count_logical_vs_wmi(self): + w = wmi.WMI() + procs = sum( + proc.NumberOfLogicalProcessors for proc in w.Win32_Processor() + ) + assert psutil.cpu_count() == procs + + def test_cpu_count_cores_vs_wmi(self): + w = wmi.WMI() + cores = sum(proc.NumberOfCores for proc in w.Win32_Processor()) + assert psutil.cpu_count(logical=False) == cores + + def test_cpu_count_vs_cpu_times(self): + assert psutil.cpu_count() == len(psutil.cpu_times(percpu=True)) + + def test_cpu_freq(self): + w = wmi.WMI() + proc = w.Win32_Processor()[0] + assert proc.CurrentClockSpeed == psutil.cpu_freq().current + assert proc.MaxClockSpeed == psutil.cpu_freq().max + + +class TestSystemAPIs(WindowsTestCase): + def test_nic_names(self): + out = sh('ipconfig /all') + nics = psutil.net_io_counters(pernic=True).keys() + for nic in nics: + if "pseudo-interface" in nic.replace(' ', '-').lower(): + continue + if nic not in out: + raise self.fail( + f"{nic!r} nic wasn't found in 'ipconfig /all' output" + ) + + def test_total_phymem(self): + w = wmi.WMI().Win32_ComputerSystem()[0] + assert int(w.TotalPhysicalMemory) == psutil.virtual_memory().total + + def test_free_phymem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + abs(int(w.AvailableBytes) - psutil.virtual_memory().free) + < TOLERANCE_SYS_MEM + ) + + def test_total_swapmem(self): + w = wmi.WMI().Win32_PerfRawData_PerfOS_Memory()[0] + assert ( + int(w.CommitLimit) - psutil.virtual_memory().total + == psutil.swap_memory().total + ) + if psutil.swap_memory().total == 0: + assert psutil.swap_memory().free == 0 + assert psutil.swap_memory().used == 0 + + def test_percent_swapmem(self): + if psutil.swap_memory().total > 0: + w = wmi.WMI().Win32_PerfRawData_PerfOS_PagingFile(Name="_Total")[0] + # calculate swap usage to percent + percentSwap = int(w.PercentUsage) * 100 / int(w.PercentUsage_Base) + # exact percent may change but should be reasonable + # assert within +/- 5% and between 0 and 100% + assert psutil.swap_memory().percent >= 0 + assert abs(psutil.swap_memory().percent - percentSwap) < 5 + assert psutil.swap_memory().percent <= 100 + + # @pytest.mark.skipif(wmi is None, reason="wmi module is not installed") + # def test__UPTIME(self): + # # _UPTIME constant is not public but it is used internally + # # as value to return for pid 0 creation time. + # # WMI behaves the same. + # w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + # p = psutil.Process(0) + # wmic_create = str(w.CreationDate.split('.')[0]) + # psutil_create = time.strftime("%Y%m%d%H%M%S", + # time.localtime(p.create_time())) + + # Note: this test is not very reliable + @retry_on_failure() + def test_pids(self): + # Note: this test might fail if the OS is starting/killing + # other processes in the meantime + w = wmi.WMI().Win32_Process() + wmi_pids = {x.ProcessId for x in w} + psutil_pids = set(psutil.pids()) + assert wmi_pids == psutil_pids + + @retry_on_failure() + def test_disks(self): + ps_parts = psutil.disk_partitions(all=True) + wmi_parts = wmi.WMI().Win32_LogicalDisk() + for ps_part in ps_parts: + for wmi_part in wmi_parts: + if ps_part.device.replace('\\', '') == wmi_part.DeviceID: + if not ps_part.mountpoint: + # this is usually a CD-ROM with no disk inserted + break + if 'cdrom' in ps_part.opts: + break + if ps_part.mountpoint.startswith('A:'): + break # floppy + try: + usage = psutil.disk_usage(ps_part.mountpoint) + except FileNotFoundError: + # usually this is the floppy + break + assert usage.total == int(wmi_part.Size) + wmi_free = int(wmi_part.FreeSpace) + assert usage.free == wmi_free + # 10 MB tolerance + if abs(usage.free - wmi_free) > 10 * 1024 * 1024: + raise self.fail(f"psutil={usage.free}, wmi={wmi_free}") + break + else: + raise self.fail(f"can't find partition {ps_part!r}") + + @retry_on_failure() + def test_disk_usage(self): + for disk in psutil.disk_partitions(): + if 'cdrom' in disk.opts: + continue + sys_value = win32api.GetDiskFreeSpaceEx(disk.mountpoint) + psutil_value = psutil.disk_usage(disk.mountpoint) + assert abs(sys_value[0] - psutil_value.free) < TOLERANCE_DISK_USAGE + assert ( + abs(sys_value[1] - psutil_value.total) < TOLERANCE_DISK_USAGE + ) + assert psutil_value.used == psutil_value.total - psutil_value.free + + def test_disk_partitions(self): + sys_value = [ + x + '\\' + for x in win32api.GetLogicalDriveStrings().split("\\\x00") + if x and not x.startswith('A:') + ] + psutil_value = [ + x.mountpoint + for x in psutil.disk_partitions(all=True) + if not x.mountpoint.startswith('A:') + ] + assert sys_value == psutil_value + + def test_net_if_stats(self): + ps_names = set(cext.net_if_stats()) + wmi_adapters = wmi.WMI().Win32_NetworkAdapter() + wmi_names = set() + for wmi_adapter in wmi_adapters: + wmi_names.add(wmi_adapter.Name) + wmi_names.add(wmi_adapter.NetConnectionID) + assert ( + ps_names & wmi_names + ), f"no common entries in {ps_names}, {wmi_names}" + + def test_boot_time(self): + wmi_os = wmi.WMI().Win32_OperatingSystem() + wmi_btime_str = wmi_os[0].LastBootUpTime.split('.')[0] + wmi_btime_dt = datetime.datetime.strptime( + wmi_btime_str, "%Y%m%d%H%M%S" + ) + psutil_dt = datetime.datetime.fromtimestamp(psutil.boot_time()) + diff = abs((wmi_btime_dt - psutil_dt).total_seconds()) + assert diff <= 5 + + def test_boot_time_fluctuation(self): + # https://github.com/giampaolo/psutil/issues/1007 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=5): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=4): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=6): + assert psutil.boot_time() == 5 + with mock.patch('psutil._pswindows.cext.boot_time', return_value=333): + assert psutil.boot_time() == 333 + + +# =================================================================== +# sensors_battery() +# =================================================================== + + +class TestSensorsBattery(WindowsTestCase): + def test_has_battery(self): + if win32api.GetPwrCapabilities()['SystemBatteriesPresent']: + assert psutil.sensors_battery() is not None + else: + assert psutil.sensors_battery() is None + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_percent(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + assert ( + abs(battery_psutil.percent - battery_wmi.EstimatedChargeRemaining) + < 1 + ) + + @pytest.mark.skipif(not HAS_BATTERY, reason="no battery") + def test_power_plugged(self): + w = wmi.WMI() + battery_wmi = w.query('select * from Win32_Battery')[0] + battery_psutil = psutil.sensors_battery() + # Status codes: + # https://msdn.microsoft.com/en-us/library/aa394074(v=vs.85).aspx + assert battery_psutil.power_plugged == (battery_wmi.BatteryStatus == 2) + + def test_emulate_no_battery(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", + return_value=(0, 128, 0, 0), + ) as m: + assert psutil.sensors_battery() is None + assert m.called + + def test_emulate_power_connected(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", return_value=(1, 0, 0, 0) + ) as m: + assert ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_power_charging(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", return_value=(0, 8, 0, 0) + ) as m: + assert ( + psutil.sensors_battery().secsleft + == psutil.POWER_TIME_UNLIMITED + ) + assert m.called + + def test_emulate_secs_left_unknown(self): + with mock.patch( + "psutil._pswindows.cext.sensors_battery", + return_value=(0, 0, 0, -1), + ) as m: + assert ( + psutil.sensors_battery().secsleft == psutil.POWER_TIME_UNKNOWN + ) + assert m.called + + +# =================================================================== +# Process APIs +# =================================================================== + + +class TestProcess(WindowsTestCase): + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_issue_24(self): + p = psutil.Process(0) + with pytest.raises(psutil.AccessDenied): + p.kill() + + def test_special_pid(self): + p = psutil.Process(4) + assert p.name() == 'System' + # use __str__ to access all common Process properties to check + # that nothing strange happens + str(p) + p.username() + assert p.create_time() >= 0.0 + try: + rss, _vms = p.memory_info()[:2] + except psutil.AccessDenied: + # expected on Windows Vista and Windows 7 + if platform.uname()[1] not in {'vista', 'win-7', 'win7'}: + raise + else: + assert rss > 0 + + def test_send_signal(self): + p = psutil.Process(self.pid) + with pytest.raises(ValueError): + p.send_signal(signal.SIGINT) + + def test_num_handles_increment(self): + p = psutil.Process(os.getpid()) + before = p.num_handles() + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + after = p.num_handles() + assert after == before + 1 + win32api.CloseHandle(handle) + assert p.num_handles() == before + + def test_ctrl_signals(self): + p = psutil.Process(self.spawn_testproc().pid) + p.send_signal(signal.CTRL_C_EVENT) + p.send_signal(signal.CTRL_BREAK_EVENT) + p.kill() + p.wait() + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_C_EVENT) + with pytest.raises(psutil.NoSuchProcess): + p.send_signal(signal.CTRL_BREAK_EVENT) + + def test_username(self): + name = win32api.GetUserNameEx(win32con.NameSamCompatible) + if name.endswith('$'): + # When running as a service account (most likely to be + # NetworkService), these user name calculations don't produce the + # same result, causing the test to fail. + raise pytest.skip('running as service account') + assert psutil.Process().username() == name + + def test_cmdline(self): + sys_value = re.sub(r"[ ]+", " ", win32api.GetCommandLine()).strip() + psutil_value = ' '.join(psutil.Process().cmdline()) + if sys_value[0] == '"' != psutil_value[0]: + # The PyWin32 command line may retain quotes around argv[0] if they + # were used unnecessarily, while psutil will omit them. So remove + # the first 2 quotes from sys_value if not in psutil_value. + # A path to an executable will not contain quotes, so this is safe. + sys_value = sys_value.replace('"', '', 2) + assert sys_value == psutil_value + + # XXX - occasional failures + + # def test_cpu_times(self): + # handle = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, + # win32con.FALSE, os.getpid()) + # self.addCleanup(win32api.CloseHandle, handle) + # sys_value = win32process.GetProcessTimes(handle) + # psutil_value = psutil.Process().cpu_times() + # self.assertAlmostEqual( + # psutil_value.user, sys_value['UserTime'] / 10000000.0, + # delta=0.2) + # self.assertAlmostEqual( + # psutil_value.user, sys_value['KernelTime'] / 10000000.0, + # delta=0.2) + + def test_nice(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetPriorityClass(handle) + psutil_value = psutil.Process().nice() + assert psutil_value == sys_value + + def test_memory_info(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetProcessMemoryInfo(handle) + psutil_value = psutil.Process(self.pid).memory_info() + assert sys_value['PeakWorkingSetSize'] == psutil_value.peak_wset + assert sys_value['WorkingSetSize'] == psutil_value.wset + assert ( + sys_value['QuotaPeakPagedPoolUsage'] + == psutil_value.peak_paged_pool + ) + assert sys_value['QuotaPagedPoolUsage'] == psutil_value.paged_pool + assert ( + sys_value['QuotaPeakNonPagedPoolUsage'] + == psutil_value.peak_nonpaged_pool + ) + assert ( + sys_value['QuotaNonPagedPoolUsage'] == psutil_value.nonpaged_pool + ) + assert sys_value['PagefileUsage'] == psutil_value.pagefile + assert sys_value['PeakPagefileUsage'] == psutil_value.peak_pagefile + + assert psutil_value.rss == psutil_value.wset + assert psutil_value.vms == psutil_value.pagefile + + def test_wait(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + p = psutil.Process(self.pid) + p.terminate() + psutil_value = p.wait() + sys_value = win32process.GetExitCodeProcess(handle) + assert psutil_value == sys_value + + def test_cpu_affinity(self): + def from_bitmask(x): + return [i for i in range(64) if (1 << i) & x] + + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, self.pid + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = from_bitmask( + win32process.GetProcessAffinityMask(handle)[0] + ) + psutil_value = psutil.Process(self.pid).cpu_affinity() + assert psutil_value == sys_value + + def test_io_counters(self): + handle = win32api.OpenProcess( + win32con.PROCESS_QUERY_INFORMATION, win32con.FALSE, os.getpid() + ) + self.addCleanup(win32api.CloseHandle, handle) + sys_value = win32process.GetProcessIoCounters(handle) + psutil_value = psutil.Process().io_counters() + assert psutil_value.read_count == sys_value['ReadOperationCount'] + assert psutil_value.write_count == sys_value['WriteOperationCount'] + assert psutil_value.read_bytes == sys_value['ReadTransferCount'] + assert psutil_value.write_bytes == sys_value['WriteTransferCount'] + assert psutil_value.other_count == sys_value['OtherOperationCount'] + assert psutil_value.other_bytes == sys_value['OtherTransferCount'] + + def test_num_handles(self): + import ctypes + import ctypes.wintypes + + PROCESS_QUERY_INFORMATION = 0x400 + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_INFORMATION, 0, self.pid + ) + self.addCleanup(ctypes.windll.kernel32.CloseHandle, handle) + + hndcnt = ctypes.wintypes.DWORD() + ctypes.windll.kernel32.GetProcessHandleCount( + handle, ctypes.byref(hndcnt) + ) + sys_value = hndcnt.value + psutil_value = psutil.Process(self.pid).num_handles() + assert psutil_value == sys_value + + def test_error_partial_copy(self): + # https://github.com/giampaolo/psutil/issues/875 + exc = OSError() + exc.winerror = 299 + with mock.patch("psutil._psplatform.cext.proc_cwd", side_effect=exc): + with mock.patch("time.sleep") as m: + p = psutil.Process() + with pytest.raises(psutil.AccessDenied): + p.cwd() + assert m.call_count >= 5 + + def test_exe(self): + # NtQuerySystemInformation succeeds if process is gone. Make sure + # it raises NSP for a non existent pid. + pid = psutil.pids()[-1] + 99999 + proc = psutil._psplatform.Process(pid) + with pytest.raises(psutil.NoSuchProcess): + proc.exe() + + +class TestProcessWMI(WindowsTestCase): + """Compare Process API results with WMI.""" + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_name(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + assert p.name() == w.Caption + + # This fail on github because using virtualenv for test environment + @pytest.mark.skipif( + GITHUB_ACTIONS, reason="unreliable path on GITHUB_ACTIONS" + ) + def test_exe(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + # Note: wmi reports the exe as a lower case string. + # Being Windows paths case-insensitive we ignore that. + assert p.exe().lower() == w.ExecutablePath.lower() + + def test_cmdline(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + assert ' '.join(p.cmdline()) == w.CommandLine.replace('"', '') + + def test_username(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + domain, _, username = w.GetOwner() + username = f"{domain}\\{username}" + assert p.username() == username + + @retry_on_failure() + def test_memory_rss(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + rss = p.memory_info().rss + assert rss == int(w.WorkingSetSize) + + @retry_on_failure() + def test_memory_vms(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + vms = p.memory_info().vms + # http://msdn.microsoft.com/en-us/library/aa394372(VS.85).aspx + # ...claims that PageFileUsage is represented in Kilo + # bytes but funnily enough on certain platforms bytes are + # returned instead. + wmi_usage = int(w.PageFileUsage) + if vms not in {wmi_usage, wmi_usage * 1024}: + raise self.fail(f"wmi={wmi_usage}, psutil={vms}") + + def test_create_time(self): + w = wmi.WMI().Win32_Process(ProcessId=self.pid)[0] + p = psutil.Process(self.pid) + wmic_create = str(w.CreationDate.split('.')[0]) + psutil_create = time.strftime( + "%Y%m%d%H%M%S", time.localtime(p.create_time()) + ) + assert wmic_create == psutil_create + + +# --- + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestDualProcessImplementation(PsutilTestCase): + """Certain APIs on Windows have 2 internal implementations, one + based on documented Windows APIs, another one based + NtQuerySystemInformation() which gets called as fallback in + case the first fails because of limited permission error. + Here we test that the two methods return the exact same value, + see: + https://github.com/giampaolo/psutil/issues/304. + """ + + @classmethod + def setUpClass(cls): + cls.pid = spawn_testproc().pid + + @classmethod + def tearDownClass(cls): + terminate(cls.pid) + + def test_memory_info(self): + mem_1 = psutil.Process(self.pid).memory_info() + with mock.patch( + "psutil._psplatform.cext.proc_memory_info", + side_effect=PermissionError, + ) as fun: + mem_2 = psutil.Process(self.pid).memory_info() + assert len(mem_1) == len(mem_2) + for i in range(len(mem_1)): + assert mem_1[i] >= 0 + assert mem_2[i] >= 0 + assert abs(mem_1[i] - mem_2[i]) < 512 + assert fun.called + + def test_create_time(self): + ctime = psutil.Process(self.pid).create_time() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=PermissionError, + ) as fun: + assert psutil.Process(self.pid).create_time() == ctime + assert fun.called + + def test_cpu_times(self): + cpu_times_1 = psutil.Process(self.pid).cpu_times() + with mock.patch( + "psutil._psplatform.cext.proc_times", + side_effect=PermissionError, + ) as fun: + cpu_times_2 = psutil.Process(self.pid).cpu_times() + assert fun.called + assert abs(cpu_times_1.user - cpu_times_2.user) < 0.01 + assert abs(cpu_times_1.system - cpu_times_2.system) < 0.01 + + def test_io_counters(self): + io_counters_1 = psutil.Process(self.pid).io_counters() + with mock.patch( + "psutil._psplatform.cext.proc_io_counters", + side_effect=PermissionError, + ) as fun: + io_counters_2 = psutil.Process(self.pid).io_counters() + for i in range(len(io_counters_1)): + assert abs(io_counters_1[i] - io_counters_2[i]) < 5 + assert fun.called + + def test_num_handles(self): + num_handles = psutil.Process(self.pid).num_handles() + with mock.patch( + "psutil._psplatform.cext.proc_num_handles", + side_effect=PermissionError, + ) as fun: + assert psutil.Process(self.pid).num_handles() == num_handles + assert fun.called + + def test_cmdline(self): + for pid in psutil.pids(): + try: + a = cext.proc_cmdline(pid, use_peb=True) + b = cext.proc_cmdline(pid, use_peb=False) + except OSError as err: + err = convert_oserror(err) + if not isinstance( + err, (psutil.AccessDenied, psutil.NoSuchProcess) + ): + raise + else: + assert a == b + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class RemoteProcessTestCase(PsutilTestCase): + """Certain functions require calling ReadProcessMemory. + This trivially works when called on the current process. + Check that this works on other processes, especially when they + have a different bitness. + """ + + @staticmethod + def find_other_interpreter(): + # find a python interpreter that is of the opposite bitness from us + code = "import sys; sys.stdout.write(str(sys.maxsize > 2**32))" + + # XXX: a different and probably more stable approach might be to access + # the registry but accessing 64 bit paths from a 32 bit process + for filename in glob.glob(r"C:\Python*\python.exe"): + proc = subprocess.Popen( + args=[filename, "-c", code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output, _ = proc.communicate() + proc.wait() + if output == str(not IS_64BIT): + return filename + + test_args = ["-c", "import sys; sys.stdin.read()"] + + def setUp(self): + super().setUp() + + other_python = self.find_other_interpreter() + if other_python is None: + raise pytest.skip( + "could not find interpreter with opposite bitness" + ) + if IS_64BIT: + self.python64 = sys.executable + self.python32 = other_python + else: + self.python64 = other_python + self.python32 = sys.executable + + env = os.environ.copy() + env["THINK_OF_A_NUMBER"] = str(os.getpid()) + self.proc32 = self.spawn_testproc( + [self.python32] + self.test_args, env=env, stdin=subprocess.PIPE + ) + self.proc64 = self.spawn_testproc( + [self.python64] + self.test_args, env=env, stdin=subprocess.PIPE + ) + + def tearDown(self): + super().tearDown() + self.proc32.communicate() + self.proc64.communicate() + + def test_cmdline_32(self): + p = psutil.Process(self.proc32.pid) + assert len(p.cmdline()) == 3 + assert p.cmdline()[1:] == self.test_args + + def test_cmdline_64(self): + p = psutil.Process(self.proc64.pid) + assert len(p.cmdline()) == 3 + assert p.cmdline()[1:] == self.test_args + + def test_cwd_32(self): + p = psutil.Process(self.proc32.pid) + assert p.cwd() == os.getcwd() + + def test_cwd_64(self): + p = psutil.Process(self.proc64.pid) + assert p.cwd() == os.getcwd() + + def test_environ_32(self): + p = psutil.Process(self.proc32.pid) + e = p.environ() + assert "THINK_OF_A_NUMBER" in e + assert e["THINK_OF_A_NUMBER"] == str(os.getpid()) + + def test_environ_64(self): + p = psutil.Process(self.proc64.pid) + try: + p.environ() + except psutil.AccessDenied: + pass + + +# =================================================================== +# Windows services +# =================================================================== + + +@pytest.mark.skipif(not WINDOWS, reason="WINDOWS only") +class TestServices(PsutilTestCase): + def test_win_service_iter(self): + valid_statuses = { + "running", + "paused", + "start", + "pause", + "continue", + "stop", + "stopped", + } + valid_start_types = {"automatic", "manual", "disabled"} + valid_statuses = { + "running", + "paused", + "start_pending", + "pause_pending", + "continue_pending", + "stop_pending", + "stopped", + } + for serv in psutil.win_service_iter(): + data = serv.as_dict() + assert isinstance(data['name'], str) + assert data['name'].strip() + assert isinstance(data['display_name'], str) + assert isinstance(data['username'], str) + assert data['status'] in valid_statuses + if data['pid'] is not None: + psutil.Process(data['pid']) + assert isinstance(data['binpath'], str) + assert isinstance(data['username'], str) + assert isinstance(data['start_type'], str) + assert data['start_type'] in valid_start_types + assert data['status'] in valid_statuses + assert isinstance(data['description'], str) + pid = serv.pid() + if pid is not None: + p = psutil.Process(pid) + assert p.is_running() + # win_service_get + s = psutil.win_service_get(serv.name()) + # test __eq__ + assert serv == s + + def test_win_service_get(self): + ERROR_SERVICE_DOES_NOT_EXIST = ( + psutil._psplatform.cext.ERROR_SERVICE_DOES_NOT_EXIST + ) + ERROR_ACCESS_DENIED = psutil._psplatform.cext.ERROR_ACCESS_DENIED + + name = next(psutil.win_service_iter()).name() + with pytest.raises(psutil.NoSuchProcess) as cm: + psutil.win_service_get(name + '???') + assert cm.value.name == name + '???' + + # test NoSuchProcess + service = psutil.win_service_get(name) + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_SERVICE_DOES_NOT_EXIST + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + with pytest.raises(psutil.NoSuchProcess): + service.status() + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + with pytest.raises(psutil.NoSuchProcess): + service.username() + + # test AccessDenied + exc = OSError(0, "msg", 0) + exc.winerror = ERROR_ACCESS_DENIED + with mock.patch( + "psutil._psplatform.cext.winservice_query_status", side_effect=exc + ): + with pytest.raises(psutil.AccessDenied): + service.status() + with mock.patch( + "psutil._psplatform.cext.winservice_query_config", side_effect=exc + ): + with pytest.raises(psutil.AccessDenied): + service.username() + + # test __str__ and __repr__ + assert service.name() in str(service) + assert service.display_name() in str(service) + assert service.name() in repr(service) + assert service.display_name() in repr(service) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da1f3e8b007b29bac03dbafd17b0da4dd086ef26 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/_constants.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/_constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aff36a5e2ff7ed2353c630816bbd60ebb124137a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/_constants.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/codata.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/codata.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..afbbffcd3e1762618fd54f6f3f34a7b63da8b809 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/codata.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/constants.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..613c4cab8270488d593e599aa70a01074085afb2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/__pycache__/constants.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a4ee48984a6a3701f25ef7ec9bc2358aaf2945f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_codata.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_codata.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c511aec2724157cdb1046003ecc7d66ad67e0a85 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_codata.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_constants.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_constants.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a91ec4c2917860d77c04bdbfa2e691d3fdc014a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/__pycache__/test_constants.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_codata.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_codata.py new file mode 100644 index 0000000000000000000000000000000000000000..ece796136a43ef6f3078272e4d0223263c9e31e8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_codata.py @@ -0,0 +1,78 @@ +from scipy.constants import find, value, c, speed_of_light, precision +from numpy.testing import assert_equal, assert_, assert_almost_equal +import scipy.constants._codata as _cd +from scipy import constants + + +def test_find(): + keys = find('weak mixing', disp=False) + assert_equal(keys, ['weak mixing angle']) + + keys = find('qwertyuiop', disp=False) + assert_equal(keys, []) + + keys = find('natural unit', disp=False) + assert_equal(keys, sorted(['natural unit of velocity', + 'natural unit of action', + 'natural unit of action in eV s', + 'natural unit of mass', + 'natural unit of energy', + 'natural unit of energy in MeV', + 'natural unit of momentum', + 'natural unit of momentum in MeV/c', + 'natural unit of length', + 'natural unit of time'])) + + +def test_basic_table_parse(): + c_s = 'speed of light in vacuum' + assert_equal(value(c_s), c) + assert_equal(value(c_s), speed_of_light) + + +def test_basic_lookup(): + assert_equal('{} {}'.format(int(_cd.value('speed of light in vacuum')), + _cd.unit('speed of light in vacuum')), + '299792458 m s^-1') + + +def test_find_all(): + assert_(len(find(disp=False)) > 300) + + +def test_find_single(): + assert_equal(find('Wien freq', disp=False)[0], + 'Wien frequency displacement law constant') + + +def test_2002_vs_2006(): + assert_almost_equal(value('magn. flux quantum'), + value('mag. flux quantum')) + + +def test_exact_values(): + # Check that updating stored values with exact ones worked. + exact = dict((k, v[0]) for k, v in _cd._physical_constants_2018.items()) + replace = _cd.exact2018(exact) + for key, val in replace.items(): + assert_equal(val, value(key)) + assert precision(key) == 0 + + +def test_gh11341(): + # gh-11341 noted that these three constants should exist (for backward + # compatibility) and should always have the same value: + a = constants.epsilon_0 + b = constants.physical_constants['electric constant'][0] + c = constants.physical_constants['vacuum electric permittivity'][0] + assert a == b == c + + +def test_gh14467(): + # gh-14467 noted that some physical constants in CODATA are rounded + # to only ten significant figures even though they are supposed to be + # exact. Check that (at least) the case mentioned in the issue is resolved. + res = constants.physical_constants['Boltzmann constant in eV/K'][0] + ref = (constants.physical_constants['Boltzmann constant'][0] + / constants.physical_constants['elementary charge'][0]) + assert res == ref diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_constants.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..672fac18884ddb012232d6e1a09a6d1ec8e277ef --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/constants/tests/test_constants.py @@ -0,0 +1,83 @@ +import pytest + +import scipy.constants as sc +from scipy._lib._array_api_no_0d import xp_assert_equal, xp_assert_close +from scipy._lib._array_api import make_xp_test_case + +lazy_xp_modules = [sc] + + +@make_xp_test_case(sc.convert_temperature) +class TestConvertTemperature: + def test_convert_temperature(self, xp): + xp_assert_equal(sc.convert_temperature(xp.asarray(32.), 'f', 'Celsius'), + xp.asarray(0.0)) + xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]), + 'celsius', 'Kelvin'), + xp.asarray([273.15, 273.15])) + xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]), 'kelvin', 'c'), + xp.asarray([-273.15, -273.15])) + xp_assert_equal(sc.convert_temperature(xp.asarray([32., 32.]), 'f', 'k'), + xp.asarray([273.15, 273.15])) + xp_assert_equal(sc.convert_temperature(xp.asarray([273.15, 273.15]), + 'kelvin', 'F'), + xp.asarray([32., 32.])) + xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]), 'C', 'fahrenheit'), + xp.asarray([32., 32.])) + xp_assert_close(sc.convert_temperature(xp.asarray([0., 0.], dtype=xp.float64), + 'c', 'r'), + xp.asarray([491.67, 491.67], dtype=xp.float64), + rtol=0., atol=1e-13) + xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 491.67], + dtype=xp.float64), + 'Rankine', 'C'), + xp.asarray([0., 0.], dtype=xp.float64), rtol=0., atol=1e-13) + xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 491.67], + dtype=xp.float64), + 'r', 'F'), + xp.asarray([32., 32.], dtype=xp.float64), rtol=0., atol=1e-13) + xp_assert_close(sc.convert_temperature(xp.asarray([32., 32.], dtype=xp.float64), + 'fahrenheit', 'R'), + xp.asarray([491.67, 491.67], dtype=xp.float64), + rtol=0., atol=1e-13) + xp_assert_close(sc.convert_temperature(xp.asarray([273.15, 273.15], + dtype=xp.float64), + 'K', 'R'), + xp.asarray([491.67, 491.67], dtype=xp.float64), + rtol=0., atol=1e-13) + xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 0.], + dtype=xp.float64), + 'rankine', 'kelvin'), + xp.asarray([273.15, 0.], dtype=xp.float64), rtol=0., atol=1e-13) + + def test_convert_temperature_array_like(self): + xp_assert_close(sc.convert_temperature([491.67, 0.], 'rankine', 'kelvin'), + [273.15, 0.], rtol=0., atol=1e-13) + + + def test_convert_temperature_errors(self): + with pytest.raises(NotImplementedError, match="old_scale="): + sc.convert_temperature(1, old_scale="cheddar", new_scale="kelvin") + with pytest.raises(NotImplementedError, match="new_scale="): + sc.convert_temperature(1, old_scale="kelvin", new_scale="brie") + + +@make_xp_test_case(sc.lambda2nu) +class TestLambdaToNu: + def test_lambda_to_nu(self, xp): + xp_assert_equal(sc.lambda2nu(xp.asarray([sc.speed_of_light, 1])), + xp.asarray([1, sc.speed_of_light])) + + + def test_lambda_to_nu_array_like(self): + xp_assert_close(sc.lambda2nu([sc.speed_of_light, 1]), [1, sc.speed_of_light]) + + +@make_xp_test_case(sc.nu2lambda) +class TestNuToLambda: + def test_nu_to_lambda(self, xp): + xp_assert_equal(sc.nu2lambda(xp.asarray([sc.speed_of_light, 1])), + xp.asarray([1, sc.speed_of_light])) + + def test_nu_to_lambda_array_like(self): + xp_assert_close(sc.nu2lambda([sc.speed_of_light, 1]), [1, sc.speed_of_light]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c9550c704d50100a8023a45990c4e20ede79d45 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_basic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_basic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5538071bf5fbcf595c9b1c963ce3d7b3a0f40c89 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_basic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d58a489073ce68dcf8e17c31cf6e4a560ba0749d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cholesky.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cholesky.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad905ed9e13f9307294752ece8c571e96adf3482 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cholesky.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cossin.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cossin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fa143a4a46e42520a55c398ba4b4cc3934f7ba6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_cossin.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_ldl.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_ldl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..798640922942feb01554b6465ac6dffc322a1d66 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_ldl.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_lu.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_lu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4dc0d24c034b8f18c28e367e457e0f310aff0b44 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_lu.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_polar.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_polar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6b05a50ef42aaa5c85e11681bb29080db03e816 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_polar.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qr.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ed655af1451a5ef4825e44853ea57ca3c59f5c7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qr.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qz.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qz.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65af3d776934474a81a0d5c8d391adb31bfd5791 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_qz.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_schur.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_schur.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..524c20e71364d06c8786e7cff7ba34678f95aaa8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_schur.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_svd.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_svd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a0d44403ab484a7dcbd9a40ce418538614a1b74 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_decomp_svd.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_expm_frechet.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_expm_frechet.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6d6f1c403a90611e58fb41e1244b189053573ef Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_expm_frechet.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1ab2d1e175a40a91199dd7d3494ccbc884c68b3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_inv_ssq.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_inv_ssq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4aa7265ccc5f204d9d399fa89debb6ce7036bc68 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_inv_ssq.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_sqrtm.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_sqrtm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12c1c33283a04e35d6eaf0367507c35ae4ec264e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_matfuncs_sqrtm.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_misc.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_misc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fedcab78e75a5cf297461c8a6e899d067b26b0fc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_misc.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_procrustes.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_procrustes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a6c9dc2829be76831c6e7bd08afca28c9f965d1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_procrustes.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_sketches.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_sketches.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e58294e8bf5774fd3347fef9e5a5db9cf3cc99b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_sketches.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_solvers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_solvers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26014e61b9cca77f6c8802ae5cd76a746f71a192 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_solvers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_special_matrices.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_special_matrices.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0108f69847aa39ff6cc213f4218b4db540509c26 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_special_matrices.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_testutils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_testutils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73136d73dbe85f1e80fc620a0072c8369a01d6b3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/_testutils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/basic.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/basic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab65014e75ffd6491008d041c3d1cd4272ab62c2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/basic.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/blas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/blas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5db111b89861ac6cd64929daf8f4d437f696b9b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/blas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f8ddf68348af7a874a2fe89678571a416980ed3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_cholesky.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_cholesky.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42d6580a2f38fe38b3fd1ed3fd161ba7fcdc9d9b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_cholesky.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_lu.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_lu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78a840888476336caf279d551843eee45234ab52 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_lu.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_qr.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_qr.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7b1edfeb30635769f217119301ce16edf93240b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_qr.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_schur.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_schur.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e63d7a46f6aef5340e58809273a9553abe73c5f0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_schur.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_svd.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_svd.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..761fab2d32d4918c6b03bf9ec81875efadff9be2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/decomp_svd.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/interpolative.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/interpolative.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c1790ab46440be8b7be55f7e95e72a191a5df2d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/interpolative.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/lapack.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/lapack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..792e8c4e027fa207fd0ef930303032945d0a83e4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/lapack.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/matfuncs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/matfuncs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fb7fdfc57b2bb4da266f0750da9fd2b94846675 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/matfuncs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/misc.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/misc.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..366aef205e2531960e6f009b77577ecd244e18ad Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/misc.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/special_matrices.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/special_matrices.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7df0d116c0a3e57c40a69c6c5f82f7a0fbd204e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/__pycache__/special_matrices.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65a7e3565b48e0ec1ea975d21215358a97ad93ed Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_batch.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_batch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd5c196b9a72fd80b1b4a65a6e6abfb23e2571f1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_batch.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_blas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_blas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16f3895cedd385610818b0f8d99ef8f85bcfeb80 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_blas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_blas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_blas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f4ba0c2d87d920b2ce4ac1e3c8bafb92295ded50 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_blas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_lapack.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_lapack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c79339dbcb262e55323b569e4879a1252ab86ce Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cython_lapack.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cythonized_array_utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cythonized_array_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b3d85ab00105e4d39f14796a19ce71edca69782 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_cythonized_array_utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cholesky.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cholesky.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d31cbf5d77330e59a796e931aabd81e50850cbc0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cholesky.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cossin.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cossin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48c17f14a72357eb9c5bd6503785300593fa37a6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_cossin.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_ldl.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_ldl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..837b9da44d202f9a15911c26395b486d67930afd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_ldl.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_lu.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_lu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64e263da565c673fd92f6986f0c8424ba2071bf9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_lu.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_polar.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_polar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db4ed0ddbb168660ff600600c5cb64953d7c2cdc Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_decomp_polar.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_extending.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_extending.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04ff649a457c3b21b6f4406b68c0de852794fbbd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_extending.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_fblas.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_fblas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07d6ea7d7dd3687149a3631b096c19306a3af527 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_fblas.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_interpolative.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_interpolative.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c87315a7bad69af0bd93f653b72d40199932481a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_interpolative.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matfuncs.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matfuncs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebf37b4b9dc54e372309828e9805bf65aa77e0d2 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matfuncs.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matmul_toeplitz.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matmul_toeplitz.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6bd3a773f58d4c8ae1d255e6340dd357138ece3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_matmul_toeplitz.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_procrustes.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_procrustes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3ab901be764f48cf8671edf2eb1af18d582a060 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_procrustes.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_sketches.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_sketches.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccd5c7f126386b2060db513a5667789069919025 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_sketches.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solve_toeplitz.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solve_toeplitz.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a666b36248041a4e1e88855994d46584b9dbb021 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solve_toeplitz.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solvers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solvers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71d04c435968dda2c84bdb6a3918c2bceb9c98f8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_solvers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_special_matrices.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_special_matrices.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55a1b13c3696915f75c50c2348ad66b9551ca008 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/__pycache__/test_special_matrices.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/extending.pyx b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/extending.pyx new file mode 100644 index 0000000000000000000000000000000000000000..3954d08791cceb3a2b66669fe3c0ec4180089208 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/extending.pyx @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +#cython: language_level=3 +#cython: boundscheck=False +#cython: wraparound=False + +cimport scipy.linalg +from scipy.linalg.cython_blas cimport cdotu +from scipy.linalg.cython_lapack cimport dgtsv + +cpdef tridiag(double[:] a, double[:] b, double[:] c, double[:] x): + """ Solve the system A y = x for y where A is the tridiagonal matrix with + subdiagonal 'a', diagonal 'b', and superdiagonal 'c'. """ + cdef int n=b.shape[0], nrhs=1, info + # Solution is written over the values in x. + dgtsv(&n, &nrhs, &a[0], &b[0], &c[0], &x[0], &n, &info) + +cpdef float complex complex_dot(float complex[:] cx, float complex[:] cy): + """ Take dot product of two complex vectors """ + cdef: + int n = cx.shape[0] + int incx = cx.strides[0] // sizeof(cx[0]) + int incy = cy.strides[0] // sizeof(cy[0]) + return cdotu(&n, &cx[0], &incx, &cy[0], &incy) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/meson.build b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..a7440fac7f2ecac0b6a43e73508c33829c60699f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/_cython_examples/meson.build @@ -0,0 +1,34 @@ +project('random-build-examples', 'c', 'cpp', 'cython') + +fs = import('fs') + +py3 = import('python').find_installation(pure: false) + +cy = meson.get_compiler('cython') + +if not cy.version().version_compare('>=3.0.8') + error('tests requires Cython >= 3.0.8') +endif + +cython_args = [] +if cy.version().version_compare('>=3.1.0') + cython_args += ['-Xfreethreading_compatible=True'] +endif + +py3.extension_module( + 'extending', + 'extending.pyx', + install: false, + cython_args: cython_args, + c_args: ['-DCYTHON_CCOMPLEX=0'] # see gh-18975 for why we need this +) + +extending_cpp = fs.copyfile('extending.pyx', 'extending_cpp.pyx') +py3.extension_module( + 'extending_cpp', + extending_cpp, + install: false, + override_options : ['cython_language=cpp'], + cython_args: cython_args, + cpp_args: ['-DCYTHON_CCOMPLEX=0'] # see gh-18975 for why we need this +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_basic.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_basic.py new file mode 100644 index 0000000000000000000000000000000000000000..862849173c2d16d9a99e28255f9ad70fcf600928 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_basic.py @@ -0,0 +1,2767 @@ +import os +import platform +import itertools +import warnings + +import numpy as np +from numpy import (arange, array, dot, zeros, identity, conjugate, transpose, + float32) + +from numpy.testing import (assert_equal, assert_almost_equal, assert_, + assert_array_almost_equal, assert_allclose, + assert_array_equal) +import pytest +from pytest import raises as assert_raises + +from scipy.linalg import (solve, inv, det, lstsq, pinv, pinvh, norm, + solve_banded, solveh_banded, solve_triangular, + solve_circulant, circulant, LinAlgError, block_diag, + matrix_balance, qr, LinAlgWarning) + +from scipy.linalg._testutils import assert_no_overwrite +from scipy._lib._testutils import check_free_memory, IS_MUSL +from scipy.linalg.blas import HAS_ILP64 +from scipy.conftest import skip_xp_invalid_arg + +REAL_DTYPES = (np.float32, np.float64, np.longdouble) +COMPLEX_DTYPES = (np.complex64, np.complex128, np.clongdouble) +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +parametrize_overwrite_arg = pytest.mark.parametrize( + "overwrite_kw", [{"overwrite_a": True}, {"overwrite_a": False}, {}], + ids=["True", "False", "None"] +) + + +parametrize_overwrite_b_arg = pytest.mark.parametrize( + "overwrite_b_kw", [{"overwrite_b": True}, {"overwrite_b": False}, {}], + ids=["True", "False", "None"] +) + + +def _eps_cast(dtyp): + """Get the epsilon for dtype, possibly downcast to BLAS types.""" + dt = dtyp + if dt == np.longdouble: + dt = np.float64 + elif dt == np.clongdouble: + dt = np.complex128 + return np.finfo(dt).eps + + +class TestSolveBanded: + + def test_real(self): + a = array([[1.0, 20, 0, 0], + [-30, 4, 6, 0], + [2, 1, 20, 2], + [0, -1, 7, 14]]) + ab = array([[0.0, 20, 6, 2], + [1, 4, 20, 14], + [-30, 1, 7, 0], + [2, -1, 0, 0]]) + l, u = 2, 1 + b4 = array([10.0, 0.0, 2.0, 14.0]) + b4by1 = b4.reshape(-1, 1) + b4by2 = array([[2, 1], + [-30, 4], + [2, 3], + [1, 3]]) + b4by4 = array([[1, 0, 0, 0], + [0, 0, 0, 1], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + for b in [b4, b4by1, b4by2, b4by4]: + x = solve_banded((l, u), ab, b) + assert_array_almost_equal(dot(a, x), b) + + def test_complex(self): + a = array([[1.0, 20, 0, 0], + [-30, 4, 6, 0], + [2j, 1, 20, 2j], + [0, -1, 7, 14]]) + ab = array([[0.0, 20, 6, 2j], + [1, 4, 20, 14], + [-30, 1, 7, 0], + [2j, -1, 0, 0]]) + l, u = 2, 1 + b4 = array([10.0, 0.0, 2.0, 14.0j]) + b4by1 = b4.reshape(-1, 1) + b4by2 = array([[2, 1], + [-30, 4], + [2, 3], + [1, 3]]) + b4by4 = array([[1, 0, 0, 0], + [0, 0, 0, 1j], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + for b in [b4, b4by1, b4by2, b4by4]: + x = solve_banded((l, u), ab, b) + assert_array_almost_equal(dot(a, x), b) + + def test_tridiag_real(self): + ab = array([[0.0, 20, 6, 2], + [1, 4, 20, 14], + [-30, 1, 7, 0]]) + a = np.diag(ab[0, 1:], 1) + np.diag(ab[1, :], 0) + np.diag( + ab[2, :-1], -1) + b4 = array([10.0, 0.0, 2.0, 14.0]) + b4by1 = b4.reshape(-1, 1) + b4by2 = array([[2, 1], + [-30, 4], + [2, 3], + [1, 3]]) + b4by4 = array([[1, 0, 0, 0], + [0, 0, 0, 1], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + for b in [b4, b4by1, b4by2, b4by4]: + x = solve_banded((1, 1), ab, b) + assert_array_almost_equal(dot(a, x), b) + + def test_tridiag_complex(self): + ab = array([[0.0, 20, 6, 2j], + [1, 4, 20, 14], + [-30, 1, 7, 0]]) + a = np.diag(ab[0, 1:], 1) + np.diag(ab[1, :], 0) + np.diag( + ab[2, :-1], -1) + b4 = array([10.0, 0.0, 2.0, 14.0j]) + b4by1 = b4.reshape(-1, 1) + b4by2 = array([[2, 1], + [-30, 4], + [2, 3], + [1, 3]]) + b4by4 = array([[1, 0, 0, 0], + [0, 0, 0, 1], + [0, 1, 0, 0], + [0, 1, 0, 0]]) + for b in [b4, b4by1, b4by2, b4by4]: + x = solve_banded((1, 1), ab, b) + assert_array_almost_equal(dot(a, x), b) + + def test_check_finite(self): + a = array([[1.0, 20, 0, 0], + [-30, 4, 6, 0], + [2, 1, 20, 2], + [0, -1, 7, 14]]) + ab = array([[0.0, 20, 6, 2], + [1, 4, 20, 14], + [-30, 1, 7, 0], + [2, -1, 0, 0]]) + l, u = 2, 1 + b4 = array([10.0, 0.0, 2.0, 14.0]) + x = solve_banded((l, u), ab, b4, check_finite=False) + assert_array_almost_equal(dot(a, x), b4) + + def test_bad_shape(self): + ab = array([[0.0, 20, 6, 2], + [1, 4, 20, 14], + [-30, 1, 7, 0], + [2, -1, 0, 0]]) + l, u = 2, 1 + bad = array([1.0, 2.0, 3.0, 4.0]).reshape(-1, 4) + assert_raises(ValueError, solve_banded, (l, u), ab, bad) + assert_raises(ValueError, solve_banded, (l, u), ab, [1.0, 2.0]) + + # Values of (l,u) are not compatible with ab. + assert_raises(ValueError, solve_banded, (1, 1), ab, [1.0, 2.0]) + + def test_1x1(self): + # gh-8906 noted that the case of A@x = b with 1x1 A was handled + # incorrectly; check that this is resolved. Typical case: + # nupper == nlower == 0 + # A = [[2]] + b = array([[1., 2., 3.]]) + ref = array([[0.5, 1.0, 1.5]]) + x = solve_banded((0, 0), [[2]], b) + assert_allclose(x, ref, rtol=1e-15) + + # However, the user *can* represent the same system with garbage rows + # in `ab`. Test the case with `nupper == 1, nlower == 1`. + x = solve_banded((1, 1), [[0], [2], [0]], b) + assert_allclose(x, ref, rtol=1e-15) + assert_equal(x.dtype, np.dtype('f8')) + assert_array_equal(b, [[1.0, 2.0, 3.0]]) + + def test_native_list_arguments(self): + a = [[1.0, 20, 0, 0], + [-30, 4, 6, 0], + [2, 1, 20, 2], + [0, -1, 7, 14]] + ab = [[0.0, 20, 6, 2], + [1, 4, 20, 14], + [-30, 1, 7, 0], + [2, -1, 0, 0]] + l, u = 2, 1 + b = [10.0, 0.0, 2.0, 14.0] + x = solve_banded((l, u), ab, b) + assert_array_almost_equal(dot(a, x), b) + + @pytest.mark.parametrize('dt_ab', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt_ab, dt_b): + # ab contains one empty row corresponding to the diagonal + ab = np.array([[]], dtype=dt_ab) + b = np.array([], dtype=dt_b) + x = solve_banded((0, 0), ab, b) + + assert x.shape == (0,) + assert x.dtype == solve(np.eye(1, dtype=dt_ab), np.ones(1, dtype=dt_b)).dtype + + b = np.empty((0, 0), dtype=dt_b) + x = solve_banded((0, 0), ab, b) + + assert x.shape == (0, 0) + assert x.dtype == solve(np.eye(1, dtype=dt_ab), np.ones(1, dtype=dt_b)).dtype + + +class TestSolveHBanded: + + def test_01_upper(self): + # Solve + # [ 4 1 2 0] [1] + # [ 1 4 1 2] X = [4] + # [ 2 1 4 1] [1] + # [ 0 2 1 4] [2] + # with the RHS as a 1D array. + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]]) + b = array([1.0, 4.0, 1.0, 2.0]) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 0.0, 0.0]) + + def test_02_upper(self): + # Solve + # [ 4 1 2 0] [1 6] + # [ 1 4 1 2] X = [4 2] + # [ 2 1 4 1] [1 6] + # [ 0 2 1 4] [2 1] + # + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]]) + b = array([[1.0, 6.0], + [4.0, 2.0], + [1.0, 6.0], + [2.0, 1.0]]) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0], + [0.0, 0.0]]) + assert_array_almost_equal(x, expected) + + def test_03_upper(self): + # Solve + # [ 4 1 2 0] [1] + # [ 1 4 1 2] X = [4] + # [ 2 1 4 1] [1] + # [ 0 2 1 4] [2] + # with the RHS as a 2D array with shape (3,1). + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]]) + b = array([1.0, 4.0, 1.0, 2.0]).reshape(-1, 1) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, array([0., 1., 0., 0.]).reshape(-1, 1)) + + def test_01_lower(self): + # Solve + # [ 4 1 2 0] [1] + # [ 1 4 1 2] X = [4] + # [ 2 1 4 1] [1] + # [ 0 2 1 4] [2] + # + ab = array([[4.0, 4.0, 4.0, 4.0], + [1.0, 1.0, 1.0, -99], + [2.0, 2.0, 0.0, 0.0]]) + b = array([1.0, 4.0, 1.0, 2.0]) + x = solveh_banded(ab, b, lower=True) + assert_array_almost_equal(x, [0.0, 1.0, 0.0, 0.0]) + + def test_02_lower(self): + # Solve + # [ 4 1 2 0] [1 6] + # [ 1 4 1 2] X = [4 2] + # [ 2 1 4 1] [1 6] + # [ 0 2 1 4] [2 1] + # + ab = array([[4.0, 4.0, 4.0, 4.0], + [1.0, 1.0, 1.0, -99], + [2.0, 2.0, 0.0, 0.0]]) + b = array([[1.0, 6.0], + [4.0, 2.0], + [1.0, 6.0], + [2.0, 1.0]]) + x = solveh_banded(ab, b, lower=True) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0], + [0.0, 0.0]]) + assert_array_almost_equal(x, expected) + + def test_01_float32(self): + # Solve + # [ 4 1 2 0] [1] + # [ 1 4 1 2] X = [4] + # [ 2 1 4 1] [1] + # [ 0 2 1 4] [2] + # + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]], dtype=float32) + b = array([1.0, 4.0, 1.0, 2.0], dtype=float32) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 0.0, 0.0]) + + def test_02_float32(self): + # Solve + # [ 4 1 2 0] [1 6] + # [ 1 4 1 2] X = [4 2] + # [ 2 1 4 1] [1 6] + # [ 0 2 1 4] [2 1] + # + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]], dtype=float32) + b = array([[1.0, 6.0], + [4.0, 2.0], + [1.0, 6.0], + [2.0, 1.0]], dtype=float32) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0], + [0.0, 0.0]]) + assert_array_almost_equal(x, expected) + + def test_01_complex(self): + # Solve + # [ 4 -j 2 0] [2-j] + # [ j 4 -j 2] X = [4-j] + # [ 2 j 4 -j] [4+j] + # [ 0 2 j 4] [2+j] + # + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, -1.0j, -1.0j, -1.0j], + [4.0, 4.0, 4.0, 4.0]]) + b = array([2-1.0j, 4.0-1j, 4+1j, 2+1j]) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 1.0, 0.0]) + + def test_02_complex(self): + # Solve + # [ 4 -j 2 0] [2-j 2+4j] + # [ j 4 -j 2] X = [4-j -1-j] + # [ 2 j 4 -j] [4+j 4+2j] + # [ 0 2 j 4] [2+j j] + # + ab = array([[0.0, 0.0, 2.0, 2.0], + [-99, -1.0j, -1.0j, -1.0j], + [4.0, 4.0, 4.0, 4.0]]) + b = array([[2-1j, 2+4j], + [4.0-1j, -1-1j], + [4.0+1j, 4+2j], + [2+1j, 1j]]) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0j], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 0.0]]) + assert_array_almost_equal(x, expected) + + def test_tridiag_01_upper(self): + # Solve + # [ 4 1 0] [1] + # [ 1 4 1] X = [4] + # [ 0 1 4] [1] + # with the RHS as a 1D array. + ab = array([[-99, 1.0, 1.0], [4.0, 4.0, 4.0]]) + b = array([1.0, 4.0, 1.0]) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 0.0]) + + def test_tridiag_02_upper(self): + # Solve + # [ 4 1 0] [1 4] + # [ 1 4 1] X = [4 2] + # [ 0 1 4] [1 4] + # + ab = array([[-99, 1.0, 1.0], + [4.0, 4.0, 4.0]]) + b = array([[1.0, 4.0], + [4.0, 2.0], + [1.0, 4.0]]) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0]]) + assert_array_almost_equal(x, expected) + + def test_tridiag_03_upper(self): + # Solve + # [ 4 1 0] [1] + # [ 1 4 1] X = [4] + # [ 0 1 4] [1] + # with the RHS as a 2D array with shape (3,1). + ab = array([[-99, 1.0, 1.0], [4.0, 4.0, 4.0]]) + b = array([1.0, 4.0, 1.0]).reshape(-1, 1) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, array([0.0, 1.0, 0.0]).reshape(-1, 1)) + + def test_tridiag_01_lower(self): + # Solve + # [ 4 1 0] [1] + # [ 1 4 1] X = [4] + # [ 0 1 4] [1] + # + ab = array([[4.0, 4.0, 4.0], + [1.0, 1.0, -99]]) + b = array([1.0, 4.0, 1.0]) + x = solveh_banded(ab, b, lower=True) + assert_array_almost_equal(x, [0.0, 1.0, 0.0]) + + def test_tridiag_02_lower(self): + # Solve + # [ 4 1 0] [1 4] + # [ 1 4 1] X = [4 2] + # [ 0 1 4] [1 4] + # + ab = array([[4.0, 4.0, 4.0], + [1.0, 1.0, -99]]) + b = array([[1.0, 4.0], + [4.0, 2.0], + [1.0, 4.0]]) + x = solveh_banded(ab, b, lower=True) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0]]) + assert_array_almost_equal(x, expected) + + def test_tridiag_01_float32(self): + # Solve + # [ 4 1 0] [1] + # [ 1 4 1] X = [4] + # [ 0 1 4] [1] + # + ab = array([[-99, 1.0, 1.0], [4.0, 4.0, 4.0]], dtype=float32) + b = array([1.0, 4.0, 1.0], dtype=float32) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 0.0]) + + def test_tridiag_02_float32(self): + # Solve + # [ 4 1 0] [1 4] + # [ 1 4 1] X = [4 2] + # [ 0 1 4] [1 4] + # + ab = array([[-99, 1.0, 1.0], + [4.0, 4.0, 4.0]], dtype=float32) + b = array([[1.0, 4.0], + [4.0, 2.0], + [1.0, 4.0]], dtype=float32) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0], + [1.0, 0.0], + [0.0, 1.0]]) + assert_array_almost_equal(x, expected) + + def test_tridiag_01_complex(self): + # Solve + # [ 4 -j 0] [ -j] + # [ j 4 -j] X = [4-j] + # [ 0 j 4] [4+j] + # + ab = array([[-99, -1.0j, -1.0j], [4.0, 4.0, 4.0]]) + b = array([-1.0j, 4.0-1j, 4+1j]) + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 1.0]) + + def test_tridiag_02_complex(self): + # Solve + # [ 4 -j 0] [ -j 4j] + # [ j 4 -j] X = [4-j -1-j] + # [ 0 j 4] [4+j 4 ] + # + ab = array([[-99, -1.0j, -1.0j], + [4.0, 4.0, 4.0]]) + b = array([[-1j, 4.0j], + [4.0-1j, -1.0-1j], + [4.0+1j, 4.0]]) + x = solveh_banded(ab, b) + expected = array([[0.0, 1.0j], + [1.0, 0.0], + [1.0, 1.0]]) + assert_array_almost_equal(x, expected) + + def test_check_finite(self): + # Solve + # [ 4 1 0] [1] + # [ 1 4 1] X = [4] + # [ 0 1 4] [1] + # with the RHS as a 1D array. + ab = array([[-99, 1.0, 1.0], [4.0, 4.0, 4.0]]) + b = array([1.0, 4.0, 1.0]) + x = solveh_banded(ab, b, check_finite=False) + assert_array_almost_equal(x, [0.0, 1.0, 0.0]) + + def test_bad_shapes(self): + ab = array([[-99, 1.0, 1.0], + [4.0, 4.0, 4.0]]) + b = array([[1.0, 4.0], + [4.0, 2.0]]) + assert_raises(ValueError, solveh_banded, ab, b) + assert_raises(ValueError, solveh_banded, ab, [1.0, 2.0]) + assert_raises(ValueError, solveh_banded, ab, [1.0]) + + def test_1x1(self): + x = solveh_banded([[1]], [[1, 2, 3]]) + assert_array_equal(x, [[1.0, 2.0, 3.0]]) + assert_equal(x.dtype, np.dtype('f8')) + + def test_native_list_arguments(self): + # Same as test_01_upper, using python's native list. + ab = [[0.0, 0.0, 2.0, 2.0], + [-99, 1.0, 1.0, 1.0], + [4.0, 4.0, 4.0, 4.0]] + b = [1.0, 4.0, 1.0, 2.0] + x = solveh_banded(ab, b) + assert_array_almost_equal(x, [0.0, 1.0, 0.0, 0.0]) + + @pytest.mark.parametrize('dt_ab', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt_ab, dt_b): + # ab contains one empty row corresponding to the diagonal + ab = np.array([[]], dtype=dt_ab) + b = np.array([], dtype=dt_b) + x = solveh_banded(ab, b) + + assert x.shape == (0,) + assert x.dtype == solve(np.eye(1, dtype=dt_ab), np.ones(1, dtype=dt_b)).dtype + + b = np.empty((0, 0), dtype=dt_b) + x = solveh_banded(ab, b) + + assert x.shape == (0, 0) + assert x.dtype == solve(np.eye(1, dtype=dt_ab), np.ones(1, dtype=dt_b)).dtype + + +class TestSolve: + def test_20Feb04_bug(self): + a = [[1, 1], [1.0, 0]] # ok + x0 = solve(a, [1, 0j]) + assert_array_almost_equal(dot(a, x0), [1, 0]) + + # gives failure with clapack.zgesv(..,rowmajor=0) + a = [[1, 1], [1.2, 0]] + b = [1, 0j] + x0 = solve(a, b) + assert_array_almost_equal(dot(a, x0), [1, 0]) + + def test_simple(self): + a = [[1, 20], [-30, 4]] + for b in ([[1, 0], [0, 1]], + [1, 0], + [[2, 1], [-30, 4]] + ): + x = solve(a, b) + assert_array_almost_equal(dot(a, x), b) + + def test_simple_complex(self): + a = array([[5, 2], [2j, 4]], 'D') + for b in ([1j, 0], + [[1j, 1j], [0, 2]], + [1, 0j], + array([1, 0], 'D'), + ): + x = solve(a, b) + assert_array_almost_equal(dot(a, x), b) + + def test_simple_pos(self): + a = [[2, 3], [3, 5]] + for lower in [0, 1]: + for b in ([[1, 0], [0, 1]], + [1, 0] + ): + x = solve(a, b, assume_a='pos', lower=lower) + assert_array_almost_equal(dot(a, x), b) + + def test_simple_pos_complexb(self): + a = [[5, 2], [2, 4]] + for b in ([1j, 0], + [[1j, 1j], [0, 2]], + ): + x = solve(a, b, assume_a='pos') + assert_array_almost_equal(dot(a, x), b) + + def test_simple_sym(self): + a = [[2, 3], [3, -5]] + for lower in [0, 1]: + for b in ([[1, 0], [0, 1]], + [1, 0] + ): + x = solve(a, b, assume_a='sym', lower=lower) + assert_array_almost_equal(dot(a, x), b) + + def test_simple_sym_complexb(self): + a = [[5, 2], [2, -4]] + for b in ([1j, 0], + [[1j, 1j], [0, 2]] + ): + x = solve(a, b, assume_a='sym') + assert_array_almost_equal(dot(a, x), b) + + def test_simple_sym_complex(self): + a = [[5, 2+1j], [2+1j, -4]] + for b in ([1j, 0], + [1, 0], + [[1j, 1j], [0, 2]] + ): + x = solve(a, b, assume_a='sym') + assert_array_almost_equal(dot(a, x), b) + + def test_simple_her_actuallysym(self): + a = [[2, 3], [3, -5]] + for lower in [0, 1]: + for b in ([[1, 0], [0, 1]], + [1, 0], + [1j, 0], + ): + x = solve(a, b, assume_a='her', lower=lower) + assert_array_almost_equal(dot(a, x), b) + + def test_simple_her(self): + a = [[5, 2+1j], [2-1j, -4]] + for b in ([1j, 0], + [1, 0], + [[1j, 1j], [0, 2]] + ): + x = solve(a, b, assume_a='her') + assert_array_almost_equal(dot(a, x), b) + + def test_nils_20Feb04(self): + rng = np.random.default_rng(1234) + n = 2 + A = rng.random([n, n])+rng.random([n, n])*1j + X = zeros((n, n), 'D') + Ainv = inv(A) + R = identity(n)+identity(n)*0j + for i in arange(0, n): + r = R[:, i] + X[:, i] = solve(A, r) + assert_array_almost_equal(X, Ainv) + + def test_random(self): + rng = np.random.default_rng(1234) + n = 20 + a = rng.random([n, n]) + for i in range(n): + a[i, i] = 20*(.1+a[i, i]) + for i in range(4): + b = rng.random([n, 3]) + x = solve(a, b) + assert_array_almost_equal(dot(a, x), b) + + def test_random_complex(self): + rng = np.random.default_rng(1234) + n = 20 + a = rng.random([n, n]) + 1j * rng.random([n, n]) + for i in range(n): + a[i, i] = 20*(.1+a[i, i]) + for i in range(2): + b = rng.random([n, 3]) + x = solve(a, b) + assert_array_almost_equal(dot(a, x), b) + + def test_random_sym(self): + rng = np.random.default_rng(1234) + n = 20 + a = rng.random([n, n]) + for i in range(n): + a[i, i] = abs(20*(.1+a[i, i])) + for j in range(i): + a[i, j] = a[j, i] + for i in range(4): + b = rng.random([n]) + x = solve(a, b, assume_a="pos") + assert_array_almost_equal(dot(a, x), b) + + def test_random_sym_complex(self): + rng = np.random.default_rng(1234) + n = 20 + a = rng.random([n, n]) + a = a + 1j*rng.random([n, n]) + for i in range(n): + a[i, i] = abs(20*(.1+a[i, i])) + for j in range(i): + a[i, j] = conjugate(a[j, i]) + b = rng.random([n])+2j*rng.random([n]) + for i in range(2): + x = solve(a, b, assume_a="pos") + assert_array_almost_equal(dot(a, x), b) + + def test_check_finite(self): + a = [[1, 20], [-30, 4]] + for b in ([[1, 0], [0, 1]], [1, 0], + [[2, 1], [-30, 4]]): + x = solve(a, b, check_finite=False) + assert_array_almost_equal(dot(a, x), b) + + def test_scalar_a_and_1D_b(self): + a = 1 + b = [1, 2, 3] + x = solve(a, b) + assert_array_almost_equal(x.ravel(), b) + assert_(x.shape == (3,), 'Scalar_a_1D_b test returned wrong shape') + + def test_simple2(self): + a = np.array([[1.80, 2.88, 2.05, -0.89], + [525.00, -295.00, -95.00, -380.00], + [1.58, -2.69, -2.90, -1.04], + [-1.11, -0.66, -0.59, 0.80]]) + + b = np.array([[9.52, 18.47], + [2435.00, 225.00], + [0.77, -13.28], + [-6.22, -6.21]]) + + x = solve(a, b) + assert_array_almost_equal(x, np.array([[1., -1, 3, -5], + [3, 2, 4, 1]]).T) + + def test_simple_complex2(self): + a = np.array([[-1.34+2.55j, 0.28+3.17j, -6.39-2.20j, 0.72-0.92j], + [-1.70-14.10j, 33.10-1.50j, -1.50+13.40j, 12.90+13.80j], + [-3.29-2.39j, -1.91+4.42j, -0.14-1.35j, 1.72+1.35j], + [2.41+0.39j, -0.56+1.47j, -0.83-0.69j, -1.96+0.67j]]) + + b = np.array([[26.26+51.78j, 31.32-6.70j], + [64.30-86.80j, 158.60-14.20j], + [-5.75+25.31j, -2.15+30.19j], + [1.16+2.57j, -2.56+7.55j]]) + + x = solve(a, b) + assert_array_almost_equal(x, np. array([[1+1.j, -1-2.j], + [2-3.j, 5+1.j], + [-4-5.j, -3+4.j], + [6.j, 2-3.j]])) + + @pytest.mark.parametrize("assume_a", ['her', 'sym']) + def test_symmetric_hermitian(self, assume_a): + # An upper triangular matrix will be used for symmetric/hermitian matrix a + a = np.array([[-1.84, 0.11-0.11j, -1.78-1.18j, 3.91-1.50j], + [0, -4.63, -1.84+0.03j, 2.21+0.21j], + [0, 0, -8.87, 1.58-0.90j], + [0, 0, 0, -1.36]]) + b = np.array([[2.98-10.18j, 28.68-39.89j], + [-9.58+3.88j, -24.79-8.40j], + [-0.77-16.05j, 4.23-70.02j], + [7.79+5.48j, -35.39+18.01j]]) + + a2 = a.T if assume_a == 'sym' else a.conj().T # for testing `lower` + a3 = a + a2 # for reference solution + a3[np.arange(4), np.arange(4)] = np.diag(a) + ref = solve(a3, b, assume_a='general') + + x = solve(a, b, assume_a=assume_a) + assert_array_almost_equal(x, ref) + # Also transpose(/conjugate) `a` and test for lower triangular data + # This also tests gh-22265 resolution; otherwise, a warning would be emitted + x = solve(a2, b, assume_a=assume_a, lower=True) + assert_array_almost_equal(x, ref) + + def test_pos_and_sym(self): + A = np.arange(1, 10).reshape(3, 3) + x = solve(np.tril(A)/9, np.ones(3), assume_a='pos') + assert_array_almost_equal(x, [9., 1.8, 1.]) + x = solve(np.tril(A)/9, np.ones(3), assume_a='sym') + assert_array_almost_equal(x, [9., 1.8, 1.]) + + def test_singularity(self): + a = np.array([[1, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 1, 1, 0, 0, 0, 1, 0, 1], + [0, 1, 1, 0, 0, 0, 1, 0, 1], + [1, 0, 1, 1, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 1, 1, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1]]) + b = np.arange(9)[:, None] + assert_raises(LinAlgError, solve, a, b) + + @pytest.mark.parametrize('structure', + ('diagonal', 'tridiagonal', 'lower triangular', + 'upper triangular', 'symmetric', 'hermitian', + 'positive definite', 'general', 'banded', None)) + def test_ill_condition_warning(self, structure): + rng = np.random.default_rng(234859349452) + n = 10 + d = np.logspace(0, 50, n) + A = np.diag(d) + b = rng.random(size=n) + message = "(Ill-conditioned matrix|An ill-conditioned matrix)" + with pytest.warns(LinAlgWarning, match=message): + solve(A, b, assume_a=structure) + + @pytest.mark.parametrize('structure', + ('diagonal', 'tridiagonal', 'lower triangular', + 'upper triangular', 'symmetric', 'hermitian', + 'positive definite', 'general', None)) + def test_exactly_singular_gh22263(self, structure): + n = 10 + A = np.zeros((n, n)) + b = np.ones(n) + with (pytest.raises(LinAlgError, match="singular"), np.errstate(all='ignore')): + solve(A, b, assume_a=structure) + + @pytest.mark.parametrize('b', [0, 1, [0, 1]]) + def test_singular_scalar(self, b): + # regression test for gh-24355: scalar a=0 is singular + # thus should raise the same error + + with pytest.raises(LinAlgError): + a = np.zeros((1, 1)) + solve(a, b) + + with pytest.raises(LinAlgError): + solve(0, b) + + with pytest.raises(LinAlgError): + solve([[0]], b) + + def test_multiple_rhs(self): + a = np.eye(2) + rng = np.random.default_rng(1234) + b = rng.random((2, 12)) + x = solve(a, b) + assert_array_almost_equal(x, b) + + def test_transposed_keyword(self): + A = np.arange(9).reshape(3, 3) + 1 + x = solve(np.tril(A)/9, np.ones(3), transposed=True) + assert_array_almost_equal(x, [1.2, 0.2, 1]) + x = solve(np.tril(A)/9, np.ones(3), transposed=False) + assert_array_almost_equal(x, [9, -5.4, -1.2]) + + @pytest.mark.skip(reason="1. why? 2. deprecate the kwarg altogether?") + def test_transposed_notimplemented(self): + a = np.eye(3).astype(complex) + with assert_raises(NotImplementedError): + solve(a, a, transposed=True) + + def test_nonsquare_a(self): + assert_raises(ValueError, solve, [1, 2], 1) + + def test_size_mismatch_with_1D_b(self): + assert_array_almost_equal(solve(np.eye(3), np.ones(3)), np.ones(3)) + assert_raises(ValueError, solve, np.eye(3), np.ones(4)) + + def test_assume_a_keyword(self): + assert_raises(ValueError, solve, 1, 1, assume_a='zxcv') + + @pytest.mark.parametrize("size", [10, 100]) + @pytest.mark.parametrize("assume_a", ['gen', 'sym', 'pos', 'her', 'tridiagonal']) + @pytest.mark.parametrize( + "dtype", [np.float32, np.float64, np.complex64, np.complex128] + ) + def test_all_type_size_routine_combinations(self, size, dtype, assume_a): + rng = np.random.default_rng(1234) + is_complex = dtype in (np.complex64, np.complex128) + + a = rng.standard_normal((size, size)).astype(dtype) + b = rng.standard_normal(size).astype(dtype) + if is_complex: + a += (1j*rng.standard_normal((size, size))).astype(dtype) + + if assume_a == 'sym': # Can still be complex but only symmetric + a = a + a.T + elif assume_a == 'her': # Handle hermitian matrices here instead + a = a + a.T.conj() + elif assume_a == 'pos': + a = a.T.conj() @ a + 0.1*np.eye(size) + elif assume_a == 'tridiagonal': + a = (np.diag(np.diag(a)) + + np.diag(np.diag(a, 1), 1) + + np.diag(np.diag(a, -1), -1) + ) + + tol = 1e-12 if dtype in (np.float64, np.complex128) else 1e-6 + + if assume_a in ['gen', 'sym', 'her']: + # We revert the tolerance from before + # 4b4a6e7c34fa4060533db38f9a819b98fa81476c + if dtype in (np.float32, np.complex64): + tol *= 10 + + x = solve(a, b, assume_a=assume_a) + assert_allclose(a @ x, b, atol=tol * size, rtol=tol * size) + + if assume_a == 'sym' and not is_complex: + x = solve(a, b, assume_a=assume_a, transposed=True) + assert_allclose(a @ x, b, atol=tol * size, rtol=tol * size) + + @pytest.mark.parametrize('dt_a', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt_a, dt_b): + a = np.empty((0, 0), dtype=dt_a) + b = np.empty(0, dtype=dt_b) + x = solve(a, b) + + assert x.size == 0 + dt_nonempty = solve(np.eye(2, dtype=dt_a), np.ones(2, dtype=dt_b)).dtype + assert x.dtype == dt_nonempty + assert x.shape == np.linalg.solve(a, b).shape + + a = np.ones((3, 0, 2, 2), dtype=dt_a) + b = np.ones((2, 4), dtype=dt_b) + x = solve(a, b) + assert x.shape == (3, 0, 2, 4) + assert x.dtype == dt_nonempty + + def test_empty_rhs(self): + a = np.eye(2) + b = [[], []] + x = solve(a, b) + assert_(x.size == 0, 'Returned array is not empty') + assert_(x.shape == (2, 0), 'Returned empty array shape is wrong') + + @pytest.mark.parametrize('dtype', [np.float64, np.complex128]) + @pytest.mark.parametrize('assume_a', ['diagonal', 'tridiagonal', 'banded', + 'lower triangular', 'upper triangular', + 'pos', 'positive definite', + 'symmetric', 'hermitian', 'banded', + 'general', 'sym', 'her', 'gen']) + @pytest.mark.parametrize('nrhs', [(), (5,)]) + @pytest.mark.parametrize('transposed', [True, False]) + @pytest.mark.parametrize('overwrite', [True, False]) + @pytest.mark.parametrize('fortran', [True, False]) + def test_structure_detection(self, dtype, assume_a, nrhs, transposed, + overwrite, fortran): + rng = np.random.default_rng(982345982439826) + n = 5 if not assume_a == 'banded' else 20 + b = rng.random(size=(n,) + nrhs) + A = rng.random(size=(n, n)) + + if np.issubdtype(dtype, np.complexfloating): + b = b + rng.random(size=(n,) + nrhs) * 1j + A = A + rng.random(size=(n, n)) * 1j + + if assume_a == 'diagonal': + A = np.diag(np.diag(A)) + elif assume_a == 'lower triangular': + A = np.tril(A) + elif assume_a == 'upper triangular': + A = np.triu(A) + elif assume_a == 'tridiagonal': + A = (np.diag(np.diag(A)) + + np.diag(np.diag(A, -1), -1) + + np.diag(np.diag(A, 1), 1)) + elif assume_a == 'banded': + A = np.triu(np.tril(A, 2), -1) + elif assume_a in {'symmetric', 'sym'}: + A = A + A.T + elif assume_a in {'hermitian', 'her'}: + A = A + A.conj().T + elif assume_a in {'positive definite', 'pos'}: + A = A @ A.T.conj() + + if fortran: + A = np.asfortranarray(A) + + A_copy = A.copy(order='A') + b_copy = b.copy() + + if np.issubdtype(dtype, np.complexfloating) and transposed: + message = "scipy.linalg.solve can currently..." + with pytest.raises(NotImplementedError, match=message): + solve(A, b, overwrite_a=overwrite, overwrite_b=overwrite, + transposed=transposed) + return + + res = solve(A, b, overwrite_a=overwrite, overwrite_b=overwrite, + transposed=transposed, assume_a=assume_a) + + # Check that solution this solution is *correct* + ref = np.linalg.solve(A_copy.T if transposed else A_copy, b_copy) + assert_allclose(res, ref) + + # Check that `solve` correctly identifies the structure and returns + # *exactly* the same solution whether `assume_a` is specified or not + if assume_a != 'banded': # structure detection removed for banded + assert_allclose( + solve(A_copy, b_copy, transposed=transposed), res, atol=1e-15 + ) + + # Check that overwrite was respected + if not overwrite: + assert_equal(A, A_copy) + assert_equal(b, b_copy) + + @pytest.mark.skipif( + np.__version__ < '2', reason="solve chokes on b.ndim == 1 in numpy < 2" + ) + @pytest.mark.parametrize( + "assume_a", + [ + None, "diagonal", "general", "upper triangular", "lower triangular", "pos", + ] + ) + def test_vs_np_solve(self, assume_a): + e = np.eye(2) + a = np.arange(1, 4*3*2 + 1).reshape((4, 3, 2, 1, 1)) * e + + b = np.ones(2) + assert_allclose(solve(a, b, assume_a=assume_a), np.linalg.solve(a, b)) + + b = np.ones((2, 1)) + assert_allclose(solve(a, b, assume_a=assume_a), np.linalg.solve(a, b)) + + b = np.ones((2, 2)) * [1, 2] + assert_allclose(solve(a, b, assume_a=assume_a), np.linalg.solve(a, b)) + + def test_pos_lower(self): + # regression test for + # https://github.com/scipy/scipy/pull/23071#issuecomment-3085826112 + rng = np.random.default_rng(0) + a = rng.normal(size=(4, 4)) + a = np.tril(np.matmul(a, np.conj(a.T))) # lower triangle of hermitian array + b = rng.normal(size=(4, 2)) + out = solve(a, b, assume_a='pos', lower=True) + + aa = a + a.T - np.diag(np.diag(a)) # the full hermitian array + result_np = np.linalg.solve(aa, b) + assert_allclose(out, result_np, atol=1e-15) + + # repeat with uplo='U' + out = solve(a.T, b, assume_a='pos', lower=False) + assert_allclose(out, result_np, atol=1e-15) + + def test_pos_fails_sym_complex(self): + # regression test for the `solve` analog of gh-24359 + # the matrix is 1) symmetric not hermitian, and 2) not positive definite: + a = np.asarray([[ 182.56985285-64.28859483j, -177.24879835+11.0780499j ], + [-177.24879835+11.0780499j , 177.24879835-11.0780499j ]]) + b = np.eye(2) + + ainv = solve(a, b) + assert_allclose(ainv @ a, np.eye(2), atol=1e-14) + + ainv_sym = solve(a, b, assume_a="sym") + assert_allclose(ainv_sym, ainv, atol=1e-14) + + # Specifying assume_a="pos" disables the structure detection, and directly + # calls LAPACK routines zportf and zpotri. + # Since zportf(a) does not error out, neither does solve. + ainv_chol = solve(a, b, assume_a="pos") + assert not np.allclose(ainv, ainv_chol, atol=1e-14) + + # Setting assume_a="pos" with a non-pos def matrix returned nonsense. + # This is at least consistent with inv. + ainv_inv = inv(a, assume_a="pos") + assert_allclose(ainv_chol, ainv_inv, atol=1e-14) + + def test_readonly(self): + a = np.eye(3) + a.flags.writeable = False + b = np.ones(3) + x = solve(a, b) + assert_allclose(x, b, atol=1e-14) + + @parametrize_overwrite_arg + def test_batch_negative_stride(self, overwrite_kw): + a = np.arange(3*8).reshape(2, 3, 2, 2) + a = a[:, ::-1, :, :] + b = np.ones(2) + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + # use b with a negative stride now + b = np.ones((2, 4))[:, ::-1] + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + (b.shape[-1],) + assert_allclose(a @ x - b, 0, atol=1e-14) + + @parametrize_overwrite_arg + def test_core_negative_stride(self, overwrite_kw): + a = np.arange(3*8).reshape(2, 3, 2, 2) + a = a[:, :, ::-1, :] + b = np.ones(2) + x = solve(a, b, **overwrite_kw) + + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + # use b with a negative stride now + b = np.ones((2, 4))[::-1, :] + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + (b.shape[-1],) + assert_allclose(a @ x - b, 0, atol=1e-14) + + @parametrize_overwrite_arg + def test_core_non_contiguous(self, overwrite_kw): + a = np.arange(3*8*2).reshape(2, 3, 2, 4) + a = a[..., ::2] + b = np.ones(2) + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + # use strided b now + b = np.ones(4)[::2] + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + @parametrize_overwrite_arg + def test_batch_non_contiguous(self, overwrite_kw): + a = np.arange(3*8*2).reshape(2, 6, 2, 2) + a = a[:, ::2, ...] + b = np.ones(2) + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + # use strided b now + b = np.ones((2, 6))[:, ::2] + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + (b.shape[-1],) + assert_allclose(a @ x - b, 0, atol=1e-14) + + @parametrize_overwrite_arg + def test_batch_weird_strides(self, overwrite_kw): + a = np.arange(3*8*2).reshape(2, 3, 2, 2, 2) + a = a.transpose(1, 3, 4, 0, 2) + + b = np.ones(2) + x = solve(a, b, **overwrite_kw) + assert x.shape == a.shape[:-1] + assert_allclose(a @ x[..., None] - b, 0, atol=1e-14) + + @parametrize_overwrite_arg + @parametrize_overwrite_b_arg + @pytest.mark.parametrize('a_dtype', [int, float]) + @pytest.mark.parametrize('a_order', ['C', 'F']) + @pytest.mark.parametrize('b_dtype', [int, float]) + @pytest.mark.parametrize('b_order', ['C', 'F']) + @pytest.mark.parametrize('b_ndim', [1, 2]) # XXX ndim > 2 + @pytest.mark.parametrize('transposed', [True, False]) + def test_overwrite_args( + self, overwrite_kw, overwrite_b_kw, a_dtype, a_order, + b_dtype, b_order, b_ndim, transposed + ): + n = 3 + a = np.arange(1, n**2 + 1).reshape(n, n) + np.eye(n) + a = a.astype(a_dtype, order=a_order) + + b = np.arange(n) + if b_ndim > 1: + b = np.stack([b*j for j in range(b_ndim)]).T + b = b.astype(b_dtype, order=b_order) + + a_ref = a.copy() + b_ref = b.copy() + + # solve and check that the solution is correct for all parameters + x = solve(a, b, **overwrite_kw, **overwrite_b_kw, transposed=transposed) + a_or_aT = a_ref.T if transposed else a_ref + assert_allclose(a_or_aT @ x, b_ref, atol=1e-14) + + # now check that it worked in-place where expected + overwrite_a = overwrite_kw.get('overwrite_a', False) + a_inplace = overwrite_a and (a.dtype != int) and a.flags['F_CONTIGUOUS'] + + overwrite_b = overwrite_b_kw.get('overwrite_b', False) + b_inplace = overwrite_b and (b.dtype != int) and b.flags['F_CONTIGUOUS'] + + assert np.shares_memory(x, b) == b_inplace + + assert (b == b_ref).all() != b_inplace + assert (a == a_ref).all() != a_inplace + + def test_posdef_not_posdef(self): + # the `b` matrix is invertible but not positive definite + a = np.arange(9).reshape(3, 3) + A = a + a.T + np.eye(3) + b = np.ones(3) + + # cholesky solver fails, and the routine falls back to the general inverse + x0 = solve(A, b) + assert_allclose(A @ x0, b, atol=1e-14) + + # but it does not fall back if `assume_a` is given + with assert_raises(LinAlgError): + solve(A, b, assume_a='pos') + + def test_diagonal(self): + a = np.stack([np.triu(np.ones((3, 3))), np.diag(np.arange(1, 4))]) + b = np.ones(3) + x = solve(a, b) + + # basic diagonal solve + assert_allclose(x[1, ...], 1 / np.arange(1, 4), atol=1e-14) + + # ill-conditioned inputs warn + a = np.asarray([[1e30, 0], [0, 1]]) + b = np.ones(2) + with pytest.warns(LinAlgWarning): + solve(a, b, assume_a="diagonal") + + # singular input raises + a = np.asarray([[0, 0], [0, 1]]) + b = np.ones(2) + with pytest.raises(LinAlgError): + solve(a, b, assume_a="diagonal") + + def test_tridiagonal(self): + n = 4 + a = -2*np.diag(np.ones(n)) + np.diag(np.ones(3), 1) + np.diag(np.ones(3), -1) + a = np.stack([np.triu(np.ones((n, n))), a]) + b = np.ones(4) + x = solve(a, b) + + # basic tridiag solve + assert_allclose(x[1, ...], np.asarray([-2., -3., -3., -2.]), atol=1e-15) + + # ill-conditioned inputs warn + a[1, 0, 0] = 1e20 + with pytest.warns(LinAlgWarning): + solve(a, b, assume_a="tridiagonal") + + # singular inputss raise + a[1, 0, 0] = a[1, 0, 1] = 0 + with pytest.raises(LinAlgError): + solve(a, b, assume_a="tridiagonal") + + +class TestSolveTriangular: + + def test_simple(self): + """ + solve_triangular on a simple 2x2 matrix. + """ + A = array([[1, 0], [1, 2]]) + b = [1, 1] + sol = solve_triangular(A, b, lower=True) + assert_array_almost_equal(sol, [1, 0]) + + # check that it works also for non-contiguous matrices + sol = solve_triangular(A.T, b, lower=False) + assert_array_almost_equal(sol, [.5, .5]) + + # and that it gives the same result as trans=1 + sol = solve_triangular(A, b, lower=True, trans=1) + assert_array_almost_equal(sol, [.5, .5]) + + b = identity(2) + sol = solve_triangular(A, b, lower=True, trans=1) + assert_array_almost_equal(sol, [[1., -.5], [0, 0.5]]) + + def test_simple_complex(self): + """ + solve_triangular on a simple 2x2 complex matrix + """ + A = array([[1+1j, 0], [1j, 2]]) + b = identity(2) + sol = solve_triangular(A, b, lower=True, trans=1) + assert_array_almost_equal(sol, [[.5-.5j, -.25-.25j], [0, 0.5]]) + + # check other option combinations with complex rhs + b = np.diag([1+1j, 1+2j]) + sol = solve_triangular(A, b, lower=True, trans=0) + assert_array_almost_equal(sol, [[1, 0], [-0.5j, 0.5+1j]]) + + sol = solve_triangular(A, b, lower=True, trans=1) + assert_array_almost_equal(sol, [[1, 0.25-0.75j], [0, 0.5+1j]]) + + sol = solve_triangular(A, b, lower=True, trans=2) + assert_array_almost_equal(sol, [[1j, -0.75-0.25j], [0, 0.5+1j]]) + + sol = solve_triangular(A.T, b, lower=False, trans=0) + assert_array_almost_equal(sol, [[1, 0.25-0.75j], [0, 0.5+1j]]) + + sol = solve_triangular(A.T, b, lower=False, trans=1) + assert_array_almost_equal(sol, [[1, 0], [-0.5j, 0.5+1j]]) + + sol = solve_triangular(A.T, b, lower=False, trans=2) + assert_array_almost_equal(sol, [[1j, 0], [-0.5, 0.5+1j]]) + + def test_check_finite(self): + """ + solve_triangular on a simple 2x2 matrix. + """ + A = array([[1, 0], [1, 2]]) + b = [1, 1] + sol = solve_triangular(A, b, lower=True, check_finite=False) + assert_array_almost_equal(sol, [1, 0]) + + @pytest.mark.parametrize('dt_a', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt_a, dt_b): + a = np.empty((0, 0), dtype=dt_a) + b = np.empty(0, dtype=dt_b) + x = solve_triangular(a, b) + + assert x.size == 0 + dt_nonempty = solve_triangular( + np.eye(2, dtype=dt_a), np.ones(2, dtype=dt_b) + ).dtype + assert x.dtype == dt_nonempty + + def test_empty_rhs(self): + a = np.eye(2) + b = [[], []] + x = solve_triangular(a, b) + assert_(x.size == 0, 'Returned array is not empty') + assert_(x.shape == (2, 0), 'Returned empty array shape is wrong') + + +class TestInv: + def test_simple(self): + a = [[1, 2], [3, 4]] + a_inv = inv(a) + assert_array_almost_equal(dot(a, a_inv), np.eye(2)) + a = [[1, 2, 3], [4, 5, 6], [7, 8, 10]] + a_inv = inv(a) + assert_array_almost_equal(dot(a, a_inv), np.eye(3)) + + def test_random(self): + rng = np.random.default_rng(1234) + n = 20 + for i in range(4): + a = rng.random([n, n]) + for i in range(n): + a[i, i] = 20*(.1+a[i, i]) + a_inv = inv(a) + assert_array_almost_equal(dot(a, a_inv), + identity(n)) + + def test_simple_complex(self): + a = [[1, 2], [3, 4j]] + a_inv = inv(a) + assert_array_almost_equal(dot(a, a_inv), [[1, 0], [0, 1]]) + + def test_random_complex(self): + rng = np.random.default_rng(1234) + n = 20 + for i in range(4): + a = rng.random([n, n])+2j*rng.random([n, n]) + for i in range(n): + a[i, i] = 20*(.1+a[i, i]) + a_inv = inv(a) + assert_array_almost_equal(dot(a, a_inv), + identity(n)) + + def test_check_finite(self): + a = [[1, 2], [3, 4]] + a_inv = inv(a, check_finite=False) + assert_array_almost_equal(dot(a, a_inv), [[1, 0], [0, 1]]) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + a_inv = inv(a) + assert a_inv.size == 0 + assert a_inv.dtype == inv(np.eye(2, dtype=dt)).dtype + + a = np.ones((3, 0, 2, 2), dtype=dt) + a_inv = inv(a) + assert a_inv.shape == (3, 0, 2, 2) + + a = np.ones((3, 1, 0, 0), dtype=dt) + a_inv = inv(a) + assert a_inv.shape == (3, 1, 0, 0) + + @parametrize_overwrite_arg + def test_overwrite_a(self, overwrite_kw): + n = 3 + a0 = np.arange(1, n**2 + 1).reshape(n, n) + np.eye(n) + + # int arrays are copied internally + a = a0.copy() + a_inv = inv(a, **overwrite_kw) + assert_allclose(a_inv @ a, np.eye(n), atol=1e-14) + assert_equal(a, a0) + assert not np.shares_memory(a, a_inv) + + # float C ordered arrays are copied, too + a = a0.copy().astype(float) + a_inv = inv(a, **overwrite_kw) + assert_allclose(a_inv @ a0, np.eye(n), atol=1e-14) + assert_equal(a, a0) + assert not np.shares_memory(a, a_inv) + + # 2D F-ordered arrays of LAPACK-compatible dtypes: inv works inplace. + # IOW, the output is always the inverse, and the original input may be + # destroyed, depending on the `overwrite_a` kwarg value + a = a0.astype(float).copy(order='F') + a_inv = inv(a, **overwrite_kw) + assert_allclose(a_inv @ a0, np.eye(n), atol=1e-14) + + overwrite_a = overwrite_kw.get("overwrite_a", False) + assert (a == a0).all() != overwrite_a + assert np.shares_memory(a, a_inv) == overwrite_a + + @pytest.mark.parametrize( + "dtyp", [np.float16, np.float32, np.longdouble, np.clongdouble] + ) + def test_dtypes(self, dtyp): + # backwards compat: inv(float16)->float32 ; inv(clongdouble)->complex128 etc + a = np.arange(4).reshape(2, 2).astype(dtyp) + + a_inv = inv(a) + assert_allclose(a @ a_inv, np.eye(a.shape[0]), atol=100*np.finfo(a.dtype).eps) + + dt_map = { + 'e': 'f', # float16 -> float32 + 'f': 'f', + 'g': 'd', # longdouble -> float64 + 'G': 'D' # clongdouble -> complex128 + } + assert a_inv.dtype.char == dt_map[a.dtype.char] + + def test_readonly(self): + a = np.eye(3) + a.flags.writeable = False + + a_inv = inv(a) + assert_allclose(a_inv, a, atol=1e-14) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_batch_core_1x1(self, dt): + a = np.arange(3*2, dtype=dt).reshape(3, 2, 1, 1) + 1 + a_inv = inv(a) + assert a_inv.shape == a.shape + assert_allclose(a @ a_inv, 1.) + + @parametrize_overwrite_arg + def test_batch_zero_stride(self, overwrite_kw): + a = np.arange(3*2*2, dtype=float).reshape(3, 2, 2) + aa = a[None, ...] + a_inv = inv(aa, **overwrite_kw) + assert a_inv.shape == aa.shape + assert_allclose(aa @ a_inv, np.broadcast_to(np.eye(2), aa.shape), atol=2e-14) + + aa = a[:, None, ...] + a_inv = inv(aa, **overwrite_kw) + assert a_inv.shape == aa.shape + assert_allclose(aa @ a_inv, np.broadcast_to(np.eye(2), aa.shape), atol=2e-14) + + @parametrize_overwrite_arg + def test_batch_negative_stride(self, overwrite_kw): + a = np.arange(3*8).reshape(2, 3, 2, 2) + a = a[:, ::-1, :, :] + a_inv = inv(a, **overwrite_kw) + assert a_inv.shape == a.shape + assert_allclose(a @ a_inv, np.broadcast_to(np.eye(2), a.shape), atol=5e-14) + + @parametrize_overwrite_arg + def test_core_negative_stride(self, overwrite_kw): + a = np.arange(3*8).reshape(2, 3, 2, 2) + a = a[:, :, ::-1, :] + a_inv = inv(a, **overwrite_kw) + assert a_inv.shape == a.shape + assert_allclose(a @ a_inv, np.broadcast_to(np.eye(2), a.shape), atol=5e-14) + + @parametrize_overwrite_arg + def test_core_non_contiguous(self, overwrite_kw): + a = np.arange(3*8*2).reshape(2, 3, 2, 4) + a = a[..., ::2] + a_inv = inv(a, **overwrite_kw) + assert a_inv.shape == (2, 3, 2, 2) + assert_allclose(a @ a_inv, np.broadcast_to(np.eye(2), a.shape), atol=5e-14) + + @parametrize_overwrite_arg + def test_batch_non_contiguous(self, overwrite_kw): + a = np.arange(3*8*2).reshape(2, 6, 2, 2) + a = a[:, ::2, ...] + a_inv = inv(a, **overwrite_kw) + assert a_inv.shape == (2, 3, 2, 2) + assert_allclose(a @ a_inv, np.broadcast_to(np.eye(2), a.shape), atol=2e-13) + + @parametrize_overwrite_arg + def test_singular(self, overwrite_kw): + # 2D case: A singular matrix: raise + + with assert_raises(LinAlgError): + inv(np.ones((2, 2))) + + # batched case: If all slices are singlar, raise + with assert_raises(LinAlgError): + inv(np.ones((3, 2, 2))) + + # XXX: shall we make this behavior configurable somehow? + # A "keep-going" option would be this: + # if some of the slices are singular and some are not, + # - singular slices are filled with nans + # - non-singular slices are inverted + # - there is no error + a = np.stack((np.ones((2, 2), dtype=complex), np.arange(4).reshape(2, 2))) + with assert_raises(LinAlgError): + inv(a) + + # this would be true for a "keep-going" option + # assert np.isnan(a_inv[0, ...]).all() + # assert_allclose(a_inv[1, ...] @ a[1, ...], np.eye(2), atol=1e-14) + + def test_ill_cond(self): + a = np.diag([1., 1e-20]) + with pytest.warns(LinAlgWarning): + inv(a) + + a2 = np.stack([np.diag([1., 1e-20]), np.diag([1, 1]), np.diag([1, 1e-20])]) + with pytest.warns(LinAlgWarning): + inv(a2) + + def test_wrong_assume_a(self): + with assert_raises(KeyError): + inv(np.eye(2), assume_a="kaboom") + + def test_posdef(self): + x = np.arange(25, dtype=float).reshape(5, 5) + y = x + x.T + y += 21*np.eye(5) + + y_inv0 = inv(y) + y_inv1 = inv(y, assume_a="pos") + + assert_allclose(y_inv1, y_inv0, atol=1e-15) + + # check that the lower triangle is not referenced for `lower=False` + mask = np.where(1 - np.tri(*y.shape, -1) == 0, np.nan, 1) + y_inv2 = inv(y*mask, check_finite=False, assume_a="pos", lower=False) + assert_allclose(y_inv2, y_inv0, atol=1e-15) + + # repeat with the upper triangle + y_inv3 = inv(y*mask.T, check_finite=False, assume_a="pos", lower=True) + assert_allclose(y_inv3, y_inv0, atol=1e-15) + + @pytest.mark.parametrize('complex_', [False, True]) + def test_posdef_not_posdef(self, complex_): + # the `b` matrix is invertible but not pos definite: test the "sym" fallback + a = np.arange(9).reshape(3, 3) + b = a + a.T + np.eye(3) + if complex_: + b = b + 1j*b + + # cholesky solver fails, and the routine falls back to the symmetric inverse + b_inv0 = inv(b) + assert_allclose(b_inv0 @ b, np.eye(3), atol=3e-15) + + # but it does not fall back if `assume_a` is given + with assert_raises(LinAlgError): + inv(b, assume_a='pos') + + # test posdef fallback to the hermitian solver, too + if complex_: + a = np.arange(9).reshape(3, 3) + a = a + 1j*a + b = a + a.T.conj() + np.eye(3) + assert_allclose(inv(b) @ b, np.eye(3), atol=3e-15) + + def test_pos_fails_sym_complex(self): + # regression test for gh-24359 + # the matrix is 1) symmetric not hermitian, and 2) not positive definite: + a = np.asarray([[ 182.56985285-64.28859483j, -177.24879835+11.0780499j ], + [-177.24879835+11.0780499j , 177.24879835-11.0780499j ]]) + + ainv = inv(a) + assert_allclose(ainv @ a, np.eye(2), atol=1e-14) + + ainv_sym = inv(a, assume_a="sym") + assert_allclose(ainv_sym, ainv, atol=1e-14) + + # Specifying assume_a="pos" disables the structure detection, and directly + # calls LAPACK routines zportf and zpotri. + # Since zportf(a) does not error out, neither does inv + ainv_chol = inv(a, assume_a="pos") + assert not np.allclose(ainv, ainv_chol, atol=1e-14) + + # Setting assume_a="pos" with a non-pos def matrix returned nonsense. + # This is at least consistent with solve. + ainv_slv = solve(a, np.eye(2), assume_a="pos") + assert_allclose(ainv_chol, ainv_slv, atol=1e-14) + + # Repeat it for bunch of simple cases to cover more branches + # Real symmetric, positive definite + a = np.eye(4) + np.ones(4) + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Real symmetric, NOT positive definite + a = -np.eye(4) + np.ones(4) + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Real, not symmetric + a = -np.eye(4) + np.ones(4) + a[0, -1] = 2. + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # | Test | is_symm | is_herm | pos def | + # |---------------------------------------|---------|---------|---------| + # | Complex, both sym+herm, pos def | 1 | 1 | yes | + # | Complex, symmetric only | 1 | 0 | - | + # | Complex, both sym+herm, NOT pos def | 1 | 1 | no | + # | Complex, neither | 0 | 0 | - | + # | Complex, hermitian only, pos def | 0 | 1 | yes | + # | Complex, hermitian only, NOT pos def | 0 | 1 | no | + + # Complex, both symmetric and hermitian, positive definite + a = (np.eye(4) + np.ones(4)).astype(np.complex128) + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Complex, symmetric only (not hermitian) + a = (np.eye(4)*1.0j + np.ones(4)).astype(np.complex128) + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Complex, both symmetric and hermitian, NOT positive definite + a = (-np.eye(4) + np.ones(4)).astype(np.complex128) + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Complex, neither symmetric nor hermitian + a = (-np.eye(4) + np.ones(4)).astype(np.complex128) + a[0, -1] = 2. + res = inv(a) + assert_allclose(res @ a, np.eye(4), atol=1e-14) + + # Complex, hermitian only, positive definite + a = np.array([[2, 1+1j], [1-1j, 2]], dtype=np.complex128) + res = inv(a) + assert_allclose(res @ a, np.eye(2), atol=1e-14) + + # Complex, hermitian only, NOT positive definite + a = np.array([[-1, 1+1j], [1-1j, -1]], dtype=np.complex128) + res = inv(a) + assert_allclose(res @ a, np.eye(2), atol=1e-14) + + @pytest.mark.parametrize('complex_', [False, True]) + @pytest.mark.parametrize('sym_herm', ['sym', 'her']) + def test_sym_her(self, complex_, sym_herm): + # test "sym" and "her" modes + a = np.arange(9).reshape(3, 3) + if complex_: + a = a + 1j*a + + if sym_herm == "sym": + b = a + a.T + else: # sym_herm == "herm": + b = a + a.T.conj() + + b = b + np.eye(3) + + b_inv0 = np.linalg.inv(b) + assert_allclose(b_inv0 @ b, np.eye(3), atol=1e-14) + + b_inv1 = inv(b, assume_a=sym_herm) + assert_allclose(b_inv0, b_inv1, atol=1e-15) + + # check that the "other" triangle is not referenced + mask = np.where(1 - np.tri(*a.shape, -1) == 0, np.nan, 1) + b_inv2 = inv(b*mask, check_finite=False, assume_a=sym_herm, lower=False) + assert_allclose(b_inv2, b_inv0, atol=1e-15) + + # repeat with the upper triangle + b_inv3 = inv(b*mask.T, check_finite=False, assume_a=sym_herm, lower=True) + assert_allclose(b_inv3, b_inv0, atol=1e-15) + + def test_triangular_1(self): + x = np.arange(25, dtype=float).reshape(5, 5) + y = x + x.T + y += 21*np.eye(5) + y_inv0 = inv(y, assume_a='upper triangular') + + # check that upper triangular differs from posdef + y_inv_posdef = inv(y, assume_a='pos') + assert not np.allclose(y_inv0, y_inv_posdef) + + def test_triangular_2(self): + y = np.ones(25, dtype=float).reshape(5, 5) + + y_inv_0_u = inv(np.triu(y)) + assert_allclose(y_inv_0_u @ np.triu(y), np.eye(5), atol=1e-15) + + y_inv_1_u = inv(y, assume_a='upper triangular') + assert_allclose(y_inv_1_u @ np.triu(y), np.eye(5), atol=1e-15) + + # check that the lower triangle is not referenced for "upper triangular" + mask = np.where(1 - np.tri(*y.shape, -1) == 0, np.nan, 1) + y_inv_2_u = inv(y*mask, check_finite=False, assume_a='upper triangular') + assert_allclose(y_inv_2_u @ np.triu(y), np.eye(5), atol=1e-15) + + # repeat for the lower traingular matrix + y_inv_0_l = inv(np.tril(y)) + assert_allclose(y_inv_0_l @ np.tril(y), np.eye(5), atol=1e-15) + + y_inv_1_l = inv(y, assume_a='lower triangular') + assert_allclose(y_inv_1_l @ np.tril(y), np.eye(5), atol=1e-15) + + # check that the lower triangle is not referenced for "lower triangular" + mask = np.where(1 - np.tri(*y.shape, -1) == 0, np.nan, 1) + y_inv_2_l = inv(y*mask.T, check_finite=False, assume_a='lower triangular') + assert_allclose(y_inv_2_l @ np.tril(y), np.eye(5), atol=1e-15) + + def test_diagonal(self): + a = np.stack([np.triu(np.ones((3, 3))), np.diag(np.arange(1, 4))]) + inv_a = inv(a) + + # basic diagonal invert + assert_allclose(inv_a[1], np.diag(1 / np.arange(1, 4)), atol=1e-14) + + # ill-conditioned inputs warn + a = np.asarray([[1e30, 0], [0, 1]]) + with pytest.warns(LinAlgWarning): + inv(a, assume_a="diagonal") + + # singular input raises + a = np.asarray([[0, 0], [0, 1]]) + with pytest.raises(LinAlgError): + inv(a, assume_a="diagonal") + + +class TestDet: + def test_1x1_all_singleton_dims(self): + a = np.array([[1]]) + deta = det(a) + assert deta.dtype.char == 'd' + assert np.isscalar(deta) + assert deta == 1. + a = np.array([[[[1]]]], dtype='f') + deta = det(a) + assert deta.dtype.char == 'd' + assert deta.shape == (1, 1) + assert_equal(deta, [[1.0]]) + a = np.array([[[1 + 3.j]]], dtype=np.complex64) + deta = det(a) + assert deta.dtype.char == 'D' + assert deta.shape == (1,) + assert_equal(deta, [1.+3.j]) + + def test_1by1_stacked_input_output(self): + rng = np.random.default_rng(1680305949878959) + a = rng.random([4, 5, 1, 1], dtype=np.float32) + deta = det(a) + assert deta.dtype.char == 'd' + assert deta.shape == (4, 5) + assert_allclose(deta, np.squeeze(a)) + + a = rng.random([4, 5, 1, 1], dtype=np.float32)*np.complex64(1.j) + deta = det(a) + assert deta.dtype.char == 'D' + assert deta.shape == (4, 5) + assert_allclose(deta, np.squeeze(a)) + + @pytest.mark.parametrize('shape', [[2, 2], [20, 20], [3, 2, 20, 20]]) + def test_simple_det_shapes_real_complex(self, shape): + rng = np.random.default_rng(1680305949878959) + a = rng.uniform(-1., 1., size=shape) + d1, d2 = det(a), np.linalg.det(a) + assert_allclose(d1, d2) + + b = rng.uniform(-1., 1., size=shape)*1j + b += rng.uniform(-0.5, 0.5, size=shape) + d3, d4 = det(b), np.linalg.det(b) + assert_allclose(d3, d4) + + def test_for_known_det_values(self): + # Hadamard8 + a = np.array([[1, 1, 1, 1, 1, 1, 1, 1], + [1, -1, 1, -1, 1, -1, 1, -1], + [1, 1, -1, -1, 1, 1, -1, -1], + [1, -1, -1, 1, 1, -1, -1, 1], + [1, 1, 1, 1, -1, -1, -1, -1], + [1, -1, 1, -1, -1, 1, -1, 1], + [1, 1, -1, -1, -1, -1, 1, 1], + [1, -1, -1, 1, -1, 1, 1, -1]]) + assert_allclose(det(a), 4096.) + + # consecutive number array always singular + assert_allclose(det(np.arange(25).reshape(5, 5)), 0.) + + # simple anti-diagonal block array + # Upper right has det (-2+1j) and lower right has (-2-1j) + # det(a) = - (-2+1j) (-2-1j) = 5. + a = np.array([[0.+0.j, 0.+0.j, 0.-1.j, 1.-1.j], + [0.+0.j, 0.+0.j, 1.+0.j, 0.-1.j], + [0.+1.j, 1.+1.j, 0.+0.j, 0.+0.j], + [1.+0.j, 0.+1.j, 0.+0.j, 0.+0.j]], dtype=np.complex64) + assert_allclose(det(a), 5.+0.j) + + # Fiedler companion complexified + # >>> a = scipy.linalg.fiedler_companion(np.arange(1, 10)) + a = np.array([[-2., -3., 1., 0., 0., 0., 0., 0.], + [1., 0., 0., 0., 0., 0., 0., 0.], + [0., -4., 0., -5., 1., 0., 0., 0.], + [0., 1., 0., 0., 0., 0., 0., 0.], + [0., 0., 0., -6., 0., -7., 1., 0.], + [0., 0., 0., 1., 0., 0., 0., 0.], + [0., 0., 0., 0., 0., -8., 0., -9.], + [0., 0., 0., 0., 0., 1., 0., 0.]])*1.j + assert_allclose(det(a), 9.) + + # g and G dtypes are handled differently in windows and other platforms + @pytest.mark.parametrize('typ', [x for x in np.typecodes['All'][:20] + if x not in 'gG']) + def test_sample_compatible_dtype_input(self, typ): + rng = np.random.default_rng(1680305949878959) + n = 4 + a = rng.random([n, n]).astype(typ) # value is not important + assert isinstance(det(a), (np.float64 | np.complex128)) + + def test_incompatible_dtype_input(self): + # Double backslashes needed for escaping pytest regex. + msg = 'cannot be cast to float\\(32, 64\\)' + + for c, t in zip('SUO', ['bytes8', 'str32', 'object']): + with assert_raises(TypeError, match=msg): + det(np.array([['a', 'b']]*2, dtype=c)) + with assert_raises(TypeError, match=msg): + det(np.array([[b'a', b'b']]*2, dtype='V')) + with assert_raises(TypeError, match=msg): + det(np.array([[100, 200]]*2, dtype='datetime64[s]')) + with assert_raises(TypeError, match=msg): + det(np.array([[100, 200]]*2, dtype='timedelta64[s]')) + + def test_empty_edge_cases(self): + assert_allclose(det(np.empty([0, 0])), 1.) + assert_allclose(det(np.empty([0, 0, 0])), np.array([])) + assert_allclose(det(np.empty([3, 0, 0])), np.array([1., 1., 1.])) + with assert_raises(ValueError, match='Last 2 dimensions'): + det(np.empty([0, 0, 3])) + with assert_raises(ValueError, match='at least two-dimensional'): + det(np.array([])) + with assert_raises(ValueError, match='Last 2 dimensions'): + det(np.array([[]])) + with assert_raises(ValueError, match='Last 2 dimensions'): + det(np.array([[[]]])) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty_dtype(self, dt): + a = np.empty((0, 0), dtype=dt) + d = det(a) + assert d.shape == () + assert d.dtype == det(np.eye(2, dtype=dt)).dtype + + a = np.empty((3, 0, 0), dtype=dt) + d = det(a) + assert d.shape == (3,) + assert d.dtype == det(np.zeros((3, 1, 1), dtype=dt)).dtype + + def test_overwrite_a(self): + # If all conditions are met then input should be overwritten; + # - dtype is one of 'fdFD' + # - C-contiguous + # - writeable + a = np.arange(9).reshape(3, 3).astype(np.float32) + ac = a.copy() + deta = det(ac, overwrite_a=True) + assert_allclose(deta, 0.) + assert not (a == ac).all() + + def test_readonly_array(self): + a = np.array([[2., 0., 1.], [5., 3., -1.], [1., 1., 1.]]) + a.setflags(write=False) + # overwrite_a will be overridden + assert_allclose(det(a, overwrite_a=True), 10.) + + def test_simple_check_finite(self): + a = [[1, 2], [3, np.inf]] + with assert_raises(ValueError, match='array must not contain'): + det(a) + + +def direct_lstsq(a, b, cmplx=0): + at = transpose(a) + if cmplx: + at = conjugate(at) + a1 = dot(at, a) + b1 = dot(at, b) + return solve(a1, b1) + + +class TestLstsq: + lapack_drivers = ('gelsd', 'gelss', 'gelsy', None) + + def test_simple_exact(self): + for dtype in REAL_DTYPES: + a = np.array([[1, 20], [-30, 4]], dtype=dtype) + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + for bt in (((1, 0), (0, 1)), (1, 0), + ((2, 1), (-30, 4))): + # Store values in case they are overwritten + # later + a1 = a.copy() + b = np.array(bt, dtype=dtype) + b1 = b.copy() + out = lstsq(a1, b1, + lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == 2, + f'expected efficient rank 2, got {r}') + assert_allclose(dot(a, x), b, + atol=25 * _eps_cast(a1.dtype), + rtol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_simple_overdet(self): + for dtype in REAL_DTYPES: + a = np.array([[1, 2], [4, 5], [3, 4]], dtype=dtype) + b = np.array([1, 2, 3], dtype=dtype) + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + if lapack_driver == 'gelsy': + residuals = np.sum((b - a.dot(x))**2) + else: + residuals = out[1] + r = out[2] + assert_(r == 2, f'expected efficient rank 2, got {r}') + assert_allclose(abs((dot(a, x) - b)**2).sum(axis=0), + residuals, + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + assert_allclose(x, (-0.428571428571429, 0.85714285714285), + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_simple_overdet_complex(self): + for dtype in COMPLEX_DTYPES: + a = np.array([[1+2j, 2], [4, 5], [3, 4]], dtype=dtype) + b = np.array([1, 2+4j, 3], dtype=dtype) + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + + x = out[0] + if lapack_driver == 'gelsy': + res = b - a.dot(x) + residuals = np.sum(res * res.conj()) + else: + residuals = out[1] + r = out[2] + assert_(r == 2, f'expected efficient rank 2, got {r}') + assert_allclose(abs((dot(a, x) - b)**2).sum(axis=0), + residuals, + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + assert_allclose( + x, (-0.4831460674157303 + 0.258426966292135j, + 0.921348314606741 + 0.292134831460674j), + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_simple_underdet(self): + for dtype in REAL_DTYPES: + a = np.array([[1, 2, 3], [4, 5, 6]], dtype=dtype) + b = np.array([1, 2], dtype=dtype) + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + + x = out[0] + r = out[2] + assert_(r == 2, f'expected efficient rank 2, got {r}') + assert_allclose(x, (-0.055555555555555, 0.111111111111111, + 0.277777777777777), + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + @pytest.mark.parametrize("dtype", REAL_DTYPES) + @pytest.mark.parametrize("n", (20, 200)) + @pytest.mark.parametrize("lapack_driver", lapack_drivers) + @pytest.mark.parametrize("overwrite", (True, False)) + def test_random_exact(self, dtype, n, lapack_driver, overwrite): + rng = np.random.RandomState(1234) + + a = np.asarray(rng.random([n, n]), dtype=dtype) + for i in range(n): + a[i, i] = 20 * (0.1 + a[i, i]) + for i in range(4): + b = np.asarray(rng.random([n, 3]), dtype=dtype) + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, + lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == n, f'expected efficient rank {n}, ' + f'got {r}') + if dtype is np.float32: + assert_allclose( + dot(a, x), b, + rtol=500 * _eps_cast(a1.dtype), + atol=500 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + else: + assert_allclose( + dot(a, x), b, + rtol=1000 * _eps_cast(a1.dtype), + atol=1000 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + @pytest.mark.skipif(IS_MUSL, reason="may segfault on Alpine, see gh-17630") + @pytest.mark.parametrize("dtype", COMPLEX_DTYPES) + @pytest.mark.parametrize("n", (20, 200)) + @pytest.mark.parametrize("lapack_driver", lapack_drivers) + @pytest.mark.parametrize("overwrite", (True, False)) + def test_random_complex_exact(self, dtype, n, lapack_driver, overwrite): + rng = np.random.RandomState(1234) + + a = np.asarray(rng.random([n, n]) + 1j*rng.random([n, n]), + dtype=dtype) + for i in range(n): + a[i, i] = 20 * (0.1 + a[i, i]) + for i in range(2): + b = np.asarray(rng.random([n, 3]), dtype=dtype) + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == n, f'expected efficient rank {n}, ' + f'got {r}') + if dtype is np.complex64: + assert_allclose( + dot(a, x), b, + rtol=400 * _eps_cast(a1.dtype), + atol=400 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + else: + assert_allclose( + dot(a, x), b, + rtol=1000 * _eps_cast(a1.dtype), + atol=1000 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_random_overdet(self): + rng = np.random.RandomState(1234) + for dtype in REAL_DTYPES: + for (n, m) in ((20, 15), (200, 2)): + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + a = np.asarray(rng.random([n, m]), dtype=dtype) + for i in range(m): + a[i, i] = 20 * (0.1 + a[i, i]) + for i in range(4): + b = np.asarray(rng.random([n, 3]), dtype=dtype) + # Store values in case they are overwritten later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, + lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == m, f'expected efficient rank {m}, ' + f'got {r}') + assert_allclose( + x, direct_lstsq(a, b, cmplx=0), + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_random_complex_overdet(self): + rng = np.random.RandomState(1234) + for dtype in COMPLEX_DTYPES: + for (n, m) in ((20, 15), (200, 2)): + for lapack_driver in TestLstsq.lapack_drivers: + for overwrite in (True, False): + a = np.asarray(rng.random([n, m]) + 1j*rng.random([n, m]), + dtype=dtype) + for i in range(m): + a[i, i] = 20 * (0.1 + a[i, i]) + for i in range(2): + b = np.asarray(rng.random([n, 3]), dtype=dtype) + # Store values in case they are overwritten + # later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, + lapack_driver=lapack_driver, + overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == m, f'expected efficient rank {m}, ' + f'got {r}') + assert_allclose( + x, direct_lstsq(a, b, cmplx=1), + rtol=25 * _eps_cast(a1.dtype), + atol=25 * _eps_cast(a1.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_check_finite(self): + with warnings.catch_warnings(): + # On (some) OSX this tests triggers a warning (gh-7538) + warnings.filterwarnings("ignore", + "internal gelsd driver lwork query error,.*" + "Falling back to 'gelss' driver.", RuntimeWarning) + + at = np.array(((1, 20), (-30, 4))) + for dtype, bt, lapack_driver, overwrite, check_finite in \ + itertools.product(REAL_DTYPES, + (((1, 0), (0, 1)), (1, 0), ((2, 1), (-30, 4))), + TestLstsq.lapack_drivers, + (True, False), + (True, False)): + + a = at.astype(dtype) + b = np.array(bt, dtype=dtype) + # Store values in case they are overwritten + # later + a1 = a.copy() + b1 = b.copy() + out = lstsq(a1, b1, lapack_driver=lapack_driver, + check_finite=check_finite, overwrite_a=overwrite, + overwrite_b=overwrite) + x = out[0] + r = out[2] + assert_(r == 2, f'expected efficient rank 2, got {r}') + assert_allclose(dot(a, x), b, + rtol=25 * _eps_cast(a.dtype), + atol=25 * _eps_cast(a.dtype), + err_msg=f"driver: {lapack_driver}") + + def test_empty(self): + for a_shape, b_shape in (((0, 2), (0,)), + ((0, 4), (0, 2)), + ((4, 0), (4,)), + ((4, 0), (4, 2))): + b = np.ones(b_shape) + x, residues, rank, s = lstsq(np.zeros(a_shape), b) + assert_equal(x, np.zeros((a_shape[1],) + b_shape[1:])) + residues_should_be = (np.empty((0,)) if a_shape[1] + else np.linalg.norm(b, axis=0)**2) + assert_equal(residues, residues_should_be) + assert_(rank == 0, 'expected rank 0') + assert_equal(s, np.empty((0,))) + + @pytest.mark.parametrize('dt_a', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty_dtype(self, dt_a, dt_b): + a = np.empty((0, 0), dtype=dt_a) + b = np.empty(0, dtype=dt_b) + x, residues, rank, s = lstsq(a, b) + + assert x.size == 0 + dt_nonempty = lstsq(np.eye(2, dtype=dt_a), np.ones(2, dtype=dt_b))[0].dtype + assert x.dtype == dt_nonempty + + +class TestPinv: + def test_simple_real(self): + a = array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], dtype=float) + a_pinv = pinv(a) + assert_array_almost_equal(dot(a, a_pinv), np.eye(3)) + + def test_simple_complex(self): + a = (array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], + dtype=float) + 1j * array([[10, 8, 7], [6, 5, 4], [3, 2, 1]], + dtype=float)) + a_pinv = pinv(a) + assert_array_almost_equal(dot(a, a_pinv), np.eye(3)) + + def test_simple_singular(self): + a = array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=float) + a_pinv = pinv(a) + expected = array([[-6.38888889e-01, -1.66666667e-01, 3.05555556e-01], + [-5.55555556e-02, 1.30136518e-16, 5.55555556e-02], + [5.27777778e-01, 1.66666667e-01, -1.94444444e-01]]) + assert_array_almost_equal(a_pinv, expected) + + def test_simple_cols(self): + a = array([[1, 2, 3], [4, 5, 6]], dtype=float) + a_pinv = pinv(a) + expected = array([[-0.94444444, 0.44444444], + [-0.11111111, 0.11111111], + [0.72222222, -0.22222222]]) + assert_array_almost_equal(a_pinv, expected) + + def test_simple_rows(self): + a = array([[1, 2], [3, 4], [5, 6]], dtype=float) + a_pinv = pinv(a) + expected = array([[-1.33333333, -0.33333333, 0.66666667], + [1.08333333, 0.33333333, -0.41666667]]) + assert_array_almost_equal(a_pinv, expected) + + def test_check_finite(self): + a = array([[1, 2, 3], [4, 5, 6.], [7, 8, 10]]) + a_pinv = pinv(a, check_finite=False) + assert_array_almost_equal(dot(a, a_pinv), np.eye(3)) + + def test_native_list_argument(self): + a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + a_pinv = pinv(a) + expected = array([[-6.38888889e-01, -1.66666667e-01, 3.05555556e-01], + [-5.55555556e-02, 1.30136518e-16, 5.55555556e-02], + [5.27777778e-01, 1.66666667e-01, -1.94444444e-01]]) + assert_array_almost_equal(a_pinv, expected) + + def test_atol_rtol(self): + rng = np.random.default_rng(1234) + n = 12 + # get a random ortho matrix for shuffling + q, _ = qr(rng.random((n, n))) + a_m = np.arange(35.0).reshape(7, 5) + a = a_m.copy() + a[0, 0] = 0.001 + atol = 1e-5 + rtol = 0.05 + # svds of a_m is ~ [116.906, 4.234, tiny, tiny, tiny] + # svds of a is ~ [116.906, 4.234, 4.62959e-04, tiny, tiny] + # Just abs cutoff such that we arrive at a_modified + a_p = pinv(a_m, atol=atol, rtol=0.) + adiff1 = a @ a_p @ a - a + adiff2 = a_m @ a_p @ a_m - a_m + # Now adiff1 should be around atol value while adiff2 should be + # relatively tiny + assert_allclose(np.linalg.norm(adiff1), 5e-4, atol=5.e-4) + assert_allclose(np.linalg.norm(adiff2), 5e-14, atol=5.e-14) + + # Now do the same but remove another sv ~4.234 via rtol + a_p = pinv(a_m, atol=atol, rtol=rtol) + adiff1 = a @ a_p @ a - a + adiff2 = a_m @ a_p @ a_m - a_m + assert_allclose(np.linalg.norm(adiff1), 4.233, rtol=0.01) + assert_allclose(np.linalg.norm(adiff2), 4.233, rtol=0.01) + + @pytest.mark.parametrize('dt', [float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + a_pinv = pinv(a) + assert a_pinv.size == 0 + assert a_pinv.dtype == pinv(np.eye(2, dtype=dt)).dtype + + +class TestPinvSymmetric: + def test_simple_real(self): + a = array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], dtype=float) + a = np.dot(a, a.T) + a_pinv = pinvh(a) + assert_array_almost_equal(np.dot(a, a_pinv), np.eye(3)) + + def test_nonpositive(self): + a = array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=float) + a = np.dot(a, a.T) + u, s, vt = np.linalg.svd(a) + s[0] *= -1 + a = np.dot(u * s, vt) # a is now symmetric non-positive and singular + a_pinv = pinv(a) + a_pinvh = pinvh(a) + assert_array_almost_equal(a_pinv, a_pinvh) + + def test_simple_complex(self): + a = (array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], + dtype=float) + 1j * array([[10, 8, 7], [6, 5, 4], [3, 2, 1]], + dtype=float)) + a = np.dot(a, a.conj().T) + a_pinv = pinvh(a) + assert_array_almost_equal(np.dot(a, a_pinv), np.eye(3)) + + def test_native_list_argument(self): + a = array([[1, 2, 3], [4, 5, 6], [7, 8, 10]], dtype=float) + a = np.dot(a, a.T) + a_pinv = pinvh(a.tolist()) + assert_array_almost_equal(np.dot(a, a_pinv), np.eye(3)) + + def test_zero_eigenvalue(self): + # https://github.com/scipy/scipy/issues/12515 + # the SYEVR eigh driver may give the zero eigenvalue > eps + a = np.array([[1, -1, 0], [-1, 2, -1], [0, -1, 1]]) + p = pinvh(a) + assert_allclose(p @ a @ p, p, atol=1e-15) + assert_allclose(a @ p @ a, a, atol=1e-15) + + def test_atol_rtol(self): + rng = np.random.default_rng(1234) + n = 12 + # get a random ortho matrix for shuffling + q, _ = qr(rng.random((n, n))) + a = np.diag([4, 3, 2, 1, 0.99e-4, 0.99e-5] + [0.99e-6]*(n-6)) + a = q.T @ a @ q + a_m = np.diag([4, 3, 2, 1, 0.99e-4, 0.] + [0.]*(n-6)) + a_m = q.T @ a_m @ q + atol = 1e-5 + rtol = (4.01e-4 - 4e-5)/4 + # Just abs cutoff such that we arrive at a_modified + a_p = pinvh(a, atol=atol, rtol=0.) + adiff1 = a @ a_p @ a - a + adiff2 = a_m @ a_p @ a_m - a_m + # Now adiff1 should dance around atol value since truncation + # while adiff2 should be relatively tiny + assert_allclose(norm(adiff1), atol, rtol=0.1) + assert_allclose(norm(adiff2), 1e-12, atol=1e-11) + + # Now do the same but through rtol cancelling atol value + a_p = pinvh(a, atol=atol, rtol=rtol) + adiff1 = a @ a_p @ a - a + adiff2 = a_m @ a_p @ a_m - a_m + # adiff1 and adiff2 should be elevated to ~1e-4 due to mismatch + assert_allclose(norm(adiff1), 1e-4, rtol=0.1) + assert_allclose(norm(adiff2), 1e-4, rtol=0.1) + + @pytest.mark.parametrize('dt', [float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + a_pinv = pinvh(a) + assert a_pinv.size == 0 + assert a_pinv.dtype == pinv(np.eye(2, dtype=dt)).dtype + + +@pytest.mark.parametrize('scale', (1e-20, 1., 1e20)) +@pytest.mark.parametrize('pinv_', (pinv, pinvh)) +def test_auto_rcond(scale, pinv_): + x = np.array([[1, 0], [0, 1e-10]]) * scale + expected = np.diag(1. / np.diag(x)) + x_inv = pinv_(x) + assert_allclose(x_inv, expected) + + +class TestVectorNorms: + + def test_types(self): + for dtype in np.typecodes['AllFloat']: + x = np.array([1, 2, 3], dtype=dtype) + tol = max(1e-15, np.finfo(dtype).eps.real * 20) + assert_allclose(norm(x), np.sqrt(14), rtol=tol) + assert_allclose(norm(x, 2), np.sqrt(14), rtol=tol) + + for dtype in np.typecodes['Complex']: + x = np.array([1j, 2j, 3j], dtype=dtype) + tol = max(1e-15, np.finfo(dtype).eps.real * 20) + assert_allclose(norm(x), np.sqrt(14), rtol=tol) + assert_allclose(norm(x, 2), np.sqrt(14), rtol=tol) + + def test_overflow(self): + # unlike numpy's norm, this one is + # safer on overflow + a = array([1e20], dtype=float32) + assert_almost_equal(norm(a), a) + + def test_stable(self): + # more stable than numpy's norm + a = array([1e4] + [1]*10000, dtype=float32) + try: + # snrm in double precision; we obtain the same as for float64 + # -- large atol needed due to varying blas implementations + assert_allclose(norm(a) - 1e4, 0.5, atol=1e-2) + except AssertionError: + # snrm implemented in single precision, == np.linalg.norm result + msg = ": Result should equal either 0.0 or 0.5 (depending on " \ + "implementation of snrm2)." + assert_almost_equal(norm(a) - 1e4, 0.0, err_msg=msg) + + def test_zero_norm(self): + assert_equal(norm([1, 0, 3], 0), 2) + assert_equal(norm([1, 2, 3], 0), 3) + + def test_axis_kwd(self): + a = np.array([[[2, 1], [3, 4]]] * 2, 'd') + assert_allclose(norm(a, axis=1), [[3.60555128, 4.12310563]] * 2) + assert_allclose(norm(a, 1, axis=1), [[5.] * 2] * 2) + + def test_keepdims_kwd(self): + a = np.array([[[2, 1], [3, 4]]] * 2, 'd') + b = norm(a, axis=1, keepdims=True) + assert_allclose(b, [[[3.60555128, 4.12310563]]] * 2) + assert_(b.shape == (2, 1, 2)) + assert_allclose(norm(a, 1, axis=2, keepdims=True), [[[3.], [7.]]] * 2) + + @pytest.mark.skipif(not HAS_ILP64, reason="64-bit BLAS required") + def test_large_vector(self): + check_free_memory(free_mb=17000) + x = np.zeros([2**31], dtype=np.float64) + x[-1] = 1 + res = norm(x) + del x + assert_allclose(res, 1.0) + + +class TestMatrixNorms: + + def test_matrix_norms(self): + # Not all of these are matrix norms in the most technical sense. + rng = np.random.default_rng(1234) + for n, m in (1, 1), (1, 3), (3, 1), (4, 4), (4, 5), (5, 4): + for t in np.float32, np.float64, np.complex64, np.complex128, np.int64: + A = 10 * rng.standard_normal((n, m)).astype(t) + if np.issubdtype(A.dtype, np.complexfloating): + A += 10j * rng.standard_normal((n, m)) + t_high = np.complex128 + else: + t_high = np.float64 + for order in (None, 'fro', 1, -1, 2, -2, np.inf, -np.inf): + actual = norm(A, ord=order) + desired = np.linalg.norm(A, ord=order) + # SciPy may return higher precision matrix norms. + # This is a consequence of using LAPACK. + if not np.allclose(actual, desired): + desired = np.linalg.norm(A.astype(t_high), ord=order) + assert_allclose(actual, desired) + + def test_axis_kwd(self): + a = np.array([[[2, 1], [3, 4]]] * 2, 'd') + b = norm(a, ord=np.inf, axis=(1, 0)) + c = norm(np.swapaxes(a, 0, 1), ord=np.inf, axis=(0, 1)) + d = norm(a, ord=1, axis=(0, 1)) + assert_allclose(b, c) + assert_allclose(c, d) + assert_allclose(b, d) + assert_(b.shape == c.shape == d.shape) + b = norm(a, ord=1, axis=(1, 0)) + c = norm(np.swapaxes(a, 0, 1), ord=1, axis=(0, 1)) + d = norm(a, ord=np.inf, axis=(0, 1)) + assert_allclose(b, c) + assert_allclose(c, d) + assert_allclose(b, d) + assert_(b.shape == c.shape == d.shape) + + def test_keepdims_kwd(self): + a = np.arange(120, dtype='d').reshape(2, 3, 4, 5) + b = norm(a, ord=np.inf, axis=(1, 0), keepdims=True) + c = norm(a, ord=1, axis=(0, 1), keepdims=True) + assert_allclose(b, c) + assert_(b.shape == c.shape) + + def test_empty(self): + a = np.empty((0, 0)) + assert_allclose(norm(a), 0.) + assert_allclose(norm(a, axis=0), np.zeros((0,))) + assert_allclose(norm(a, keepdims=True), np.zeros((1, 1))) + + a = np.empty((0, 3)) + assert_allclose(norm(a), 0.) + assert_allclose(norm(a, axis=0), np.zeros((3,))) + assert_allclose(norm(a, keepdims=True), np.zeros((1, 1))) + + +class TestOverwrite: + def test_solve(self): + assert_no_overwrite(solve, [(3, 3), (3,)]) + + def test_solve_triangular(self): + assert_no_overwrite(solve_triangular, [(3, 3), (3,)]) + + def test_solve_banded(self): + assert_no_overwrite(lambda ab, b: solve_banded((2, 1), ab, b), + [(4, 6), (6,)]) + + def test_solveh_banded(self): + assert_no_overwrite(solveh_banded, [(2, 6), (6,)]) + + def test_inv(self): + assert_no_overwrite(inv, [(3, 3)]) + + def test_det(self): + assert_no_overwrite(det, [(3, 3)]) + + def test_lstsq(self): + assert_no_overwrite(lstsq, [(3, 2), (3,)]) + + def test_pinv(self): + assert_no_overwrite(pinv, [(3, 3)]) + + def test_pinvh(self): + assert_no_overwrite(pinvh, [(3, 3)]) + + +class TestSolveCirculant: + + def test_basic1(self): + c = np.array([1, 2, 3, 5]) + b = np.array([1, -1, 1, 0]) + x = solve_circulant(c, b) + y = solve(circulant(c), b) + assert_allclose(x, y) + + def test_basic2(self): + # b is a 2-d matrix. + c = np.array([1, 2, -3, -5]) + b = np.arange(12).reshape(4, 3) + x = solve_circulant(c, b) + y = solve(circulant(c), b) + assert_allclose(x, y) + + def test_basic3(self): + # b is a 3-d matrix. + c = np.array([1, 2, -3, -5]) + b = np.arange(24).reshape(4, 3, 2) + x = solve_circulant(c, b) + y = solve(circulant(c), b.reshape(4, -1)).reshape(b.shape) + assert_allclose(x, y) + + def test_complex(self): + # Complex b and c + c = np.array([1+2j, -3, 4j, 5]) + b = np.arange(8).reshape(4, 2) + 0.5j + x = solve_circulant(c, b) + y = solve(circulant(c), b) + assert_allclose(x, y) + + def test_random_b_and_c(self): + # Random b and c + rng = np.random.RandomState(54321) + c = rng.standard_normal(50) + b = rng.standard_normal(50) + x = solve_circulant(c, b) + y = solve(circulant(c), b) + assert_allclose(x, y) + + def test_singular(self): + # c gives a singular circulant matrix. + c = np.array([1, 1, 0, 0]) + b = np.array([1, 2, 3, 4]) + x = solve_circulant(c, b, singular='lstsq') + y, res, rnk, s = lstsq(circulant(c), b) + assert_allclose(x, y) + assert_raises(LinAlgError, solve_circulant, x, y) + + def test_axis_args(self): + # Test use of caxis, baxis and outaxis. + + # c has shape (2, 1, 4) + c = np.array([[[-1, 2.5, 3, 3.5]], [[1, 6, 6, 6.5]]]) + + # b has shape (3, 4) + b = np.array([[0, 0, 1, 1], [1, 1, 0, 0], [1, -1, 0, 0]]) + + x = solve_circulant(c, b, baxis=1) + assert_equal(x.shape, (4, 2, 3)) + expected = np.empty_like(x) + expected[:, 0, :] = solve(circulant(c[0].ravel()), b.T) + expected[:, 1, :] = solve(circulant(c[1].ravel()), b.T) + assert_allclose(x, expected) + + x = solve_circulant(c, b, baxis=1, outaxis=-1) + assert_equal(x.shape, (2, 3, 4)) + assert_allclose(np.moveaxis(x, -1, 0), expected) + + # np.swapaxes(c, 1, 2) has shape (2, 4, 1); b.T has shape (4, 3). + x = solve_circulant(np.swapaxes(c, 1, 2), b.T, caxis=1) + assert_equal(x.shape, (4, 2, 3)) + assert_allclose(x, expected) + + def test_native_list_arguments(self): + # Same as test_basic1 using python's native list. + c = [1, 2, 3, 5] + b = [1, -1, 1, 0] + x = solve_circulant(c, b) + y = solve(circulant(c), b) + assert_allclose(x, y) + + @pytest.mark.parametrize('dt_c', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt_c, dt_b): + c = np.array([], dtype=dt_c) + b = np.array([], dtype=dt_b) + x = solve_circulant(c, b) + assert x.shape == (0,) + assert x.dtype == solve_circulant(np.arange(3, dtype=dt_c), + np.ones(3, dtype=dt_b)).dtype + + b = np.empty((0, 0), dtype=dt_b) + x1 = solve_circulant(c, b) + assert x1.shape == (0, 0) + assert x1.dtype == x.dtype + + +class TestMatrix_Balance: + @skip_xp_invalid_arg + def test_string_arg(self): + assert_raises(ValueError, matrix_balance, 'Some string for fail') + + def test_infnan_arg(self): + assert_raises(ValueError, matrix_balance, + np.array([[1, 2], [3, np.inf]])) + assert_raises(ValueError, matrix_balance, + np.array([[1, 2], [3, np.nan]])) + + def test_scaling(self): + _, y = matrix_balance(np.array([[1000, 1], [1000, 0]])) + # Pre/post LAPACK 3.5.0 gives the same result up to an offset + # since in each case col norm is x1000 greater and + # 1000 / 32 ~= 1 * 32 hence balanced with 2 ** 5. + assert_allclose(np.diff(np.log2(np.diag(y))), [5]) + + def test_scaling_order(self): + A = np.array([[1, 0, 1e-4], [1, 1, 1e-2], [1e4, 1e2, 1]]) + x, y = matrix_balance(A) + assert_allclose(solve(y, A).dot(y), x) + + def test_separate(self): + _, (y, z) = matrix_balance(np.array([[1000, 1], [1000, 0]]), + separate=1) + assert_equal(np.diff(np.log2(y)), [5]) + assert_allclose(z, np.arange(2)) + + def test_permutation(self): + A = block_diag(np.ones((2, 2)), np.tril(np.ones((2, 2))), + np.ones((3, 3))) + x, (y, z) = matrix_balance(A, separate=1) + assert_allclose(y, np.ones_like(y)) + assert_allclose(z, np.array([0, 1, 6, 5, 4, 3, 2])) + + def test_perm_and_scaling(self): + # Matrix with its diagonal removed + cases = ( # Case 0 + np.array([[0., 0., 0., 0., 0.000002], + [0., 0., 0., 0., 0.], + [2., 2., 0., 0., 0.], + [2., 2., 0., 0., 0.], + [0., 0., 0.000002, 0., 0.]]), + # Case 1 user reported GH-7258 + np.array([[-0.5, 0., 0., 0.], + [0., -1., 0., 0.], + [1., 0., -0.5, 0.], + [0., 1., 0., -1.]]), + # Case 2 user reported GH-7258 + np.array([[-3., 0., 1., 0.], + [-1., -1., -0., 1.], + [-3., -0., -0., 0.], + [-1., -0., 1., -1.]]) + ) + + for A in cases: + x, y = matrix_balance(A) + x, (s, p) = matrix_balance(A, separate=1) + ip = np.empty_like(p) + ip[p] = np.arange(A.shape[0]) + assert_allclose(y, np.diag(s)[ip, :]) + assert_allclose(solve(y, A).dot(y), x) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + b, t = matrix_balance(a) + + assert b.size == 0 + assert t.size == 0 + + b_n, t_n = matrix_balance(np.eye(2, dtype=dt)) + assert b.dtype == b_n.dtype + assert t.dtype == t_n.dtype + + b, (scale, perm) = matrix_balance(a, separate=True) + assert b.size == 0 + assert scale.size == 0 + assert perm.size == 0 + + b_n, (scale_n, perm_n) = matrix_balance(a, separate=True) + assert b.dtype == b_n.dtype + assert scale.dtype == scale_n.dtype + assert perm.dtype == perm_n.dtype + + +class TestDTypes: + """Check backwards compatibility for dtypes vs scipy 1.16.""" + + def get_arr2D(self, tcode): + # return a valid 2D array for the typecode + if tcode == 'M': + return np.eye(2, dtype='datetime64[ms]') + elif tcode == 'V': + return np.asarray([[b'a', b'b'], [b'c', b'd']], dtype='V') + else: + return np.eye(2, dtype=tcode) + + def get_arr1D(self, tcode): + # return a valid 1D array for the typecode + if tcode == 'M': + return np.ones(2, dtype='datetime64[ms]') + elif tcode == 'V': + return np.asarray([b'a', b'b'], dtype='V') + else: + return np.ones(2, dtype=tcode) + + @pytest.mark.parametrize("tcode", np.typecodes['All']) + def test_inv(self, tcode): + # check backwards compat vs scipy 1.16 + a = self.get_arr2D(tcode) + if tcode in 'SUVO': + # raises + with pytest.raises(ValueError): + inv(a) + else: + # passes + inv(a) + + @pytest.mark.parametrize("tcode", np.typecodes['All']) + def test_det(self, tcode): + a = self.get_arr2D(tcode) + + is_arm = platform.machine() == 'arm64' + is_windows = os.name == 'nt' + + failing_tcodes = 'SUVOmM' + if not (is_arm or is_windows): + failing_tcodes += 'gG' + + if tcode in failing_tcodes: + # raises + with pytest.raises(TypeError): + det(a) + else: + # passes + det(a) + + @pytest.mark.filterwarnings("ignore:Casting complex values") + @pytest.mark.parametrize("tcode_a", np.typecodes['All']) + @pytest.mark.parametrize("tcode_b", np.typecodes['All']) + def test_solve(self, tcode_a, tcode_b): + a = self.get_arr2D(tcode_a) + b = self.get_arr1D(tcode_b) + + can_combine = True + try: + np.result_type(tcode_a, tcode_b) + except TypeError: + can_combine = False + + if not can_combine: + # np.exceptions.DTypePromotionError subclasses TypeError + with pytest.raises(TypeError): + solve(a, b) + elif tcode_a in 'SUVO' or tcode_b in 'VO': + with pytest.raises(ValueError): + solve(a, b) + else: + solve(a, b) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_batch.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_batch.py new file mode 100644 index 0000000000000000000000000000000000000000..dff2bb92262c951ee2529609a6c19b6fff142f91 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_batch.py @@ -0,0 +1,620 @@ +import inspect +import pytest +import numpy as np +from numpy.testing import assert_allclose +from scipy import linalg, sparse + + +real_floating = [np.float32, np.float64] +complex_floating = [np.complex64, np.complex128] +floating = real_floating + complex_floating + + +def get_random(shape, *, dtype, rng): + A = rng.random(shape) + if np.issubdtype(dtype, np.complexfloating): + A = A + rng.random(shape) * 1j + return A.astype(dtype) + +def get_nearly_hermitian(shape, dtype, atol, rng): + # Generate a batch of nearly Hermitian matrices with specified + # `shape` and `dtype`. `atol` controls the level of noise in + # Hermitian-ness to by generated by `rng`. + A = rng.random(shape).astype(dtype) + At = np.conj(A.swapaxes(-1, -2)) + noise = rng.standard_normal(size=A.shape).astype(dtype) * atol + return A + At + noise + + +class TestBatch: + # Test batch support for most linalg functions + + def batch_test(self, fun, arrays, *, core_dim=2, n_out=1, kwargs=None, dtype=None, + broadcast=True, check_kwargs=True): + # Check that all outputs of batched call `fun(A, **kwargs)` are the same + # as if we loop over the separate vectors/matrices in `A`. Also check + # that `fun` accepts `A` by position or keyword and that results are + # identical. This is important because the name of the array argument + # is manually specified to the decorator, and it's easy to mess up. + # However, this makes it hard to test positional arguments passed + # after the array, so we test that separately for a few functions to + # make sure the decorator is working as it should. + + kwargs = {} if kwargs is None else kwargs + parameters = list(inspect.signature(fun).parameters.keys()) + arrays = (arrays,) if not isinstance(arrays, tuple) else arrays + + # Identical results when passing argument by keyword or position + res2 = fun(*arrays, **kwargs) + if check_kwargs: + res1 = fun(**dict(zip(parameters, arrays)), **kwargs) + for out1, out2 in zip(res1, res2): # even a single array is iterable... + np.testing.assert_equal(out1, out2) + + # Check results vs looping over + res = (res2,) if n_out == 1 else res2 + # This is not the general behavior (only batch dimensions get + # broadcasted by the decorator) but it's easier for testing. + if broadcast: + arrays = np.broadcast_arrays(*arrays) + batch_shape = arrays[0].shape[:-core_dim] + for i in range(batch_shape[0]): + for j in range(batch_shape[1]): + arrays_ij = (array[i, j] for array in arrays) + ref = fun(*arrays_ij, **kwargs) + ref = ((np.asarray(ref),) if n_out == 1 else + tuple(np.asarray(refk) for refk in ref)) + for k in range(n_out): + assert_allclose(res[k][i, j], ref[k]) + assert np.shape(res[k][i, j]) == ref[k].shape + + for k in range(len(ref)): + out_dtype = ref[k].dtype if dtype is None else dtype + assert res[k].dtype == out_dtype + + return res2 # return original, non-tuplized result + + @pytest.mark.parametrize('dtype', floating) + def test_expm_cond(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = rng.random((5, 3, 4, 4)).astype(dtype) + self.batch_test(linalg.expm_cond, A) + + @pytest.mark.parametrize('dtype', floating) + def test_issymmetric(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_nearly_hermitian((5, 3, 4, 4), dtype, 3e-4, rng) + res = self.batch_test(linalg.issymmetric, A, kwargs=dict(atol=1e-3)) + assert not np.all(res) # ensure test is not trivial: not all True or False; + assert np.any(res) # also confirms that `atol` is passed to issymmetric + + @pytest.mark.parametrize('dtype', floating) + def test_ishermitian(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_nearly_hermitian((5, 3, 4, 4), dtype, 3e-4, rng) + res = self.batch_test(linalg.ishermitian, A, kwargs=dict(atol=1e-3)) + assert not np.all(res) # ensure test is not trivial: not all True or False; + assert np.any(res) # also confirms that `atol` is passed to ishermitian + + @pytest.mark.parametrize('dtype', floating) + def test_diagsvd(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = rng.random((5, 3, 4)).astype(dtype) + res1 = self.batch_test(linalg.diagsvd, A, kwargs=dict(M=6, N=4), core_dim=1) + # test that `M, N` can be passed by position + res2 = linalg.diagsvd(A, 6, 4) + np.testing.assert_equal(res1, res2) + + @pytest.mark.parametrize('fun', [linalg.inv, linalg.sqrtm, linalg.signm, + linalg.sinm, linalg.cosm, linalg.tanhm, + linalg.sinhm, linalg.coshm, linalg.tanhm, + linalg.pinv, linalg.pinvh, linalg.orth]) + @pytest.mark.parametrize('dtype', floating) + def test_matmat(self, fun, dtype): # matrix in, matrix out + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + + # sqrtm can return complex output for real input resulting in i/o type + # mismatch. Nudge the eigenvalues to positive side to avoid this. + if fun == linalg.sqrtm: + A = A + 3*np.eye(4, dtype=dtype) + + self.batch_test(fun, A) + + @pytest.mark.parametrize('dtype', floating) + def test_null_space(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 6), dtype=dtype, rng=rng) + self.batch_test(linalg.null_space, A) + + @pytest.mark.parametrize('dtype', floating) + def test_funm(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 4, 3, 3), dtype=dtype, rng=rng) + self.batch_test(linalg.funm, A, kwargs=dict(func=np.sin)) + + @pytest.mark.parametrize('dtype', floating) + def test_fractional_matrix_power(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 4, 3, 3), dtype=dtype, rng=rng) + res1 = self.batch_test(linalg.fractional_matrix_power, A, kwargs={'t':1.5}) + # test that `t` can be passed by position + res2 = linalg.fractional_matrix_power(A, 1.5) + np.testing.assert_equal(res1, res2) + + @pytest.mark.parametrize('dtype', floating) + def test_logm(self, dtype): + # One test failed absolute tolerance with default random seed + rng = np.random.default_rng(89940026998903887141749720079406074936) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + A = A + 3*np.eye(4) # avoid complex output for real input + res1 = self.batch_test(linalg.logm, A) + # test that `disp` can be passed by position + res2 = linalg.logm(A) + for res1i, res2i in zip(res1, res2): + np.testing.assert_equal(res1i, res2i) + + @pytest.mark.parametrize('dtype', floating) + def test_pinv(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(linalg.pinv, A, n_out=2, kwargs=dict(return_rank=True)) + + @pytest.mark.parametrize('dtype', floating) + def test_matrix_balance(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(linalg.matrix_balance, A, n_out=2) + self.batch_test(linalg.matrix_balance, A, n_out=2, kwargs={'separate':True}) + + @pytest.mark.parametrize('dtype', floating) + def test_bandwidth(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((4, 4), dtype=dtype, rng=rng) + A = np.asarray([np.triu(A, k) for k in range(-3, 3)]).reshape((2, 3, 4, 4)) + self.batch_test(linalg.bandwidth, A, n_out=2) + + @pytest.mark.parametrize('fun_n_out', [(linalg.cholesky, 1), (linalg.ldl, 3), + (linalg.cho_factor, 2)]) + @pytest.mark.parametrize('dtype', floating) + def test_ldl_cholesky(self, fun_n_out, dtype): + rng = np.random.default_rng(8342310302941288912051) + fun, n_out = fun_n_out + A = get_nearly_hermitian((5, 3, 4, 4), dtype, 0, rng) # exactly Hermitian + A = A + 4*np.eye(4, dtype=dtype) # ensure positive definite for Cholesky + self.batch_test(fun, A, n_out=n_out) + + @pytest.mark.parametrize('compute_uv', [False, True]) + @pytest.mark.parametrize('dtype', floating) + def test_svd(self, compute_uv, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 2, 4), dtype=dtype, rng=rng) + n_out = 3 if compute_uv else 1 + self.batch_test(linalg.svd, A, n_out=n_out, kwargs=dict(compute_uv=compute_uv)) + + @pytest.mark.parametrize('fun', [linalg.polar, linalg.qr, linalg.rq]) + @pytest.mark.parametrize('dtype', floating) + def test_polar_qr_rq(self, fun, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 2, 4), dtype=dtype, rng=rng) + self.batch_test(fun, A, n_out=2) + + @pytest.mark.parametrize('cdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_qr_multiply(self, cdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + c = get_random(cdim, dtype=dtype, rng=rng) + res = linalg.qr_multiply(A, c, mode='left') + q, r = linalg.qr(A) + ref = q @ c + atol = 1e-6 if dtype in {np.float32, np.complex64} else 1e-12 + assert_allclose(res[0], ref, atol=atol) + assert_allclose(res[1], r, atol=atol) + + @pytest.mark.parametrize('uvdim', [[(5,), (3,)], [(4, 5, 2), (4, 3, 2)]]) + @pytest.mark.parametrize('dtype', floating) + def test_qr_update(self, uvdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + udim, vdim = uvdim + A = get_random((4, 5, 3), dtype=dtype, rng=rng) + u = get_random(udim, dtype=dtype, rng=rng) + v = get_random(vdim, dtype=dtype, rng=rng) + q, r = linalg.qr(A) + res = linalg.qr_update(q, r, u, v) + for i in range(4): + qi, ri = q[i], r[i] + ui, vi = (u, v) if u.ndim == 1 else (u[i], v[i]) + ref_i = linalg.qr_update(qi, ri, ui, vi) + assert_allclose(res[0][i], ref_i[0]) + assert_allclose(res[1][i], ref_i[1]) + + @pytest.mark.parametrize('udim', [(5,), (4, 3, 5)]) + @pytest.mark.parametrize('kdim', [(), (4,)]) + @pytest.mark.parametrize('dtype', floating) + def test_qr_insert(self, udim, kdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((4, 5, 5), dtype=dtype, rng=rng) + u = get_random(udim, dtype=dtype, rng=rng) + k = rng.integers(0, 5, size=kdim) + q, r = linalg.qr(A) + res = linalg.qr_insert(q, r, u, k) + for i in range(4): + qi, ri = q[i], r[i] + ki = k if k.ndim == 0 else k[i] + ui = u if u.ndim == 1 else u[i] + ref_i = linalg.qr_insert(qi, ri, ui, ki) + assert_allclose(res[0][i], ref_i[0]) + assert_allclose(res[1][i], ref_i[1]) + + @pytest.mark.parametrize('kdim', [(), (4,)]) + @pytest.mark.parametrize('dtype', floating) + def test_qr_delete(self, kdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((4, 5, 5), dtype=dtype, rng=rng) + k = rng.integers(0, 4, size=kdim) + q, r = linalg.qr(A) + res = linalg.qr_delete(q, r, k) + for i in range(4): + qi, ri = q[i], r[i] + ki = k if k.ndim == 0 else k[i] + ref_i = linalg.qr_delete(qi, ri, ki) + assert_allclose(res[0][i], ref_i[0]) + assert_allclose(res[1][i], ref_i[1]) + + @pytest.mark.parametrize('fun', [linalg.schur, linalg.lu_factor]) + @pytest.mark.parametrize('dtype', floating) + def test_schur_lu(self, fun, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(fun, A, n_out=2) + + @pytest.mark.parametrize('calc_q', [False, True]) + @pytest.mark.parametrize('dtype', floating) + def test_hessenberg(self, calc_q, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + n_out = 2 if calc_q else 1 + self.batch_test(linalg.hessenberg, A, n_out=n_out, kwargs=dict(calc_q=calc_q)) + + @pytest.mark.parametrize('eigvals_only', [False, True]) + @pytest.mark.parametrize('dtype', floating) + def test_eig_banded(self, eigvals_only, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + n_out = 1 if eigvals_only else 2 + self.batch_test(linalg.eig_banded, A, n_out=n_out, + kwargs=dict(eigvals_only=eigvals_only)) + + @pytest.mark.parametrize('dtype', floating) + def test_eigvals_banded(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(linalg.eigvals_banded, A) + + @pytest.mark.parametrize('two_in', [False, True]) + @pytest.mark.parametrize('fun_n_nout', [(linalg.eigh, 1), (linalg.eigh, 2), + (linalg.eigvalsh, 1), (linalg.eigvals, 1)]) + @pytest.mark.parametrize('dtype', floating) + def test_eigh(self, two_in, fun_n_nout, dtype): + rng = np.random.default_rng(8342310302941288912051) + fun, n_out = fun_n_nout + A = get_nearly_hermitian((1, 3, 4, 4), dtype, 0, rng) # exactly Hermitian + B = get_nearly_hermitian((2, 1, 4, 4), dtype, 0, rng) # exactly Hermitian + B = B + 4*np.eye(4).astype(dtype) # needs to be positive definite + args = (A, B) if two_in else (A,) + kwargs = dict(eigvals_only=True) if (n_out == 1 and fun==linalg.eigh) else {} + self.batch_test(fun, args, n_out=n_out, kwargs=kwargs) + + @pytest.mark.parametrize('compute_expm', [False, True]) + @pytest.mark.parametrize('dtype', floating) + def test_expm_frechet(self, compute_expm, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((1, 3, 4, 4), dtype=dtype, rng=rng) + E = get_random((2, 1, 4, 4), dtype=dtype, rng=rng) + n_out = 2 if compute_expm else 1 + self.batch_test(linalg.expm_frechet, (A, E), n_out=n_out, + kwargs=dict(compute_expm=compute_expm)) + + @pytest.mark.parametrize('dtype', floating) + def test_subspace_angles(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((1, 3, 4, 3), dtype=dtype, rng=rng) + B = get_random((2, 1, 4, 3), dtype=dtype, rng=rng) + self.batch_test(linalg.subspace_angles, (A, B)) + # just to show that A and B don't need to be broadcastable + M, N, K = 4, 5, 3 + A = get_random((1, 3, M, N), dtype=dtype, rng=rng) + B = get_random((2, 1, M, K), dtype=dtype, rng=rng) + assert linalg.subspace_angles(A, B).shape == (2, 3, min(N, K)) + + @pytest.mark.parametrize('fun', [linalg.svdvals]) + @pytest.mark.parametrize('dtype', floating) + def test_svdvals(self, fun, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 4, 5), dtype=dtype, rng=rng) + self.batch_test(fun, A) + + @pytest.mark.parametrize('fun_n_out', [(linalg.orthogonal_procrustes, 2), + (linalg.khatri_rao, 1), + (linalg.solve_continuous_lyapunov, 1), + (linalg.solve_discrete_lyapunov, 1), + (linalg.qz, 4), + (linalg.ordqz, 6)]) + @pytest.mark.parametrize('dtype', floating) + def test_two_generic_matrix_inputs(self, fun_n_out, dtype): + rng = np.random.default_rng(8342310302941288912051) + fun, n_out = fun_n_out + A = get_random((2, 3, 4, 4), dtype=dtype, rng=rng) + B = get_random((2, 3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(fun, (A, B), n_out=n_out) + + @pytest.mark.parametrize('dtype', floating) + def test_cossin(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + p, q = 3, 4 + X = get_random((2, 3, 10, 10), dtype=dtype, rng=rng) + x11, x12, x21, x22 = (X[..., :p, :q], X[..., :p, q:], + X[..., p:, :q], X[..., p:, q:]) + res = linalg.cossin(X, p, q) + ref = linalg.cossin((x11, x12, x21, x22)) + for res_i, ref_i in zip(res, ref): + np.testing.assert_equal(res_i, ref_i) + + for j in range(2): + for k in range(3): + ref_jk = linalg.cossin(X[j, k], p, q) + for res_i, ref_ijk in zip(res, ref_jk): + np.testing.assert_equal(res_i[j, k], ref_ijk) + + @pytest.mark.parametrize('dtype', floating) + def test_sylvester(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + B = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + C = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + self.batch_test(linalg.solve_sylvester, (A, B, C)) + + @pytest.mark.parametrize('fun', [linalg.solve_continuous_are, + linalg.solve_discrete_are]) + @pytest.mark.parametrize('dtype', floating) + def test_are(self, fun, dtype): + rng = np.random.default_rng(8342310302941288912051) + a = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + b = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + q = get_nearly_hermitian((2, 3, 5, 5), dtype=dtype, atol=0, rng=rng) + r = get_nearly_hermitian((2, 3, 5, 5), dtype=dtype, atol=0, rng=rng) + a = a + 5*np.eye(5) # making these positive definite seems to help + b = b + 5*np.eye(5) + q = q + 5*np.eye(5) + r = r + 5*np.eye(5) + e = np.eye(5) + s = np.zeros((5, 5)) + self.batch_test(fun, (a, b, q, r)) + self.batch_test(fun, (a, b, q, r, e)) + self.batch_test(fun, (a, b, q, r, e, s)) + + res = fun(a, b, q, r) + ref = fun(a, b, q, r, s=s) + np.testing.assert_allclose(res, ref) + + @pytest.mark.parametrize('dtype', floating) + def test_rsf2cs(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 4, 4), dtype=dtype, rng=rng) + T, Z = linalg.schur(A) + self.batch_test(linalg.rsf2csf, (T, Z), n_out=2) + + @pytest.mark.parametrize('dtype', floating) + def test_cholesky_banded(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + ab = get_random((5, 4, 3, 6), dtype=dtype, rng=rng) + ab[..., -1, :] = 10 # make diagonal dominant + self.batch_test(linalg.cholesky_banded, ab) + + @pytest.mark.parametrize('dtype', floating) + def test_block_diag(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + a = get_random((1, 3, 1, 3), dtype=dtype, rng=rng) + b = get_random((2, 1, 3, 6), dtype=dtype, rng=rng) + c = get_random((1, 1, 3, 2), dtype=dtype, rng=rng) + + # batch_test doesn't have the logic to broadcast just the batch shapes, + # so do it manually. + a2 = np.broadcast_to(a, (2, 3, 1, 3)) + b2 = np.broadcast_to(b, (2, 3, 3, 6)) + c2 = np.broadcast_to(c, (2, 3, 3, 2)) + ref = self.batch_test(linalg.block_diag, (a2, b2, c2), + check_kwargs=False, broadcast=False) + + # Check that `block_diag` broadcasts the batch shapes as expected. + res = linalg.block_diag(a, b, c) + assert_allclose(res, ref) + + @pytest.mark.parametrize('fun_n_out', [(linalg.eigh_tridiagonal, 2), + (linalg.eigvalsh_tridiagonal, 1)]) + @pytest.mark.parametrize('dtype', real_floating) + # "Only real arrays currently supported" + def test_eigh_tridiagonal(self, fun_n_out, dtype): + rng = np.random.default_rng(8342310302941288912051) + fun, n_out = fun_n_out + d = get_random((3, 4, 5), dtype=dtype, rng=rng) + e = get_random((3, 4, 4), dtype=dtype, rng=rng) + self.batch_test(fun, (d, e), core_dim=1, n_out=n_out, broadcast=False) + + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_solve(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.solve(A, b) + if len(bdim) == 1: + x = x[..., np.newaxis] + b = b[..., np.newaxis] + assert_allclose(A @ x - b, 0, atol=2e-6) + assert_allclose(x, np.linalg.solve(A, b), atol=3e-6) + + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_lu_solve(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + b = get_random(bdim, dtype=dtype, rng=rng) + lu_and_piv = linalg.lu_factor(A) + x = linalg.lu_solve(lu_and_piv, b) + if len(bdim) == 1: + x = x[..., np.newaxis] + b = b[..., np.newaxis] + assert_allclose(A @ x - b, 0, atol=2e-6) + assert_allclose(x, np.linalg.solve(A, b), atol=3e-6) + + @pytest.mark.parametrize('l_and_u', [(1, 1), ([2, 1, 0], [0, 1 , 2])]) + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_solve_banded(self, l_and_u, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + l, u = l_and_u + ab = get_random((2, 3, 3, 5), dtype=dtype, rng=rng) + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.solve_banded((l, u), ab, b) + for i in range(2): + for j in range(3): + bij = b if len(bdim) <= 2 else b[i, j] + lj = l if np.ndim(l) == 0 else l[j] + uj = u if np.ndim(u) == 0 else u[j] + xij = linalg.solve_banded((lj, uj), ab[i, j], bij) + assert_allclose(x[i, j], xij) + + @pytest.mark.parametrize('separate_r', [False, True]) + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_solve_toeplitz(self, separate_r, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + c = get_random((2, 3, 5), dtype=dtype, rng=rng) + r = get_random((2, 3, 5), dtype=dtype, rng=rng) + c_or_cr = (c, r) if separate_r else c + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.solve_toeplitz(c_or_cr, b) + for i in range(2): + for j in range(3): + bij = b if len(bdim) <= 2 else b[i, j] + c_or_cr_ij = (c[i, j], r[i, j]) if separate_r else c[i, j] + xij = linalg.solve_toeplitz(c_or_cr_ij, bij) + assert_allclose(x[i, j], xij) + + @pytest.mark.parametrize('separate_r', [False, True]) + @pytest.mark.parametrize('xdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_matmul_toeplitz(self, separate_r, xdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + c = get_random((2, 3, 5), dtype=dtype, rng=rng) + r = get_random((2, 3, 5), dtype=dtype, rng=rng) + c_or_cr = (c, r) if separate_r else c + x = get_random(xdim, dtype=dtype, rng=rng) + res = linalg.matmul_toeplitz(c_or_cr, x) + if separate_r: + ref = linalg.toeplitz(c, r) @ x + else: + ref = linalg.toeplitz(c) @ x + atol = 1e-6 if dtype in {np.float32, np.complex64} else 1e-12 + assert_allclose(res, ref, atol=atol) + + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_cho_solve(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_nearly_hermitian((2, 3, 5, 5), dtype=dtype, atol=0, rng=rng) + A = A + 5*np.eye(5) + c_and_lower = linalg.cho_factor(A) + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.cho_solve(c_and_lower, b) + if len(bdim) == 1: + x = x[..., np.newaxis] + b = b[..., np.newaxis] + assert_allclose(A @ x - b, 0, atol=1e-6) + assert_allclose(x, np.linalg.solve(A, b), atol=2e-6) + + @pytest.mark.parametrize('lower', [False, True]) + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_cho_solve_banded(self, lower, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 3, 5), dtype=dtype, rng=rng) + row_diag = 0 if lower else -1 + A[:, :, row_diag] = 10 + cb = linalg.cholesky_banded(A, lower=lower) + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.cho_solve_banded((cb, lower), b) + for i in range(2): + for j in range(3): + bij = b if len(bdim) <= 2 else b[i, j] + xij = linalg.cho_solve_banded((cb[i, j], lower), bij) + assert_allclose(x[i, j], xij) + + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_solveh_banded(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 3, 5), dtype=dtype, rng=rng) + A[:, :, -1] = 10 + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.solveh_banded(A, b) + for i in range(2): + for j in range(3): + bij = b if len(bdim) <= 2 else b[i, j] + xij = linalg.solveh_banded(A[i, j], bij) + assert_allclose(x[i, j], xij) + + @pytest.mark.parametrize('bdim', [(5,), (5, 4), (2, 3, 5, 4)]) + @pytest.mark.parametrize('dtype', floating) + def test_solve_triangular(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 5, 5), dtype=dtype, rng=rng) + A = np.tril(A) + b = get_random(bdim, dtype=dtype, rng=rng) + x = linalg.solve_triangular(A, b, lower=True) + if len(bdim) == 1: + x = x[..., np.newaxis] + b = b[..., np.newaxis] + atol = 1e-10 if dtype in (np.complex128, np.float64) else 2e-4 + assert_allclose(A @ x - b, 0, atol=atol) + assert_allclose(x, np.linalg.solve(A, b), atol=5*atol) + + @pytest.mark.parametrize('bdim', [(4,), (4, 3), (2, 3, 4, 3)]) + @pytest.mark.parametrize('dtype', floating) + def test_lstsq(self, bdim, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((2, 3, 4, 5), dtype=dtype, rng=rng) + b = get_random(bdim, dtype=dtype, rng=rng) + res = linalg.lstsq(A, b) + x = res[0] + if len(bdim) == 1: + x = x[..., np.newaxis] + b = b[..., np.newaxis] + assert_allclose(A @ x - b, 0, atol=2e-6) + assert len(res) == 4 + + @pytest.mark.parametrize('dtype', floating) + def test_clarkson_woodruff_transform(self, dtype): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 6), dtype=dtype, rng=rng) + self.batch_test(linalg.clarkson_woodruff_transform, A, + kwargs=dict(sketch_size=3, rng=311224)) + + def test_clarkson_woodruff_transform_sparse(self): + rng = np.random.default_rng(8342310302941288912051) + A = get_random((5, 3, 4, 6), dtype=np.float64, rng=rng) + A = sparse.coo_array(A) + message = "Batch support for sparse arrays is not available." + with pytest.raises(NotImplementedError, match=message): + linalg.clarkson_woodruff_transform(A, sketch_size=3, rng=rng) + + @pytest.mark.parametrize('f, args', [ + (linalg.toeplitz, (np.ones((0, 4)),)), + (linalg.eig, (np.ones((3, 0, 5, 5)),)), + ]) + def test_zero_size_batch(self, f, args): + message = "does not support zero-size batches." + with pytest.raises(ValueError, match=message): + f(*args) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_blas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_blas.py new file mode 100644 index 0000000000000000000000000000000000000000..62c157c0b42fb4e02f8eb3c094854705f336796e --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_blas.py @@ -0,0 +1,1037 @@ +# +# Created by: Pearu Peterson, April 2002 +# + +import math +import pytest +import numpy as np +from numpy.testing import (assert_equal, assert_almost_equal, + assert_array_almost_equal, assert_allclose) +from pytest import raises as assert_raises + +from numpy import (arange, triu, tril, zeros, tril_indices, ones, + diag, append, eye, nonzero) + +import scipy +from scipy.linalg import _fblas as fblas, get_blas_funcs, toeplitz, solve + +try: + from scipy.linalg import _cblas as cblas +except ImportError: + cblas = None + +REAL_DTYPES = [np.float32, np.float64] +COMPLEX_DTYPES = [np.complex64, np.complex128] +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +def test_get_blas_funcs(): + # check that it returns Fortran code for arrays that are + # fortran-ordered + f1, f2, f3 = get_blas_funcs( + ('axpy', 'axpy', 'axpy'), + (np.empty((2, 2), dtype=np.complex64, order='F'), + np.empty((2, 2), dtype=np.complex128, order='C')) + ) + + # get_blas_funcs will choose libraries depending on most generic + # array + assert_equal(f1.typecode, 'z') + assert_equal(f2.typecode, 'z') + if cblas is not None: + assert_equal(f1.module_name, 'cblas') + assert_equal(f2.module_name, 'cblas') + + # check defaults. + f1 = get_blas_funcs('rotg') + assert_equal(f1.typecode, 'd') + + # check also dtype interface + f1 = get_blas_funcs('gemm', dtype=np.complex64) + assert_equal(f1.typecode, 'c') + f1 = get_blas_funcs('gemm', dtype='F') + assert_equal(f1.typecode, 'c') + + # extended precision complex + f1 = get_blas_funcs('gemm', dtype=np.clongdouble) + assert_equal(f1.typecode, 'z') + + # check safe complex upcasting + f1 = get_blas_funcs('axpy', + (np.empty((2, 2), dtype=np.float64), + np.empty((2, 2), dtype=np.complex64)) + ) + assert_equal(f1.typecode, 'z') + + +def test_get_blas_funcs_alias(): + # check alias for get_blas_funcs + f, g = get_blas_funcs(('nrm2', 'dot'), dtype=np.complex64) + assert f.typecode == 'c' + assert g.typecode == 'c' + + f, g, h = get_blas_funcs(('dot', 'dotc', 'dotu'), dtype=np.float64) + assert f is g + assert f is h + + +def parametrize_blas(mod, func_name, prefixes): + if mod is None: + return pytest.mark.skip(reason="cblas not available") + params = [] + for prefix in prefixes: + if 'z' in prefix: + dtype = np.complex128 + elif 'c' in prefix: + dtype = np.complex64 + elif 'd' in prefix: + dtype = np.float64 + else: + assert 's' in prefix + dtype = np.float32 + + f = getattr(mod, prefix + func_name) + params.append(pytest.param(f, dtype, id=prefix + func_name)) + + return pytest.mark.parametrize("f,dtype", params) + + +class TestCBLAS1Simple: + @parametrize_blas(cblas, "axpy", "sdcz") + def test_axpy(self, f, dtype): + assert_array_almost_equal(f([1, 2, 3], [2, -1, 3], a=5), + [7, 9, 18]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f([1, 2j, 3], [2, -1, 3], a=5), + [7, 10j-1, 18]) + + +class TestFBLAS1Simple: + + @parametrize_blas(fblas, "axpy", "sdcz") + def test_axpy(self, f, dtype): + assert_array_almost_equal(f([1, 2, 3], [2, -1, 3], a=5), + [7, 9, 18]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f([1, 2j, 3], [2, -1, 3], a=5), + [7, 10j-1, 18]) + + @parametrize_blas(fblas, "copy", "sdcz") + def test_copy(self, f, dtype): + assert_array_almost_equal(f([3, 4, 5], [8]*3), [3, 4, 5]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f([3, 4j, 5+3j], [8]*3), [3, 4j, 5+3j]) + + @parametrize_blas(fblas, "asum", ["s", "d", "sc", "dz"]) + def test_asum(self, f, dtype): + assert_almost_equal(f([3, -4, 5]), 12) + if dtype in COMPLEX_DTYPES: + assert_almost_equal(f([3j, -4, 3-4j]), 14) + + @parametrize_blas(fblas, "dot", "sd") + def test_dot(self, f, dtype): + assert_almost_equal(f([3, -4, 5], [2, 5, 1]), -9) + + @parametrize_blas(fblas, "dotu", "cz") + def test_dotu(self, f, dtype): + assert_almost_equal(f([3j, -4, 3-4j], [2, 3, 1]), -9+2j) + + @parametrize_blas(fblas, "dotc", "cz") + def test_dotc(self, f, dtype): + assert_almost_equal(f([3j, -4, 3-4j], [2, 3j, 1]), 3-14j) + + @parametrize_blas(fblas, "nrm2", ["s", "d", "sc", "dz"]) + def test_nrm2(self, f, dtype): + assert_almost_equal(f([3, -4, 5]), math.sqrt(50)) + if dtype in COMPLEX_DTYPES: + assert_almost_equal(f([3j, -4, 3-4j]), math.sqrt(50)) + + @parametrize_blas(fblas, "scal", ["s", "d", "cs", "zd"]) + def test_scal(self, f, dtype): + assert_array_almost_equal(f(2, [3, -4, 5]), [6, -8, 10]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f(3, [3j, -4, 3-4j]), [9j, -12, 9-12j]) + + @parametrize_blas(fblas, "swap", "sdcz") + def test_swap(self, f, dtype): + x, y = [2, 3, 1], [-2, 3, 7] + x1, y1 = f(x, y) + assert_array_almost_equal(x1, y) + assert_array_almost_equal(y1, x) + + if dtype in COMPLEX_DTYPES: + x, y = [2, 3j, 1], [-2, 3, 7-3j] + x1, y1 = f(x, y) + assert_array_almost_equal(x1, y) + assert_array_almost_equal(y1, x) + + @parametrize_blas(fblas, "amax", ["is", "id", "ic", "iz"]) + def test_amax(self, f, dtype): + assert_equal(f([-2, 4, 3]), 1) + if dtype in COMPLEX_DTYPES: + assert_equal(f([-5, 4+3j, 6]), 1) + + # XXX: need tests for rot,rotm,rotg,rotmg + + +class TestFBLAS2Simple: + @parametrize_blas(fblas, "gemv", "sdcz") + def test_gemv(self, f, dtype): + assert_array_almost_equal(f(3, [[3]], [-4]), [-36]) + assert_array_almost_equal(f(3, [[3]], [-4], 3, [5]), [-21]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f(3j, [[3-4j]], [-4]), [-48-36j]) + assert_array_almost_equal(f(3j, [[3-4j]], [-4], 3, [5j]), + [-48-21j]) + + @parametrize_blas(fblas, "ger", "sd") + def test_ger(self, f, dtype): + assert_array_almost_equal(f(1, [1, 2], [3, 4]), [[3, 4], [6, 8]]) + assert_array_almost_equal(f(2, [1, 2, 3], [3, 4]), + [[6, 8], [12, 16], [18, 24]]) + assert_array_almost_equal(f(1, [1, 2], [3, 4], + a=[[1, 2], [3, 4]]), [[4, 6], [9, 12]]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f(1, [1j, 2], [3, 4]), + [[3j, 4j], [6, 8]]) + assert_array_almost_equal(f(2, [1j, 2j, 3j], [3j, 4j]), + [[6, 8], [12, 16], [18, 24]]) + + @parametrize_blas(fblas, "geru", "cz") + def test_geru(self, f, dtype): + assert_array_almost_equal(f(1, [1j, 2], [3, 4]), + [[3j, 4j], [6, 8]]) + assert_array_almost_equal(f(-2, [1j, 2j, 3j], [3j, 4j]), + [[6, 8], [12, 16], [18, 24]]) + + @parametrize_blas(fblas, "gerc", "cz") + def test_gerc(self, f, dtype): + assert_array_almost_equal(f(1, [1j, 2], [3, 4]), + [[3j, 4j], [6, 8]]) + assert_array_almost_equal(f(2, [1j, 2j, 3j], [3j, 4j]), + [[6, 8], [12, 16], [18, 24]]) + + @parametrize_blas(fblas, "syr", "sdcz") + def test_syr(self, f, dtype): + x = np.arange(1, 5, dtype='d') + resx = np.triu(x[:, np.newaxis] * x) + resx_reverse = np.triu(x[::-1, np.newaxis] * x[::-1]) + y = np.linspace(0, 8.5, 17, endpoint=False) + z = np.arange(1, 9, dtype='d').view('D') + resz = np.triu(z[:, np.newaxis] * z) + resz_reverse = np.triu(z[::-1, np.newaxis] * z[::-1]) + w = np.c_[np.zeros(4), z, np.zeros(4)].ravel() + + rtol = np.finfo(dtype).eps + + assert_allclose(f(1.0, x), resx, rtol=rtol) + assert_allclose(f(1.0, x, lower=True), resx.T, rtol=rtol) + assert_allclose(f(1.0, y, incx=2, offx=2, n=4), resx, rtol=rtol) + # negative increments imply reversed vectors in blas + assert_allclose(f(1.0, y, incx=-2, offx=2, n=4), + resx_reverse, rtol=rtol) + + if dtype in COMPLEX_DTYPES: + assert_allclose(f(1.0, z), resz, rtol=rtol) + assert_allclose(f(1.0, z, lower=True), resz.T, rtol=rtol) + assert_allclose(f(1.0, w, incx=3, offx=1, n=4), resz, rtol=rtol) + # negative increments imply reversed vectors in blas + assert_allclose(f(1.0, w, incx=-3, offx=1, n=4), + resz_reverse, rtol=rtol) + + a = np.zeros((4, 4), dtype, 'F') + b = f(1.0, z, a=a, overwrite_a=True) + assert_allclose(a, resz, rtol=rtol) + b = f(2.0, z, a=a) + assert a is not b + assert_allclose(b, 3*resz, rtol=rtol) + + else: + a = np.zeros((4, 4), dtype, 'F') + b = f(1.0, x, a=a, overwrite_a=True) + assert_allclose(a, resx, rtol=rtol) + b = f(2.0, x, a=a) + assert a is not b + assert_allclose(b, 3*resx, rtol=rtol) + + assert_raises(Exception, f, 1.0, x, incx=0) + assert_raises(Exception, f, 1.0, x, offx=5) + assert_raises(Exception, f, 1.0, x, offx=-2) + assert_raises(Exception, f, 1.0, x, n=-2) + assert_raises(Exception, f, 1.0, x, n=5) + assert_raises(Exception, f, 1.0, x, lower=2) + assert_raises(Exception, f, 1.0, x, a=np.zeros((2, 2), 'd', 'F')) + + @parametrize_blas(fblas, "her", "cz") + def test_her(self, f, dtype): + x = np.arange(1, 5, dtype='d') + z = np.arange(1, 9, dtype='d').view('D') + rehz = np.triu(z[:, np.newaxis] * z.conj()) + rehz_reverse = np.triu(z[::-1, np.newaxis] * z[::-1].conj()) + w = np.c_[np.zeros(4), z, np.zeros(4)].ravel() + + rtol = np.finfo(dtype).eps + + assert_allclose(f(1.0, z), rehz, rtol=rtol) + assert_allclose(f(1.0, z, lower=True), rehz.T.conj(), rtol=rtol) + assert_allclose(f(1.0, w, incx=3, offx=1, n=4), rehz, rtol=rtol) + # negative increments imply reversed vectors in blas + assert_allclose(f(1.0, w, incx=-3, offx=1, n=4), + rehz_reverse, rtol=rtol) + + a = np.zeros((4, 4), dtype, 'F') + b = f(1.0, z, a=a, overwrite_a=True) + assert_allclose(a, rehz, rtol=rtol) + + b = f(2.0, z, a=a) + assert a is not b + assert_allclose(b, 3*rehz, rtol=rtol) + + assert_raises(Exception, f, 1.0, x, incx=0) + assert_raises(Exception, f, 1.0, x, offx=5) + assert_raises(Exception, f, 1.0, x, offx=-2) + assert_raises(Exception, f, 1.0, x, n=-2) + assert_raises(Exception, f, 1.0, x, n=5) + assert_raises(Exception, f, 1.0, x, lower=2) + assert_raises(Exception, f, 1.0, x, a=np.zeros((2, 2), 'd', 'F')) + + @parametrize_blas(fblas, "syr2", "sd") + def test_syr2(self, f, dtype): + x = np.arange(1, 5, dtype='d') + y = np.arange(5, 9, dtype='d') + resxy = np.triu(x[:, np.newaxis] * y + y[:, np.newaxis] * x) + resxy_reverse = np.triu(x[::-1, np.newaxis] * y[::-1] + + y[::-1, np.newaxis] * x[::-1]) + + q = np.linspace(0, 8.5, 17, endpoint=False) + rtol = np.finfo(dtype).eps + + assert_allclose(f(1.0, x, y), resxy, rtol=rtol) + assert_allclose(f(1.0, x, y, n=3), resxy[:3, :3], rtol=rtol) + assert_allclose(f(1.0, x, y, lower=True), resxy.T, rtol=rtol) + + assert_allclose(f(1.0, q, q, incx=2, offx=2, incy=2, offy=10), + resxy, rtol=rtol) + assert_allclose(f(1.0, q, q, incx=2, offx=2, incy=2, offy=10, n=3), + resxy[:3, :3], rtol=rtol) + # negative increments imply reversed vectors in blas + assert_allclose(f(1.0, q, q, incx=-2, offx=2, incy=-2, offy=10), + resxy_reverse, rtol=rtol) + + a = np.zeros((4, 4), dtype, 'F') + b = f(1.0, x, y, a=a, overwrite_a=True) + assert_allclose(a, resxy, rtol=rtol) + + b = f(2.0, x, y, a=a) + assert a is not b + assert_allclose(b, 3*resxy, rtol=rtol) + + assert_raises(Exception, f, 1.0, x, y, incx=0) + assert_raises(Exception, f, 1.0, x, y, offx=5) + assert_raises(Exception, f, 1.0, x, y, offx=-2) + assert_raises(Exception, f, 1.0, x, y, incy=0) + assert_raises(Exception, f, 1.0, x, y, offy=5) + assert_raises(Exception, f, 1.0, x, y, offy=-2) + assert_raises(Exception, f, 1.0, x, y, n=-2) + assert_raises(Exception, f, 1.0, x, y, n=5) + assert_raises(Exception, f, 1.0, x, y, lower=2) + assert_raises(Exception, f, 1.0, x, y, a=np.zeros((2, 2), 'd', 'F')) + + @parametrize_blas(fblas, "her2", "cz") + def test_her2(self, f, dtype): + x = np.arange(1, 9, dtype='d').view('D') + y = np.arange(9, 17, dtype='d').view('D') + resxy = x[:, np.newaxis] * y.conj() + y[:, np.newaxis] * x.conj() + resxy = np.triu(resxy) + + resxy_reverse = x[::-1, np.newaxis] * y[::-1].conj() + resxy_reverse += y[::-1, np.newaxis] * x[::-1].conj() + resxy_reverse = np.triu(resxy_reverse) + + u = np.c_[np.zeros(4), x, np.zeros(4)].ravel() + v = np.c_[np.zeros(4), y, np.zeros(4)].ravel() + + rtol = np.finfo(dtype).eps + + assert_allclose(f(1.0, x, y), resxy, rtol=rtol) + assert_allclose(f(1.0, x, y, n=3), resxy[:3, :3], rtol=rtol) + assert_allclose(f(1.0, x, y, lower=True), resxy.T.conj(), + rtol=rtol) + + assert_allclose(f(1.0, u, v, incx=3, offx=1, incy=3, offy=1), + resxy, rtol=rtol) + assert_allclose(f(1.0, u, v, incx=3, offx=1, incy=3, offy=1, n=3), + resxy[:3, :3], rtol=rtol) + # negative increments imply reversed vectors in blas + assert_allclose(f(1.0, u, v, incx=-3, offx=1, incy=-3, offy=1), + resxy_reverse, rtol=rtol) + + a = np.zeros((4, 4), dtype, 'F') + b = f(1.0, x, y, a=a, overwrite_a=True) + assert_allclose(a, resxy, rtol=rtol) + + b = f(2.0, x, y, a=a) + assert a is not b + assert_allclose(b, 3*resxy, rtol=rtol) + + assert_raises(Exception, f, 1.0, x, y, incx=0) + assert_raises(Exception, f, 1.0, x, y, offx=5) + assert_raises(Exception, f, 1.0, x, y, offx=-2) + assert_raises(Exception, f, 1.0, x, y, incy=0) + assert_raises(Exception, f, 1.0, x, y, offy=5) + assert_raises(Exception, f, 1.0, x, y, offy=-2) + assert_raises(Exception, f, 1.0, x, y, n=-2) + assert_raises(Exception, f, 1.0, x, y, n=5) + assert_raises(Exception, f, 1.0, x, y, lower=2) + assert_raises(Exception, f, 1.0, x, y, + a=np.zeros((2, 2), 'd', 'F')) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_gbmv(self, dtype): + rng = np.random.default_rng(1234) + n = 7 + m = 5 + kl = 1 + ku = 2 + # fake a banded matrix via toeplitz + A = toeplitz(append(rng.random(kl+1), zeros(m-kl-1)), + append(rng.random(ku+1), zeros(n-ku-1))) + A = A.astype(dtype) + Ab = zeros((kl+ku+1, n), dtype=dtype) + + # Form the banded storage + Ab[2, :5] = A[0, 0] # diag + Ab[1, 1:6] = A[0, 1] # sup1 + Ab[0, 2:7] = A[0, 2] # sup2 + Ab[3, :4] = A[1, 0] # sub1 + + x = rng.random(n).astype(dtype) + y = rng.random(m).astype(dtype) + alpha, beta = dtype(3), dtype(-5) + + func, = get_blas_funcs(('gbmv',), dtype=dtype) + y1 = func(m=m, n=n, ku=ku, kl=kl, alpha=alpha, a=Ab, + x=x, y=y, beta=beta) + y2 = alpha * A.dot(x) + beta * y + assert_array_almost_equal(y1, y2) + + y1 = func(m=m, n=n, ku=ku, kl=kl, alpha=alpha, a=Ab, + x=y, y=x, beta=beta, trans=1) + y2 = alpha * A.T.dot(y) + beta * x + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_sbmv_hbmv(self, dtype): + rng = np.random.default_rng(1234) + n = 6 + k = 2 + A = zeros((n, n), dtype=dtype) + Ab = zeros((k+1, n), dtype=dtype) + + # Form the array and its packed banded storage + A[arange(n), arange(n)] = rng.random(n) + for ind2 in range(1, k+1): + temp = rng.random(n-ind2) + A[arange(n-ind2), arange(ind2, n)] = temp + Ab[-1-ind2, ind2:] = temp + A = A.astype(dtype) + if dtype in COMPLEX_DTYPES: + A += A.conj().T + func, = get_blas_funcs(('hbmv',), dtype=dtype) + else: + A += A.T + func, = get_blas_funcs(('sbmv',), dtype=dtype) + + Ab[-1, :] = diag(A) + x = rng.random(n).astype(dtype) + y = rng.random(n).astype(dtype) + alpha, beta = dtype(1.25), dtype(3) + + y1 = func(k=k, alpha=alpha, a=Ab, x=x, y=y, beta=beta) + y2 = alpha * A.dot(x) + beta * y + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("fname,dtype", [ + *[('spmv', dtype) for dtype in REAL_DTYPES + COMPLEX_DTYPES], + *[('hpmv', dtype) for dtype in COMPLEX_DTYPES], + ]) + def test_spmv_hpmv(self, fname, dtype): + rng = np.random.default_rng(1234) + n = 3 + A = rng.random((n, n)).astype(dtype) + if dtype in COMPLEX_DTYPES: + A += rng.random((n, n))*1j + A += A.T if fname == 'spmv' else A.conj().T + c, r = tril_indices(n) + Ap = A[r, c] + x = rng.random(n).astype(dtype) + y = rng.random(n).astype(dtype) + xlong = arange(2*n).astype(dtype) + ylong = ones(2*n).astype(dtype) + alpha, beta = dtype(1.25), dtype(2) + + func, = get_blas_funcs((fname,), dtype=dtype) + y1 = func(n=n, alpha=alpha, ap=Ap, x=x, y=y, beta=beta) + y2 = alpha * A.dot(x) + beta * y + assert_array_almost_equal(y1, y2) + + # Test inc and offsets + y1 = func(n=n-1, alpha=alpha, beta=beta, x=xlong, y=ylong, ap=Ap, + incx=2, incy=2, offx=n, offy=n) + y2 = (alpha * A[:-1, :-1]).dot(xlong[3::2]) + beta * ylong[3::2] + assert_array_almost_equal(y1[3::2], y2) + assert_almost_equal(y1[4], ylong[4]) + + @pytest.mark.parametrize("fname,dtype", [ + *[('spr', dtype) for dtype in REAL_DTYPES + COMPLEX_DTYPES], + *[('hpr', dtype) for dtype in COMPLEX_DTYPES], + ]) + def test_spr_hpr(self, fname, dtype): + rng = np.random.default_rng(1234) + n = 3 + A = rng.random((n, n)).astype(dtype) + if dtype in COMPLEX_DTYPES: + A += rng.random((n, n))*1j + A += A.T if fname == 'spr' else A.conj().T + c, r = tril_indices(n) + Ap = A[r, c] + x = rng.random(n).astype(dtype) + + alpha = np.finfo(dtype).dtype.type(2.5) + if fname == 'hpr': + func, = get_blas_funcs(('hpr',), dtype=dtype) + y2 = alpha * x[:, None].dot(x[None, :].conj()) + A + else: + func, = get_blas_funcs(('spr',), dtype=dtype) + y2 = alpha * x[:, None].dot(x[None, :]) + A + + y1 = func(n=n, alpha=alpha, ap=Ap, x=x) + y1f = zeros((3, 3), dtype=dtype) + y1f[r, c] = y1 + y1f[c, r] = y1.conj() if fname == 'hpr' else y1 + assert_array_almost_equal(y1f, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_spr2_hpr2(self, dtype): + rng = np.random.default_rng(1234) + n = 3 + A = rng.random((n, n)).astype(dtype) + if dtype in COMPLEX_DTYPES: + A += rng.random((n, n))*1j + A += A.conj().T + func, = get_blas_funcs(('hpr2',), dtype=dtype) + else: + A += A.T + func, = get_blas_funcs(('spr2',), dtype=dtype) + + c, r = tril_indices(n) + Ap = A[r, c] + x = rng.random(n).astype(dtype) + y = rng.random(n).astype(dtype) + alpha = dtype(2) + + u = alpha.conj() * x[:, None].dot(y[None, :].conj()) + y2 = A + u + u.conj().T + y1 = func(n=n, alpha=alpha, x=x, y=y, ap=Ap) + y1f = zeros((3, 3), dtype=dtype) + y1f[r, c] = y1 + y1f[[1, 2, 2], [0, 0, 1]] = y1[[1, 3, 4]].conj() + assert_array_almost_equal(y1f, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_tbmv(self, dtype): + rng = np.random.default_rng(1234) + n = 10 + k = 3 + x = rng.random(n).astype(dtype) + A = zeros((n, n), dtype=dtype) + # Banded upper triangular array + for sup in range(k+1): + A[arange(n-sup), arange(sup, n)] = rng.random(n-sup) + + # Add complex parts for c,z + if dtype in COMPLEX_DTYPES: + A[nonzero(A)] += 1j * rng.random((k+1)*n-(k*(k+1)//2)).astype(dtype) + + # Form the banded storage + Ab = zeros((k+1, n), dtype=dtype) + for row in range(k+1): + Ab[-row-1, row:] = diag(A, k=row) + func, = get_blas_funcs(('tbmv',), dtype=dtype) + + y1 = func(k=k, a=Ab, x=x) + y2 = A.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = A.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1, trans=1) + y2 = A.T.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1, trans=2) + y2 = A.conj().T.dot(x) + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_tbsv(self, dtype): + rng = np.random.default_rng(12345) + n = 6 + k = 3 + x = rng.random(n).astype(dtype) + A = zeros((n, n), dtype=dtype) + # Banded upper triangular array + for sup in range(k+1): + A[arange(n-sup), arange(sup, n)] = rng.random(n-sup) + + # Add complex parts for c,z + if dtype in COMPLEX_DTYPES: + A[nonzero(A)] += 1j * rng.random((k+1)*n-(k*(k+1)//2)).astype(dtype) + + # Form the banded storage + Ab = zeros((k+1, n), dtype=dtype) + for row in range(k+1): + Ab[-row-1, row:] = diag(A, k=row) + func, = get_blas_funcs(('tbsv',), dtype=dtype) + + y1 = func(k=k, a=Ab, x=x) + y2 = solve(A, x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = solve(A, x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1, trans=1) + y2 = solve(A.T, x) + assert_array_almost_equal(y1, y2) + + y1 = func(k=k, a=Ab, x=x, diag=1, trans=2) + y2 = solve(A.conj().T, x) + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_tpmv(self, dtype): + rng = np.random.default_rng(1234) + n = 10 + x = rng.random(n).astype(dtype) + # Upper triangular array + if dtype in COMPLEX_DTYPES: + A = triu(rng.random((n, n)) + rng.random((n, n))*1j) + else: + A = triu(rng.random((n, n))) + + # Form the packed storage + c, r = tril_indices(n) + Ap = A[r, c] + func, = get_blas_funcs(('tpmv',), dtype=dtype) + + y1 = func(n=n, ap=Ap, x=x) + y2 = A.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = A.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1, trans=1) + y2 = A.T.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1, trans=2) + y2 = A.conj().T.dot(x) + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_tpsv(self, dtype): + rng = np.random.default_rng(1234) + n = 10 + x = rng.random(n).astype(dtype) + # Upper triangular array + if dtype in COMPLEX_DTYPES: + A = triu(rng.random((n, n)) + rng.random((n, n))*1j) + else: + A = triu(rng.random((n, n))) + A += eye(n) + # Form the packed storage + c, r = tril_indices(n) + Ap = A[r, c] + func, = get_blas_funcs(('tpsv',), dtype=dtype) + + y1 = func(n=n, ap=Ap, x=x) + y2 = solve(A, x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = solve(A, x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1, trans=1) + y2 = solve(A.T, x) + assert_array_almost_equal(y1, y2) + + y1 = func(n=n, ap=Ap, x=x, diag=1, trans=2) + y2 = solve(A.conj().T, x) + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_trmv(self, dtype): + rng = np.random.default_rng(1234) + n = 3 + A = (rng.random((n, n))+eye(n)).astype(dtype) + x = rng.random(3).astype(dtype) + func, = get_blas_funcs(('trmv',), dtype=dtype) + + y1 = func(a=A, x=x) + y2 = triu(A).dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = triu(A).dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1, trans=1) + y2 = triu(A).T.dot(x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1, trans=2) + y2 = triu(A).conj().T.dot(x) + assert_array_almost_equal(y1, y2) + + @pytest.mark.parametrize("dtype", DTYPES) + def test_trsv(self, dtype): + rng = np.random.default_rng(1234) + n = 15 + A = (rng.random((n, n))+eye(n)).astype(dtype) + x = rng.random(n).astype(dtype) + func, = get_blas_funcs(('trsv',), dtype=dtype) + + y1 = func(a=A, x=x) + y2 = solve(triu(A), x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, lower=1) + y2 = solve(tril(A), x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1) + A[arange(n), arange(n)] = dtype(1) + y2 = solve(triu(A), x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1, trans=1) + y2 = solve(triu(A).T, x) + assert_array_almost_equal(y1, y2) + + y1 = func(a=A, x=x, diag=1, trans=2) + y2 = solve(triu(A).conj().T, x) + assert_array_almost_equal(y1, y2) + + +class TestFBLAS3Simple: + @parametrize_blas(fblas, "gemm", "sdcz") + def test_gemm(self, f, dtype): + assert_array_almost_equal(f(3, [3], [-4]), [[-36]]) + assert_array_almost_equal(f(3, [3], [-4], 3, [5]), [-21]) + if dtype in COMPLEX_DTYPES: + assert_array_almost_equal(f(3j, [3-4j], [-4]), [[-48-36j]]) + assert_array_almost_equal(f(3j, [3-4j], [-4], 3, [5j]), [-48-21j]) + + +class TestBLAS3Symm: + + def setup_method(self): + self.a = np.array([[1., 2.], + [0., 1.]]) + self.b = np.array([[1., 0., 3.], + [0., -1., 2.]]) + self.c = np.ones((2, 3)) + self.t = np.array([[2., -1., 8.], + [3., 0., 9.]]) + + @parametrize_blas(fblas, "symm", "sdcz") + def test_symm(self, f, dtype): + res = f(a=self.a, b=self.b, c=self.c, alpha=1., beta=1.) + assert_array_almost_equal(res, self.t) + + res = f(a=self.a.T, b=self.b, lower=1, c=self.c, alpha=1., beta=1.) + assert_array_almost_equal(res, self.t) + + res = f(a=self.a, b=self.b.T, side=1, c=self.c.T, + alpha=1., beta=1.) + assert_array_almost_equal(res, self.t.T) + + @parametrize_blas(fblas, "symm", "sdcz") + def test_symm_wrong_side(self, f, dtype): + """`side=1` means C <- B*A, hence shapes of A and B are to be + compatible. Otherwise, f2py exception is raised. + """ + # FIXME narrow down to _fblas.error + with pytest.raises(Exception): + f(a=self.a, b=self.b, alpha=1, side=1) + + @parametrize_blas(fblas, "symm", "sdcz") + def test_symm_wrong_uplo(self, f, dtype): + """SYMM only considers the upper/lower part of A. Hence setting + wrong value for `lower` (default is lower=0, meaning upper triangle) + gives a wrong result. + """ + res = f(a=self.a, b=self.b, c=self.c, alpha=1., beta=1.) + assert np.allclose(res, self.t) + res = f(a=self.a, b=self.b, lower=1, c=self.c, alpha=1., beta=1.) + assert not np.allclose(res, self.t) + + +class TestBLAS3Syrk: + def setup_method(self): + self.a = np.array([[1., 0.], + [0., -2.], + [2., 3.]]) + self.t = np.array([[1., 0., 2.], + [0., 4., -6.], + [2., -6., 13.]]) + self.tt = np.array([[5., 6.], + [6., 13.]]) + + @parametrize_blas(fblas, "syrk", "sdcz") + def test_syrk(self, f, dtype): + c = f(a=self.a, alpha=1.) + assert_array_almost_equal(np.triu(c), np.triu(self.t)) + + c = f(a=self.a, alpha=1., lower=1) + assert_array_almost_equal(np.tril(c), np.tril(self.t)) + + c0 = np.ones(self.t.shape) + c = f(a=self.a, alpha=1., beta=1., c=c0) + assert_array_almost_equal(np.triu(c), np.triu(self.t+c0)) + + c = f(a=self.a, alpha=1., trans=1) + assert_array_almost_equal(np.triu(c), np.triu(self.tt)) + + # prints '0-th dimension must be fixed to 3 but got 5', + # FIXME: suppress? + @parametrize_blas(fblas, "syrk", "sdcz") + def test_syrk_wrong_c(self, f, dtype): + # FIXME narrow down to _fblas.error + with pytest.raises(Exception): + f(a=self.a, alpha=1., c=np.ones((5, 8))) + # if C is supplied, it must have compatible dimensions + + +class TestBLAS3Syr2k: + def setup_method(self): + self.a = np.array([[1., 0.], + [0., -2.], + [2., 3.]]) + self.b = np.array([[0., 1.], + [1., 0.], + [0, 1.]]) + self.t = np.array([[0., -1., 3.], + [-1., 0., 0.], + [3., 0., 6.]]) + self.tt = np.array([[0., 1.], + [1., 6]]) + + @parametrize_blas(fblas, "syr2k", "sdcz") + def test_syr2k(self, f, dtype): + c = f(a=self.a, b=self.b, alpha=1.) + assert_array_almost_equal(np.triu(c), np.triu(self.t)) + + c = f(a=self.a, b=self.b, alpha=1., lower=1) + assert_array_almost_equal(np.tril(c), np.tril(self.t)) + + c0 = np.ones(self.t.shape) + c = f(a=self.a, b=self.b, alpha=1., beta=1., c=c0) + assert_array_almost_equal(np.triu(c), np.triu(self.t+c0)) + + c = f(a=self.a, b=self.b, alpha=1., trans=1) + assert_array_almost_equal(np.triu(c), np.triu(self.tt)) + + # prints '0-th dimension must be fixed to 3 but got 5', FIXME: suppress? + @parametrize_blas(fblas, "syr2k", "sdcz") + def test_syr2k_wrong_c(self, f, dtype): + with pytest.raises(Exception): + f(a=self.a, b=self.b, alpha=1., c=np.zeros((15, 8))) + # if C is supplied, it must have compatible dimensions + + +class TestSyHe: + """Quick and simple tests for (zc)-symm, syrk, syr2k.""" + + def setup_method(self): + self.sigma_y = np.array([[0., -1.j], + [1.j, 0.]]) + + @parametrize_blas(fblas, "symm", "zc") + def test_symm(self, f, dtype): + # NB: a is symmetric w/upper diag of ONLY + res = f(a=self.sigma_y, b=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), np.diag([1, -1])) + + @parametrize_blas(fblas, "hemm", "zc") + def test_hemm(self, f, dtype): + # NB: a is hermitian w/upper diag of ONLY + res = f(a=self.sigma_y, b=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), np.diag([1, 1])) + + @parametrize_blas(fblas, "syrk", "zc") + def test_syrk(self, f, dtype): + res = f(a=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), np.diag([-1, -1])) + + @parametrize_blas(fblas, "herk", "zc") + def test_herk(self, f, dtype): + res = f(a=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), np.diag([1, 1])) + + @parametrize_blas(fblas, "syr2k", "zc") + def test_syr2k_zr(self, f, dtype): + res = f(a=self.sigma_y, b=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), 2.*np.diag([-1, -1])) + + @parametrize_blas(fblas, "her2k", "zc") + def test_her2k_zr(self, f, dtype): + res = f(a=self.sigma_y, b=self.sigma_y, alpha=1.) + assert_array_almost_equal(np.triu(res), 2.*np.diag([1, 1])) + + +class TestTRMM: + """Quick and simple tests for *trmm.""" + + def setup_method(self): + self.a = np.array([[1., 2., ], + [-2., 1.]]) + self.b = np.array([[3., 4., -1.], + [5., 6., -2.]]) + + self.a2 = np.array([[1, 1, 2, 3], + [0, 1, 4, 5], + [0, 0, 1, 6], + [0, 0, 0, 1]], order="f") + self.b2 = np.array([[1, 4], [2, 5], [3, 6], [7, 8], [9, 10]], + order="f") + + @pytest.mark.parametrize("dtype", DTYPES) + def test_side(self, dtype): + trmm = get_blas_funcs("trmm", dtype=dtype) + # Provide large A array that works for side=1 but not 0 (see gh-10841) + assert_raises(Exception, trmm, 1.0, self.a2, self.b2) + res = trmm(1.0, self.a2.astype(dtype), self.b2.astype(dtype), + side=1) + k = self.b2.shape[1] + assert_allclose(res, self.b2 @ self.a2[:k, :k], rtol=0., + atol=100*np.finfo(dtype).eps) + + @parametrize_blas(fblas, "trmm", "sdcz") + def test_ab(self, f, dtype): + result = f(1., self.a, self.b) + # default a is upper triangular + expected = np.array([[13., 16., -5.], + [ 5., 6., -2.]]) + assert_array_almost_equal(result, expected) + + @parametrize_blas(fblas, "trmm", "sdcz") + def test_ab_lower(self, f, dtype): + result = f(1., self.a, self.b, lower=True) + expected = np.array([[ 3., 4., -1.], + [-1., -2., 0.]]) # now a is lower triangular + assert_array_almost_equal(result, expected) + + @parametrize_blas(fblas, "trmm", "sdcz") + def test_b_overwrites(self, f, dtype): + # BLAS *trmm modifies B argument in-place. + # Here the default is to copy, but this can be overridden + b = self.b.astype(dtype) + for overwr in [True, False]: + bcopy = b.copy() + result = f(1., self.a, bcopy, overwrite_b=overwr) + # C-contiguous arrays are copied + assert not bcopy.flags.f_contiguous + assert not np.may_share_memory(bcopy, result) + assert_equal(bcopy, b) + + bcopy = np.asfortranarray(b.copy()) # or just transpose it + result = f(1., self.a, bcopy, overwrite_b=True) + assert bcopy.flags.f_contiguous + assert np.may_share_memory(bcopy, result) + assert_array_almost_equal(bcopy, result) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_trsm(dtype): + rng = np.random.default_rng(1234) + tol = np.finfo(dtype).eps*1000 + func, = get_blas_funcs(('trsm',), dtype=dtype) + + # Test protection against size mismatches + A = rng.random((4, 5)).astype(dtype) + B = rng.random((4, 4)).astype(dtype) + alpha = dtype(1) + assert_raises(Exception, func, alpha, A, B) + assert_raises(Exception, func, alpha, A.T, B) + + n = 8 + m = 7 + alpha = dtype(-2.5) + if dtype in COMPLEX_DTYPES: + A = (rng.random((m, m)) + rng.random((m, m))*1j) + eye(m) + else: + A = rng.random((m, m)) + eye(m) + A = A.astype(dtype) + Au = triu(A) + Al = tril(A) + B1 = rng.random((m, n)).astype(dtype) + B2 = rng.random((n, m)).astype(dtype) + + x1 = func(alpha=alpha, a=A, b=B1) + assert_equal(B1.shape, x1.shape) + x2 = solve(Au, alpha*B1) + assert_allclose(x1, x2, atol=tol) + + x1 = func(alpha=alpha, a=A, b=B1, trans_a=1) + x2 = solve(Au.T, alpha*B1) + assert_allclose(x1, x2, atol=tol) + + x1 = func(alpha=alpha, a=A, b=B1, trans_a=2) + x2 = solve(Au.conj().T, alpha*B1) + assert_allclose(x1, x2, atol=tol) + + x1 = func(alpha=alpha, a=A, b=B1, diag=1) + Au[arange(m), arange(m)] = dtype(1) + x2 = solve(Au, alpha*B1) + assert_allclose(x1, x2, atol=tol) + + x1 = func(alpha=alpha, a=A, b=B2, diag=1, side=1) + x2 = solve(Au.conj().T, alpha*B2.conj().T) + assert_allclose(x1, x2.conj().T, atol=tol) + + x1 = func(alpha=alpha, a=A, b=B2, diag=1, side=1, lower=1) + Al[arange(m), arange(m)] = dtype(1) + x2 = solve(Al.conj().T, alpha*B2.conj().T) + assert_allclose(x1, x2.conj().T, atol=tol) + + +@pytest.mark.xfail(run=False, + reason="gh-16930") +def test_gh_169309(): + x = np.repeat(10, 9) + actual = scipy.linalg.blas.dnrm2(x, 5, 3, -1) + expected = math.sqrt(500) + assert_allclose(actual, expected) + + +def test_dnrm2_neg_incx(): + # check that dnrm2(..., incx < 0) raises + # XXX: remove the test after the lowest supported BLAS implements + # negative incx (new in LAPACK 3.10) + x = np.repeat(10, 9) + incx = -1 + with assert_raises(fblas.__fblas_error): + scipy.linalg.blas.dnrm2(x, 5, 3, incx) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_blas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_blas.py new file mode 100644 index 0000000000000000000000000000000000000000..284e214d38ed331cf0493d1e3bba6e1214939b2c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_blas.py @@ -0,0 +1,118 @@ +import numpy as np +from numpy.testing import (assert_allclose, + assert_equal) +import scipy.linalg.cython_blas as blas + +class TestDGEMM: + + def test_transposes(self): + + a = np.arange(12, dtype='d').reshape((3, 4))[:2,:2] + b = np.arange(1, 13, dtype='d').reshape((4, 3))[:2,:2] + c = np.empty((2, 4))[:2,:2] + + blas._test_dgemm(1., a, b, 0., c) + assert_allclose(c, a.dot(b)) + + blas._test_dgemm(1., a.T, b, 0., c) + assert_allclose(c, a.T.dot(b)) + + blas._test_dgemm(1., a, b.T, 0., c) + assert_allclose(c, a.dot(b.T)) + + blas._test_dgemm(1., a.T, b.T, 0., c) + assert_allclose(c, a.T.dot(b.T)) + + blas._test_dgemm(1., a, b, 0., c.T) + assert_allclose(c, a.dot(b).T) + + blas._test_dgemm(1., a.T, b, 0., c.T) + assert_allclose(c, a.T.dot(b).T) + + blas._test_dgemm(1., a, b.T, 0., c.T) + assert_allclose(c, a.dot(b.T).T) + + blas._test_dgemm(1., a.T, b.T, 0., c.T) + assert_allclose(c, a.T.dot(b.T).T) + + def test_shapes(self): + a = np.arange(6, dtype='d').reshape((3, 2)) + b = np.arange(-6, 2, dtype='d').reshape((2, 4)) + c = np.empty((3, 4)) + + blas._test_dgemm(1., a, b, 0., c) + assert_allclose(c, a.dot(b)) + + blas._test_dgemm(1., b.T, a.T, 0., c.T) + assert_allclose(c, b.T.dot(a.T).T) + +class TestWfuncPointers: + """ Test the function pointers that are expected to fail on + Mac OS X without the additional entry statement in their definitions + in fblas_l1.pyf.src. """ + + def test_complex_args(self): + + cx = np.array([.5 + 1.j, .25 - .375j, 12.5 - 4.j], np.complex64) + cy = np.array([.8 + 2.j, .875 - .625j, -1. + 2.j], np.complex64) + + assert_allclose(blas._test_cdotc(cx, cy), + -17.6468753815+21.3718757629j) + assert_allclose(blas._test_cdotu(cx, cy), + -6.11562538147+30.3156242371j) + + assert_equal(blas._test_icamax(cx), 3) + + assert_allclose(blas._test_scasum(cx), 18.625) + assert_allclose(blas._test_scnrm2(cx), 13.1796483994) + + assert_allclose(blas._test_cdotc(cx[::2], cy[::2]), + -18.1000003815+21.2000007629j) + assert_allclose(blas._test_cdotu(cx[::2], cy[::2]), + -6.10000038147+30.7999992371j) + assert_allclose(blas._test_scasum(cx[::2]), 18.) + assert_allclose(blas._test_scnrm2(cx[::2]), 13.1719398499) + + def test_double_args(self): + + x = np.array([5., -3, -.5], np.float64) + y = np.array([2, 1, .5], np.float64) + + assert_allclose(blas._test_dasum(x), 8.5) + assert_allclose(blas._test_ddot(x, y), 6.75) + assert_allclose(blas._test_dnrm2(x), 5.85234975815) + + assert_allclose(blas._test_dasum(x[::2]), 5.5) + assert_allclose(blas._test_ddot(x[::2], y[::2]), 9.75) + assert_allclose(blas._test_dnrm2(x[::2]), 5.0249376297) + + assert_equal(blas._test_idamax(x), 1) + + def test_float_args(self): + + x = np.array([5., -3, -.5], np.float32) + y = np.array([2, 1, .5], np.float32) + + assert_equal(blas._test_isamax(x), 1) + + assert_allclose(blas._test_sasum(x), 8.5) + assert_allclose(blas._test_sdot(x, y), 6.75) + assert_allclose(blas._test_snrm2(x), 5.85234975815) + + assert_allclose(blas._test_sasum(x[::2]), 5.5) + assert_allclose(blas._test_sdot(x[::2], y[::2]), 9.75) + assert_allclose(blas._test_snrm2(x[::2]), 5.0249376297) + + def test_double_complex_args(self): + + cx = np.array([.5 + 1.j, .25 - .375j, 13. - 4.j], np.complex128) + cy = np.array([.875 + 2.j, .875 - .625j, -1. + 2.j], np.complex128) + + assert_equal(blas._test_izamax(cx), 3) + + assert_allclose(blas._test_zdotc(cx, cy), -18.109375+22.296875j) + assert_allclose(blas._test_zdotu(cx, cy), -6.578125+31.390625j) + + assert_allclose(blas._test_zdotc(cx[::2], cy[::2]), -18.5625+22.125j) + assert_allclose(blas._test_zdotu(cx[::2], cy[::2]), -6.5625+31.875j) + diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_lapack.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_lapack.py new file mode 100644 index 0000000000000000000000000000000000000000..2a4e7b34b62042efdb0ce0f8ee61ce0189320995 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cython_lapack.py @@ -0,0 +1,22 @@ +from numpy.testing import assert_allclose +from scipy.linalg import cython_lapack as cython_lapack +from scipy.linalg import lapack + + +class TestLamch: + + def test_slamch(self): + for c in [b'e', b's', b'b', b'p', b'n', b'r', b'm', b'u', b'l', b'o']: + assert_allclose(cython_lapack._test_slamch(c), + lapack.slamch(c)) + + def test_dlamch(self): + for c in [b'e', b's', b'b', b'p', b'n', b'r', b'm', b'u', b'l', b'o']: + assert_allclose(cython_lapack._test_dlamch(c), + lapack.dlamch(c)) + + def test_complex_ladiv(self): + cx = .5 + 1.j + cy = .875 + 2.j + assert_allclose(cython_lapack._test_zladiv(cy, cx), 1.95+0.1j) + assert_allclose(cython_lapack._test_cladiv(cy, cx), 1.95+0.1j) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cythonized_array_utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cythonized_array_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d7bbefe345fefba0e610b16c06ca1b43ce104bf2 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_cythonized_array_utils.py @@ -0,0 +1,134 @@ +import numpy as np +from scipy.linalg import bandwidth, issymmetric, ishermitian +from scipy.conftest import skip_xp_invalid_arg +import pytest +from pytest import raises + + +@skip_xp_invalid_arg +def test_bandwidth_dtypes(): + n = 5 + for t in np.typecodes['All']: + A = np.zeros([n, n], dtype=t) + if t in 'eUVOMm': + raises(TypeError, bandwidth, A) + elif t == 'G': # No-op test. On win these pass on others fail. + pass + else: + _ = bandwidth(A) + + +def test_bandwidth_non2d_input(): + A = np.array([1, 2, 3]) + raises(ValueError, bandwidth, A) + + +@pytest.mark.parametrize('T', [x for x in np.typecodes['All'] + if x not in 'eGUVOMmS']) +def test_bandwidth_square_inputs(T): + n = 20 + k = 4 + R = np.zeros([n, n], dtype=T, order='F') + # form a banded matrix inplace + R[[x for x in range(n)], [x for x in range(n)]] = 1 + R[[x for x in range(n-k)], [x for x in range(k, n)]] = 1 + R[[x for x in range(1, n)], [x for x in range(n-1)]] = 1 + R[[x for x in range(k, n)], [x for x in range(n-k)]] = 1 + assert bandwidth(R) == (k, k) + A = np.array([ + [1, 1, 0, 0, 0, 0, 0, 0], + [1, 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, 1, 1], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + ]) + assert bandwidth(A) == (2, 2) + + +@skip_xp_invalid_arg +@pytest.mark.parametrize('T', [x for x in np.typecodes['All'] + if x not in 'eGUVOMm']) +def test_bandwidth_rect_inputs(T): + n, m = 10, 20 + k = 5 + R = np.zeros([n, m], dtype=T, order='F') + # form a banded matrix inplace + R[[x for x in range(n)], [x for x in range(n)]] = 1 + R[[x for x in range(n-k)], [x for x in range(k, n)]] = 1 + R[[x for x in range(1, n)], [x for x in range(n-1)]] = 1 + R[[x for x in range(k, n)], [x for x in range(n-k)]] = 1 + assert bandwidth(R) == (k, k) + + +@skip_xp_invalid_arg +def test_issymetric_ishermitian_dtypes(): + n = 5 + for t in np.typecodes['All']: + A = np.zeros([n, n], dtype=t) + if t in 'eUVOMm': + raises(TypeError, issymmetric, A) + raises(TypeError, ishermitian, A) + elif t == 'G': # No-op test. On win these pass on others fail. + pass + else: + assert issymmetric(A) + assert ishermitian(A) + + +def test_issymmetric_ishermitian_invalid_input(): + A = np.array([1, 2, 3]) + raises(ValueError, issymmetric, A) + raises(ValueError, ishermitian, A) + A = np.array([[[1, 2, 3], [4, 5, 6]]]) + raises(ValueError, issymmetric, A) + raises(ValueError, ishermitian, A) + A = np.array([[1, 2, 3], [4, 5, 6]]) + raises(ValueError, issymmetric, A) + raises(ValueError, ishermitian, A) + + +def test_issymetric_complex_decimals(): + A = np.arange(1, 10).astype(complex).reshape(3, 3) + A += np.arange(-4, 5).astype(complex).reshape(3, 3)*1j + # make entries decimal + A /= np.pi + A = A + A.T + assert issymmetric(A) + + +def test_ishermitian_complex_decimals(): + A = np.arange(1, 10).astype(complex).reshape(3, 3) + A += np.arange(-4, 5).astype(complex).reshape(3, 3)*1j + # make entries decimal + A /= np.pi + A = A + A.T.conj() + assert ishermitian(A) + + +def test_issymmetric_approximate_results(): + n = 20 + rng = np.random.RandomState(123456789) + x = rng.uniform(high=5., size=[n, n]) + y = x @ x.T # symmetric + p = rng.standard_normal([n, n]) + z = p @ y @ p.T + assert issymmetric(z, atol=1e-10) + assert issymmetric(z, atol=1e-10, rtol=0.) + assert issymmetric(z, atol=0., rtol=1e-12) + assert issymmetric(z, atol=1e-13, rtol=1e-12) + + +def test_ishermitian_approximate_results(): + n = 20 + rng = np.random.RandomState(987654321) + x = rng.uniform(high=5., size=[n, n]) + y = x @ x.T # symmetric + p = rng.standard_normal([n, n]) + rng.standard_normal([n, n])*1j + z = p @ y @ p.conj().T + assert ishermitian(z, atol=1e-10) + assert ishermitian(z, atol=1e-10, rtol=0.) + assert ishermitian(z, atol=0., rtol=1e-12) + assert ishermitian(z, atol=1e-13, rtol=1e-12) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp.py new file mode 100644 index 0000000000000000000000000000000000000000..31080a6d1c1520d4fce7c09f866418fa4adb236b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp.py @@ -0,0 +1,3189 @@ +import itertools +import platform +import sys +import warnings + +import numpy as np +from numpy.testing import (assert_equal, assert_almost_equal, + assert_array_almost_equal, assert_array_equal, + assert_, assert_allclose) + +import pytest +from pytest import raises as assert_raises + +from scipy.linalg import (eig, eigvals, lu, svd, svdvals, cholesky, qr, + schur, rsf2csf, lu_solve, lu_factor, solve, diagsvd, + hessenberg, rq, eig_banded, eigvals_banded, eigh, + eigvalsh, qr_multiply, qz, orth, ordqz, + subspace_angles, hadamard, eigvalsh_tridiagonal, + eigh_tridiagonal, null_space, cdf2rdf, LinAlgError) + +from scipy.linalg.lapack import (dgbtrf, dgbtrs, zgbtrf, zgbtrs, dsbev, + dsbevd, dsbevx, zhbevd, zhbevx) + +from scipy.linalg._misc import norm +from scipy.linalg._decomp_qz import _select_function +from scipy.stats import ortho_group + +from numpy import (array, diag, full, linalg, argsort, zeros, arange, + float32, complex64, ravel, sqrt, iscomplex, shape, sort, + sign, asarray, isfinite, ndarray, eye,) + +from scipy.linalg._testutils import assert_no_overwrite +from scipy.sparse._sputils import matrix + +from scipy._lib._testutils import check_free_memory +from scipy.linalg.blas import HAS_ILP64 +from scipy.conftest import skip_xp_invalid_arg +from scipy.__config__ import CONFIG + +IS_WASM = (sys.platform == "emscripten" or platform.machine() in ["wasm32", "wasm64"]) + + +def _random_hermitian_matrix(n, posdef=False, dtype=float): + "Generate random sym/hermitian array of the given size n" + # FIXME non-deterministic rng + if dtype in COMPLEX_DTYPES: + A = np.random.rand(n, n) + np.random.rand(n, n)*1.0j + A = (A + A.conj().T)/2 + else: + A = np.random.rand(n, n) + A = (A + A.T)/2 + + if posdef: + A += sqrt(2*n)*np.eye(n) + + return A.astype(dtype) + + +REAL_DTYPES = [np.float32, np.float64] +COMPLEX_DTYPES = [np.complex64, np.complex128] +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +# XXX: This function should not be defined here, but somewhere in +# scipy.linalg namespace +def symrand(dim_or_eigv, rng): + """Return a random symmetric (Hermitian) matrix. + + If 'dim_or_eigv' is an integer N, return a NxN matrix, with eigenvalues + uniformly distributed on (-1,1). + + If 'dim_or_eigv' is 1-D real array 'a', return a matrix whose + eigenvalues are 'a'. + """ + if isinstance(dim_or_eigv, int): + dim = dim_or_eigv + d = rng.random(dim)*2 - 1 + elif (isinstance(dim_or_eigv, ndarray) and + len(dim_or_eigv.shape) == 1): + dim = dim_or_eigv.shape[0] + d = dim_or_eigv + else: + raise TypeError("input type not supported.") + + v = ortho_group.rvs(dim) + h = v.T.conj() @ diag(d) @ v + # to avoid roundoff errors, symmetrize the matrix (again) + h = 0.5*(h.T+h) + return h + + +class TestEigVals: + + def test_simple(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + w = eigvals(a) + exact_w = [(9+sqrt(93))/2, 0, (9-sqrt(93))/2] + assert_array_almost_equal(w, exact_w) + + def test_simple_tr(self): + a = array([[1, 2, 3], [1, 2, 3], [2, 5, 6]], 'd').T + a = a.copy() + a = a.T + w = eigvals(a) + exact_w = [(9+sqrt(93))/2, 0, (9-sqrt(93))/2] + assert_array_almost_equal(w, exact_w) + + def test_simple_complex(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6+1j]] + w = eigvals(a) + exact_w = [(9+1j+sqrt(92+6j))/2, + 0, + (9+1j-sqrt(92+6j))/2] + assert_array_almost_equal(w, exact_w) + + def test_finite(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + w = eigvals(a, check_finite=False) + exact_w = [(9+sqrt(93))/2, 0, (9-sqrt(93))/2] + assert_array_almost_equal(w, exact_w) + + @pytest.mark.parametrize('dt', [int, float, float32, complex, complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + w = eigvals(a) + assert w.shape == (0,) + assert w.dtype == eigvals(np.eye(2, dtype=dt)).dtype + + w = eigvals(a, homogeneous_eigvals=True) + assert w.shape == (2, 0) + assert w.dtype == eigvals(np.eye(2, dtype=dt)).dtype + + +class TestEig: + + def test_simple(self): + a = array([[1, 2, 3], [1, 2, 3], [2, 5, 6]]) + w, v = eig(a) + exact_w = [(9+sqrt(93))/2, 0, (9-sqrt(93))/2] + v0 = array([1, 1, (1+sqrt(93)/3)/2]) + v1 = array([3., 0, -1]) + v2 = array([1, 1, (1-sqrt(93)/3)/2]) + v0 = v0 / norm(v0) + v1 = v1 / norm(v1) + v2 = v2 / norm(v2) + assert_array_almost_equal(w, exact_w) + assert_array_almost_equal(v0, v[:, 0]*sign(v[0, 0])) + assert_array_almost_equal(v1, v[:, 1]*sign(v[0, 1])) + assert_array_almost_equal(v2, v[:, 2]*sign(v[0, 2])) + for i in range(3): + assert_array_almost_equal(a @ v[:, i], w[i]*v[:, i]) + w, v = eig(a, left=1, right=0) + for i in range(3): + assert_array_almost_equal(a.T @ v[:, i], w[i]*v[:, i]) + + def test_simple_complex_eig(self): + a = array([[1, 2], [-2, 1]]) + w, vl, vr = eig(a, left=1, right=1) + assert_array_almost_equal(w, array([1+2j, 1-2j])) + for i in range(2): + assert_array_almost_equal(a @ vr[:, i], w[i]*vr[:, i]) + for i in range(2): + assert_array_almost_equal(a.conj().T @ vl[:, i], + w[i].conj()*vl[:, i]) + + def test_simple_complex(self): + a = array([[1, 2, 3], [1, 2, 3], [2, 5, 6+1j]]) + w, vl, vr = eig(a, left=1, right=1) + for i in range(3): + assert_array_almost_equal(a @ vr[:, i], w[i]*vr[:, i]) + for i in range(3): + assert_array_almost_equal(a.conj().T @ vl[:, i], + w[i].conj()*vl[:, i]) + + def test_gh_3054(self): + a = [[1]] + b = [[0]] + w, vr = eig(a, b, homogeneous_eigvals=True) + assert_allclose(w[1, 0], 0) + assert_(w[0, 0] != 0) + assert_allclose(vr, 1) + + w, vr = eig(a, b) + assert_equal(w, np.inf) + assert_allclose(vr, 1) + + def _check_gen_eig(self, A, B, atol_homog=1e-13, rtol_homog=1e-13, + atol=1e-13, rtol=1e-13): + if B is not None: + A, B = asarray(A), asarray(B) + B0 = B + else: + A = asarray(A) + B0 = B + B = np.eye(*A.shape) + msg = f"\n{A!r}\n{B!r}" + + # Eigenvalues in homogeneous coordinates + w, vr = eig(A, B0, homogeneous_eigvals=True) + wt = eigvals(A, B0, homogeneous_eigvals=True) + val1 = A @ vr * w[1, :] + val2 = B @ vr * w[0, :] + for i in range(val1.shape[1]): + assert_allclose(val1[:, i], val2[:, i], + rtol=rtol_homog, atol=atol_homog, err_msg=msg) + + if B0 is None: + assert_allclose(w[1, :], 1) + assert_allclose(wt[1, :], 1) + + perm = np.lexsort(w) + permt = np.lexsort(wt) + assert_allclose(w[:, perm], wt[:, permt], atol=1e-7, rtol=1e-7, + err_msg=msg) + + length = np.empty(len(vr)) + + for i in range(len(vr)): + length[i] = norm(vr[:, i]) + + assert_allclose(length, np.ones(length.size), err_msg=msg, + atol=1e-7, rtol=1e-7) + + # Convert homogeneous coordinates + beta_nonzero = (w[1, :] != 0) + wh = w[0, beta_nonzero] / w[1, beta_nonzero] + + # Eigenvalues in standard coordinates + w, vr = eig(A, B0) + wt = eigvals(A, B0) + val1 = A @ vr + val2 = B @ vr * w + res = val1 - val2 + for i in range(res.shape[1]): + if np.all(isfinite(res[:, i])): + assert_allclose(res[:, i], 0, + rtol=rtol, atol=atol, err_msg=msg) + + # try to consistently order eigenvalues, including complex conjugate pairs + w_fin = w[isfinite(w)] + wt_fin = wt[isfinite(wt)] + + # prune noise in the real parts + w_fin = -1j * np.real_if_close(1j*w_fin, tol=1e-10) + wt_fin = -1j * np.real_if_close(1j*wt_fin, tol=1e-10) + + perm = argsort(abs(w_fin) + w_fin.imag) + permt = argsort(abs(wt_fin) + wt_fin.imag) + + assert_allclose(w_fin[perm], wt_fin[permt], + atol=1e-7, rtol=1e-7, err_msg=msg) + + length = np.empty(len(vr)) + for i in range(len(vr)): + length[i] = norm(vr[:, i]) + assert_allclose(length, np.ones(length.size), err_msg=msg) + + # Compare homogeneous and nonhomogeneous versions + assert_allclose(sort(wh), sort(w[np.isfinite(w)])) + + def test_singular(self): + # Example taken from + # https://web.archive.org/web/20040903121217/http://www.cs.umu.se/research/nla/singular_pairs/guptri/matlab.html + A = array([[22, 34, 31, 31, 17], + [45, 45, 42, 19, 29], + [39, 47, 49, 26, 34], + [27, 31, 26, 21, 15], + [38, 44, 44, 24, 30]]) + B = array([[13, 26, 25, 17, 24], + [31, 46, 40, 26, 37], + [26, 40, 19, 25, 25], + [16, 25, 27, 14, 23], + [24, 35, 18, 21, 22]]) + + with np.errstate(all='ignore'): + self._check_gen_eig(A, B, atol_homog=5e-13, atol=5e-13) + + def test_falker(self): + # Test matrices giving some Nan generalized eigenvalues. + M = diag(array([1, 0, 3])) + K = array(([2, -1, -1], [-1, 2, -1], [-1, -1, 2])) + D = array(([1, -1, 0], [-1, 1, 0], [0, 0, 0])) + Z = zeros((3, 3)) + I3 = eye(3) + A = np.block([[I3, Z], [Z, -K]]) + B = np.block([[Z, I3], [M, D]]) + + with np.errstate(all='ignore'): + self._check_gen_eig(A, B) + + def test_bad_geneig(self): + # Ticket #709 (strange return values from DGGEV) + + def matrices(omega): + c1 = -9 + omega**2 + c2 = 2*omega + A = [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, c1, 0], + [0, 0, 0, c1]] + B = [[0, 0, 1, 0], + [0, 0, 0, 1], + [1, 0, 0, -c2], + [0, 1, c2, 0]] + return A, B + + # With a buggy LAPACK, this can fail for different omega on different + # machines -- so we need to test several values + with np.errstate(all='ignore'): + for k in range(100): + A, B = matrices(omega=k*5./100) + self._check_gen_eig(A, B) + + def test_make_eigvals(self): + # Step through all paths in _make_eigvals + # Real eigenvalues + rng = np.random.RandomState(1234) + A = symrand(3, rng) + self._check_gen_eig(A, None) + B = symrand(3, rng) + self._check_gen_eig(A, B) + # Complex eigenvalues + A = rng.random((3, 3)) + 1j*rng.random((3, 3)) + self._check_gen_eig(A, None) + B = rng.random((3, 3)) + 1j*rng.random((3, 3)) + self._check_gen_eig(A, B) + + def test_check_finite(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + w, v = eig(a, check_finite=False) + exact_w = [(9+sqrt(93))/2, 0, (9-sqrt(93))/2] + v0 = array([1, 1, (1+sqrt(93)/3)/2]) + v1 = array([3., 0, -1]) + v2 = array([1, 1, (1-sqrt(93)/3)/2]) + v0 = v0 / norm(v0) + v1 = v1 / norm(v1) + v2 = v2 / norm(v2) + assert_array_almost_equal(w, exact_w) + assert_array_almost_equal(v0, v[:, 0]*sign(v[0, 0])) + assert_array_almost_equal(v1, v[:, 1]*sign(v[0, 1])) + assert_array_almost_equal(v2, v[:, 2]*sign(v[0, 2])) + for i in range(3): + assert_array_almost_equal(a @ v[:, i], w[i]*v[:, i]) + + def test_not_square_error(self): + """Check that passing a non-square array raises a ValueError.""" + A = np.arange(6).reshape(3, 2) + assert_raises(ValueError, eig, A) + + def test_shape_mismatch(self): + """Check that passing arrays of with different shapes + raises a ValueError.""" + A = eye(2) + B = np.arange(9.0).reshape(3, 3) + assert_raises(ValueError, eig, A, B) + assert_raises(ValueError, eig, B, A) + + def test_gh_11577(self): + # https://github.com/scipy/scipy/issues/11577 + # `A - lambda B` should have 4 and 8 among the eigenvalues, and this + # was apparently broken on some platforms + A = np.array([[12.0, 28.0, 76.0, 220.0], + [16.0, 32.0, 80.0, 224.0], + [24.0, 40.0, 88.0, 232.0], + [40.0, 56.0, 104.0, 248.0]], dtype='float64') + B = np.array([[2.0, 4.0, 10.0, 28.0], + [3.0, 5.0, 11.0, 29.0], + [5.0, 7.0, 13.0, 31.0], + [9.0, 11.0, 17.0, 35.0]], dtype='float64') + + D, V = eig(A, B) + + # The problem is ill-conditioned, and two other eigenvalues + # depend on ATLAS/OpenBLAS version, compiler version etc + # see gh-11577 for discussion + # + # NB: it is tempting to use `assert_allclose(D[:2], [4, 8])` instead but + # the ordering of eigenvalues also comes out different on different + # systems depending on who knows what. + with warnings.catch_warnings(): + # isclose chokes on inf/nan values + warnings.filterwarnings( + "ignore", "invalid value encountered in multiply", RuntimeWarning) + assert np.isclose(D, 4.0, atol=1e-14).any() + assert np.isclose(D, 8.0, atol=1e-14).any() + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + w, vr = eig(a) + + w_n, vr_n = eig(np.eye(2, dtype=dt)) + + assert w.shape == (0,) + assert w.dtype == w_n.dtype #eigvals(np.eye(2, dtype=dt)).dtype + + assert_allclose(vr, np.empty((0, 0))) + assert vr.shape == (0, 0) + assert vr.dtype == vr_n.dtype + + w, vr = eig(a, homogeneous_eigvals=True) + assert w.shape == (2, 0) + assert w.dtype == w_n.dtype + + assert vr.shape == (0, 0) + assert vr.dtype == vr_n.dtype + + @pytest.mark.parametrize("include_B", [False, True]) + @pytest.mark.parametrize("left", [False, True]) + @pytest.mark.parametrize("right", [False, True]) + @pytest.mark.parametrize("homogeneous_eigvals", [False, True]) + @pytest.mark.parametrize("dtype", [np.float32, np.complex128]) + def test_nd_input(self, include_B, left, right, homogeneous_eigvals, dtype): + batch_shape = (3, 2) + core_shape = (4, 4) + rng = np.random.default_rng(3249823598235) + A = rng.random(batch_shape + core_shape).astype(dtype) + B = rng.random(batch_shape + core_shape).astype(dtype) + kwargs = dict(right=right, homogeneous_eigvals=homogeneous_eigvals) + + if include_B: + res = eig(A, b=B, left=left, **kwargs) + else: + res = eig(A, left=left, **kwargs) + + for i in range(batch_shape[0]): + for j in range(batch_shape[1]): + if include_B: + ref = eig(A[i, j], b=B[i, j], left=left, **kwargs) + else: + ref = eig(A[i, j], left=left, **kwargs) + + if left or right: + for k in range(len(ref)): + assert_allclose(res[k][i, j], ref[k]) + else: + assert_allclose(res[i, j], ref) + + +class TestEigBanded: + def setup_method(self): + self.create_bandmat() + + def create_bandmat(self): + """Create the full matrix `self.fullmat` and + the corresponding band matrix `self.bandmat`.""" + N = 10 + self.KL = 2 # number of subdiagonals (below the diagonal) + self.KU = 2 # number of superdiagonals (above the diagonal) + + # symmetric band matrix + self.sym_mat = (diag(full(N, 1.0)) + + diag(full(N-1, -1.0), -1) + diag(full(N-1, -1.0), 1) + + diag(full(N-2, -2.0), -2) + diag(full(N-2, -2.0), 2)) + + # hermitian band matrix + self.herm_mat = (diag(full(N, -1.0)) + + 1j*diag(full(N-1, 1.0), -1) + - 1j*diag(full(N-1, 1.0), 1) + + diag(full(N-2, -2.0), -2) + + diag(full(N-2, -2.0), 2)) + + # general real band matrix + self.real_mat = (diag(full(N, 1.0)) + + diag(full(N-1, -1.0), -1) + diag(full(N-1, -3.0), 1) + + diag(full(N-2, 2.0), -2) + diag(full(N-2, -2.0), 2)) + + # general complex band matrix + self.comp_mat = (1j*diag(full(N, 1.0)) + + diag(full(N-1, -1.0), -1) + + 1j*diag(full(N-1, -3.0), 1) + + diag(full(N-2, 2.0), -2) + + diag(full(N-2, -2.0), 2)) + + # Eigenvalues and -vectors from linalg.eig + ew, ev = linalg.eig(self.sym_mat) + ew = ew.real + args = argsort(ew) + self.w_sym_lin = ew[args] + self.evec_sym_lin = ev[:, args] + + ew, ev = linalg.eig(self.herm_mat) + ew = ew.real + args = argsort(ew) + self.w_herm_lin = ew[args] + self.evec_herm_lin = ev[:, args] + + # Extract upper bands from symmetric and hermitian band matrices + # (for use in dsbevd, dsbevx, zhbevd, zhbevx + # and their single precision versions) + LDAB = self.KU + 1 + self.bandmat_sym = zeros((LDAB, N), dtype=float) + self.bandmat_herm = zeros((LDAB, N), dtype=complex) + for i in range(LDAB): + self.bandmat_sym[LDAB-i-1, i:N] = diag(self.sym_mat, i) + self.bandmat_herm[LDAB-i-1, i:N] = diag(self.herm_mat, i) + + # Extract bands from general real and complex band matrix + # (for use in dgbtrf, dgbtrs and their single precision versions) + LDAB = 2*self.KL + self.KU + 1 + self.bandmat_real = zeros((LDAB, N), dtype=float) + self.bandmat_real[2*self.KL, :] = diag(self.real_mat) # diagonal + for i in range(self.KL): + # superdiagonals + self.bandmat_real[2*self.KL-1-i, i+1:N] = diag(self.real_mat, i+1) + # subdiagonals + self.bandmat_real[2*self.KL+1+i, 0:N-1-i] = diag(self.real_mat, + -i-1) + + self.bandmat_comp = zeros((LDAB, N), dtype=complex) + self.bandmat_comp[2*self.KL, :] = diag(self.comp_mat) # diagonal + for i in range(self.KL): + # superdiagonals + self.bandmat_comp[2*self.KL-1-i, i+1:N] = diag(self.comp_mat, i+1) + # subdiagonals + self.bandmat_comp[2*self.KL+1+i, 0:N-1-i] = diag(self.comp_mat, + -i-1) + + # absolute value for linear equation system A*x = b + self.b = 1.0*arange(N) + self.bc = self.b * (1 + 1j) + + ##################################################################### + + def test_dsbev(self): + """Compare dsbev eigenvalues and eigenvectors with + the result of linalg.eig.""" + w, evec, info = dsbev(self.bandmat_sym, compute_v=1) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w_sym_lin) + assert_array_almost_equal(abs(evec_), abs(self.evec_sym_lin)) + + def test_dsbevd(self): + """Compare dsbevd eigenvalues and eigenvectors with + the result of linalg.eig.""" + w, evec, info = dsbevd(self.bandmat_sym, compute_v=1) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w_sym_lin) + assert_array_almost_equal(abs(evec_), abs(self.evec_sym_lin)) + + def test_dsbevx(self): + """Compare dsbevx eigenvalues and eigenvectors + with the result of linalg.eig.""" + N, N = shape(self.sym_mat) + # Achtung: Argumente 0.0,0.0,range? + w, evec, num, ifail, info = dsbevx(self.bandmat_sym, 0.0, 0.0, 1, N, + compute_v=1, range=2) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w_sym_lin) + assert_array_almost_equal(abs(evec_), abs(self.evec_sym_lin)) + + def test_zhbevd(self): + """Compare zhbevd eigenvalues and eigenvectors + with the result of linalg.eig.""" + w, evec, info = zhbevd(self.bandmat_herm, compute_v=1) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w_herm_lin) + assert_array_almost_equal(abs(evec_), abs(self.evec_herm_lin)) + + def test_zhbevx(self): + """Compare zhbevx eigenvalues and eigenvectors + with the result of linalg.eig.""" + N, N = shape(self.herm_mat) + # Achtung: Argumente 0.0,0.0,range? + w, evec, num, ifail, info = zhbevx(self.bandmat_herm, 0.0, 0.0, 1, N, + compute_v=1, range=2) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w_herm_lin) + assert_array_almost_equal(abs(evec_), abs(self.evec_herm_lin)) + + def test_eigvals_banded(self): + """Compare eigenvalues of eigvals_banded with those of linalg.eig.""" + w_sym = eigvals_banded(self.bandmat_sym) + w_sym = w_sym.real + assert_array_almost_equal(sort(w_sym), self.w_sym_lin) + + w_herm = eigvals_banded(self.bandmat_herm) + w_herm = w_herm.real + assert_array_almost_equal(sort(w_herm), self.w_herm_lin) + + # extracting eigenvalues with respect to an index range + ind1 = 2 + ind2 = np.longlong(6) + w_sym_ind = eigvals_banded(self.bandmat_sym, + select='i', select_range=(ind1, ind2)) + assert_array_almost_equal(sort(w_sym_ind), + self.w_sym_lin[ind1:ind2+1]) + w_herm_ind = eigvals_banded(self.bandmat_herm, + select='i', select_range=(ind1, ind2)) + assert_array_almost_equal(sort(w_herm_ind), + self.w_herm_lin[ind1:ind2+1]) + + # extracting eigenvalues with respect to a value range + v_lower = self.w_sym_lin[ind1] - 1.0e-5 + v_upper = self.w_sym_lin[ind2] + 1.0e-5 + w_sym_val = eigvals_banded(self.bandmat_sym, + select='v', select_range=(v_lower, v_upper)) + assert_array_almost_equal(sort(w_sym_val), + self.w_sym_lin[ind1:ind2+1]) + + v_lower = self.w_herm_lin[ind1] - 1.0e-5 + v_upper = self.w_herm_lin[ind2] + 1.0e-5 + w_herm_val = eigvals_banded(self.bandmat_herm, + select='v', + select_range=(v_lower, v_upper)) + assert_array_almost_equal(sort(w_herm_val), + self.w_herm_lin[ind1:ind2+1]) + + w_sym = eigvals_banded(self.bandmat_sym, check_finite=False) + w_sym = w_sym.real + assert_array_almost_equal(sort(w_sym), self.w_sym_lin) + + def test_eig_banded(self): + """Compare eigenvalues and eigenvectors of eig_banded + with those of linalg.eig. """ + w_sym, evec_sym = eig_banded(self.bandmat_sym) + evec_sym_ = evec_sym[:, argsort(w_sym.real)] + assert_array_almost_equal(sort(w_sym), self.w_sym_lin) + assert_array_almost_equal(abs(evec_sym_), abs(self.evec_sym_lin)) + + w_herm, evec_herm = eig_banded(self.bandmat_herm) + evec_herm_ = evec_herm[:, argsort(w_herm.real)] + assert_array_almost_equal(sort(w_herm), self.w_herm_lin) + assert_array_almost_equal(abs(evec_herm_), abs(self.evec_herm_lin)) + + # extracting eigenvalues with respect to an index range + ind1 = 2 + ind2 = 6 + w_sym_ind, evec_sym_ind = eig_banded(self.bandmat_sym, + select='i', + select_range=(ind1, ind2)) + assert_array_almost_equal(sort(w_sym_ind), + self.w_sym_lin[ind1:ind2+1]) + assert_array_almost_equal(abs(evec_sym_ind), + abs(self.evec_sym_lin[:, ind1:ind2+1])) + + w_herm_ind, evec_herm_ind = eig_banded(self.bandmat_herm, + select='i', + select_range=(ind1, ind2)) + assert_array_almost_equal(sort(w_herm_ind), + self.w_herm_lin[ind1:ind2+1]) + assert_array_almost_equal(abs(evec_herm_ind), + abs(self.evec_herm_lin[:, ind1:ind2+1])) + + # extracting eigenvalues with respect to a value range + v_lower = self.w_sym_lin[ind1] - 1.0e-5 + v_upper = self.w_sym_lin[ind2] + 1.0e-5 + w_sym_val, evec_sym_val = eig_banded(self.bandmat_sym, + select='v', + select_range=(v_lower, v_upper)) + assert_array_almost_equal(sort(w_sym_val), + self.w_sym_lin[ind1:ind2+1]) + assert_array_almost_equal(abs(evec_sym_val), + abs(self.evec_sym_lin[:, ind1:ind2+1])) + + v_lower = self.w_herm_lin[ind1] - 1.0e-5 + v_upper = self.w_herm_lin[ind2] + 1.0e-5 + w_herm_val, evec_herm_val = eig_banded(self.bandmat_herm, + select='v', + select_range=(v_lower, v_upper)) + assert_array_almost_equal(sort(w_herm_val), + self.w_herm_lin[ind1:ind2+1]) + assert_array_almost_equal(abs(evec_herm_val), + abs(self.evec_herm_lin[:, ind1:ind2+1])) + + w_sym, evec_sym = eig_banded(self.bandmat_sym, check_finite=False) + evec_sym_ = evec_sym[:, argsort(w_sym.real)] + assert_array_almost_equal(sort(w_sym), self.w_sym_lin) + assert_array_almost_equal(abs(evec_sym_), abs(self.evec_sym_lin)) + + def test_dgbtrf(self): + """Compare dgbtrf LU factorisation with the LU factorisation result + of linalg.lu.""" + M, N = shape(self.real_mat) + lu_symm_band, ipiv, info = dgbtrf(self.bandmat_real, self.KL, self.KU) + + # extract matrix u from lu_symm_band + u = diag(lu_symm_band[2*self.KL, :]) + for i in range(self.KL + self.KU): + u += diag(lu_symm_band[2*self.KL-1-i, i+1:N], i+1) + + p_lin, l_lin, u_lin = lu(self.real_mat, permute_l=0) + assert_array_almost_equal(u, u_lin) + + def test_zgbtrf(self): + """Compare zgbtrf LU factorisation with the LU factorisation result + of linalg.lu.""" + M, N = shape(self.comp_mat) + lu_symm_band, ipiv, info = zgbtrf(self.bandmat_comp, self.KL, self.KU) + + # extract matrix u from lu_symm_band + u = diag(lu_symm_band[2*self.KL, :]) + for i in range(self.KL + self.KU): + u += diag(lu_symm_band[2*self.KL-1-i, i+1:N], i+1) + + p_lin, l_lin, u_lin = lu(self.comp_mat, permute_l=0) + assert_array_almost_equal(u, u_lin) + + def test_dgbtrs(self): + """Compare dgbtrs solutions for linear equation system A*x = b + with solutions of linalg.solve.""" + + lu_symm_band, ipiv, info = dgbtrf(self.bandmat_real, self.KL, self.KU) + y, info = dgbtrs(lu_symm_band, self.KL, self.KU, self.b, ipiv) + + y_lin = linalg.solve(self.real_mat, self.b) + assert_array_almost_equal(y, y_lin) + + def test_zgbtrs(self): + """Compare zgbtrs solutions for linear equation system A*x = b + with solutions of linalg.solve.""" + + lu_symm_band, ipiv, info = zgbtrf(self.bandmat_comp, self.KL, self.KU) + y, info = zgbtrs(lu_symm_band, self.KL, self.KU, self.bc, ipiv) + + y_lin = linalg.solve(self.comp_mat, self.bc) + assert_array_almost_equal(y, y_lin) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a_band = np.empty((0, 0), dtype=dt) + w, v = eig_banded(a_band) + + w_n, v_n = eig_banded(np.array([[0, 0], [1, 1]], dtype=dt)) + + assert w.shape == (0,) + assert w.dtype == w_n.dtype + + assert v.shape == (0, 0) + assert v.dtype == v_n.dtype + + w = eig_banded(a_band, eigvals_only=True) + assert w.shape == (0,) + assert w.dtype == w_n.dtype + +class TestEigTridiagonal: + def setup_method(self): + self.create_trimat() + + def create_trimat(self): + """Create the full matrix `self.fullmat`, `self.d`, and `self.e`.""" + N = 10 + + # symmetric band matrix + self.d = full(N, 1.0) + self.e = full(N-1, -1.0) + self.full_mat = (diag(self.d) + diag(self.e, -1) + diag(self.e, 1)) + + ew, ev = linalg.eig(self.full_mat) + ew = ew.real + args = argsort(ew) + self.w = ew[args] + self.evec = ev[:, args] + + def test_degenerate(self): + """Test error conditions.""" + # Wrong sizes + assert_raises(ValueError, eigvalsh_tridiagonal, self.d, self.e[:-1]) + # Must be real + assert_raises(TypeError, eigvalsh_tridiagonal, self.d, self.e * 1j) + # Bad driver + assert_raises(TypeError, eigvalsh_tridiagonal, self.d, self.e, + lapack_driver=1.) + assert_raises(ValueError, eigvalsh_tridiagonal, self.d, self.e, + lapack_driver='foo') + # Bad bounds + assert_raises(ValueError, eigvalsh_tridiagonal, self.d, self.e, + select='i', select_range=(0, -1)) + + def test_eigvalsh_tridiagonal(self): + """Compare eigenvalues of eigvalsh_tridiagonal with those of eig.""" + # can't use ?STERF with subselection + for driver in ('sterf', 'stev', 'stevd', 'stebz', 'stemr', 'auto'): + w = eigvalsh_tridiagonal(self.d, self.e, lapack_driver=driver) + assert_array_almost_equal(sort(w), self.w) + + for driver in ('sterf', 'stev', 'stevd'): + assert_raises(ValueError, eigvalsh_tridiagonal, self.d, self.e, + lapack_driver=driver, select='i', + select_range=(0, 1)) + for driver in ('stebz', 'stemr', 'auto'): + # extracting eigenvalues with respect to the full index range + w_ind = eigvalsh_tridiagonal( + self.d, self.e, select='i', select_range=(0, len(self.d)-1), + lapack_driver=driver) + assert_array_almost_equal(sort(w_ind), self.w) + + # extracting eigenvalues with respect to an index range + ind1 = 2 + ind2 = 6 + w_ind = eigvalsh_tridiagonal( + self.d, self.e, select='i', select_range=(ind1, ind2), + lapack_driver=driver) + assert_array_almost_equal(sort(w_ind), self.w[ind1:ind2+1]) + + # extracting eigenvalues with respect to a value range + v_lower = self.w[ind1] - 1.0e-5 + v_upper = self.w[ind2] + 1.0e-5 + w_val = eigvalsh_tridiagonal( + self.d, self.e, select='v', select_range=(v_lower, v_upper), + lapack_driver=driver) + assert_array_almost_equal(sort(w_val), self.w[ind1:ind2+1]) + + def test_eigh_tridiagonal(self): + """Compare eigenvalues and eigenvectors of eigh_tridiagonal + with those of eig. """ + # can't use ?STERF when eigenvectors are requested + assert_raises(ValueError, eigh_tridiagonal, self.d, self.e, + lapack_driver='sterf') + for driver in ('stebz', 'stev', 'stevd', 'stemr', 'auto'): + w, evec = eigh_tridiagonal(self.d, self.e, lapack_driver=driver) + evec_ = evec[:, argsort(w)] + assert_array_almost_equal(sort(w), self.w) + assert_array_almost_equal(abs(evec_), abs(self.evec)) + + assert_raises(ValueError, eigh_tridiagonal, self.d, self.e, + lapack_driver='stev', select='i', select_range=(0, 1)) + for driver in ('stebz', 'stemr', 'auto'): + # extracting eigenvalues with respect to an index range + ind1 = 0 + ind2 = len(self.d)-1 + w, evec = eigh_tridiagonal( + self.d, self.e, select='i', select_range=(ind1, ind2), + lapack_driver=driver) + assert_array_almost_equal(sort(w), self.w) + assert_array_almost_equal(abs(evec), abs(self.evec)) + ind1 = 2 + ind2 = 6 + w, evec = eigh_tridiagonal( + self.d, self.e, select='i', select_range=(ind1, ind2), + lapack_driver=driver) + assert_array_almost_equal(sort(w), self.w[ind1:ind2+1]) + assert_array_almost_equal(abs(evec), + abs(self.evec[:, ind1:ind2+1])) + + # extracting eigenvalues with respect to a value range + v_lower = self.w[ind1] - 1.0e-5 + v_upper = self.w[ind2] + 1.0e-5 + w, evec = eigh_tridiagonal( + self.d, self.e, select='v', select_range=(v_lower, v_upper), + lapack_driver=driver) + assert_array_almost_equal(sort(w), self.w[ind1:ind2+1]) + assert_array_almost_equal(abs(evec), + abs(self.evec[:, ind1:ind2+1])) + + def test_eigh_tridiagonal_1x1(self): + """See gh-20075""" + a = np.array([-2.0]) + b = np.array([]) + x = eigh_tridiagonal(a, b, eigvals_only=True) + assert x.ndim == 1 + assert_allclose(x, a) + x, V = eigh_tridiagonal(a, b, select="i", select_range=(0, 0)) + assert x.ndim == 1 + assert V.ndim == 2 + assert_allclose(x, a) + assert_allclose(V, array([[1.]])) + + x, V = eigh_tridiagonal(a, b, select="v", select_range=(-2, 0)) + assert x.size == 0 + assert x.shape == (0,) + assert V.shape == (1, 0) + + +class TestEigh: + def test_wrong_inputs(self): + # Nonsquare a + assert_raises(ValueError, eigh, np.ones([1, 2])) + # Nonsquare b + assert_raises(ValueError, eigh, np.ones([2, 2]), np.ones([2, 1])) + # Incompatible a, b sizes + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([2, 2])) + # Wrong type parameter for generalized problem + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + type=4) + # Both value and index subsets requested + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + subset_by_value=[1, 2], subset_by_index=[2, 4]) + # Invalid upper index spec + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + subset_by_index=[0, 4]) + # Invalid lower index + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + subset_by_index=[-2, 2]) + # Invalid index spec #2 + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + subset_by_index=[2, 0]) + # Invalid value spec + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + subset_by_value=[2, 0]) + # Invalid driver name + assert_raises(ValueError, eigh, np.ones([2, 2]), driver='wrong') + # Generalized driver selection without b + assert_raises(ValueError, eigh, np.ones([3, 3]), None, driver='gvx') + # Standard driver with b + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + driver='evr') + # Subset request from invalid driver + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + driver='gvd', subset_by_index=[1, 2]) + assert_raises(ValueError, eigh, np.ones([3, 3]), np.ones([3, 3]), + driver='gvd', subset_by_index=[1, 2]) + + def test_nonpositive_b(self): + assert_raises(LinAlgError, eigh, np.ones([3, 3]), np.ones([3, 3])) + + # index based subsets are done in the legacy test_eigh() + def test_value_subsets(self): + for ind, dt in enumerate(DTYPES): + + a = _random_hermitian_matrix(20, dtype=dt) + w, v = eigh(a, subset_by_value=[-2, 2]) + assert_equal(v.shape[1], len(w)) + assert all((w > -2) & (w < 2)) + + b = _random_hermitian_matrix(20, posdef=True, dtype=dt) + w, v = eigh(a, b, subset_by_value=[-2, 2]) + assert_equal(v.shape[1], len(w)) + assert all((w > -2) & (w < 2)) + + def test_eigh_integer(self): + a = array([[1, 2], [2, 7]]) + b = array([[3, 1], [1, 5]]) + w, z = eigh(a) + w, z = eigh(a, b) + + @skip_xp_invalid_arg + def test_eigh_of_sparse(self): + # This tests the rejection of inputs that eigh cannot currently handle. + import scipy.sparse + a = scipy.sparse.identity(2).tocsc() + b = np.atleast_2d(a) + assert_raises(ValueError, eigh, a) + assert_raises(ValueError, eigh, b) + + @pytest.mark.parametrize('dtype_', DTYPES) + @pytest.mark.parametrize('driver', ("ev", "evd", "evr", "evx")) + def test_various_drivers_standard(self, driver, dtype_): + a = _random_hermitian_matrix(n=20, dtype=dtype_) + w, v = eigh(a, driver=driver) + assert_allclose(a @ v - (v * w), 0., + atol=1000*np.finfo(dtype_).eps, + rtol=0.) + + @pytest.mark.parametrize('driver', ("ev", "evd", "evr", "evx")) + def test_1x1_lwork(self, driver): + w, v = eigh([[1]], driver=driver) + assert_allclose(w, array([1.]), atol=1e-15) + assert_allclose(v, array([[1.]]), atol=1e-15) + + # complex case now + w, v = eigh([[1j]], driver=driver) + assert_allclose(w, array([0]), atol=1e-15) + assert_allclose(v, array([[1.]]), atol=1e-15) + + @pytest.mark.parametrize('type', (1, 2, 3)) + @pytest.mark.parametrize('driver', ("gv", "gvd", "gvx")) + def test_various_drivers_generalized(self, driver, type): + atol = np.spacing(5000.) + a = _random_hermitian_matrix(20) + b = _random_hermitian_matrix(20, posdef=True) + w, v = eigh(a=a, b=b, driver=driver, type=type) + if type == 1: + assert_allclose(a @ v - w*(b @ v), 0., atol=atol, rtol=0.) + elif type == 2: + assert_allclose(a @ b @ v - v * w, 0., atol=atol, rtol=0.) + else: + assert_allclose(b @ a @ v - v * w, 0., atol=atol, rtol=0.) + + def test_eigvalsh_new_args(self): + a = _random_hermitian_matrix(5) + w = eigvalsh(a, subset_by_index=[1, 2]) + assert_equal(len(w), 2) + + w2 = eigvalsh(a, subset_by_index=[1, 2]) + assert_equal(len(w2), 2) + assert_allclose(w, w2) + + b = np.diag([1, 1.2, 1.3, 1.5, 2]) + w3 = eigvalsh(b, subset_by_value=[1, 1.4]) + assert_equal(len(w3), 2) + assert_allclose(w3, np.array([1.2, 1.3])) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + w, v = eigh(a) + + w_n, v_n = eigh(np.eye(2, dtype=dt)) + + assert w.shape == (0,) + assert w.dtype == w_n.dtype + + assert v.shape == (0, 0) + assert v.dtype == v_n.dtype + + w = eigh(a, eigvals_only=True) + assert_allclose(w, np.empty((0,))) + + assert w.shape == (0,) + assert w.dtype == w_n.dtype + +class TestSVD_GESDD: + lapack_driver = 'gesdd' + + def test_degenerate(self): + assert_raises(TypeError, svd, [[1.]], lapack_driver=1.) + assert_raises(ValueError, svd, [[1.]], lapack_driver='foo') + + def test_simple(self): + a = [[1, 2, 3], [1, 20, 3], [2, 5, 6]] + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(3)) + assert_array_almost_equal(vh.T @ vh, eye(3)) + sigma = zeros((u.shape[0], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_simple_singular(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(3)) + assert_array_almost_equal(vh.T @ vh, eye(3)) + sigma = zeros((u.shape[0], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_simple_underdet(self): + a = [[1, 2, 3], [4, 5, 6]] + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(u.shape[0])) + sigma = zeros((u.shape[0], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_simple_overdet(self): + a = [[1, 2], [4, 5], [3, 4]] + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(u.shape[1])) + assert_array_almost_equal(vh.T @ vh, eye(2)) + sigma = zeros((u.shape[1], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_random(self): + rng = np.random.RandomState(1234) + n = 20 + m = 15 + for i in range(3): + for a in [rng.random([n, m]), rng.random([m, n])]: + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(u.shape[1])) + assert_array_almost_equal(vh @ vh.T, eye(vh.shape[0])) + sigma = zeros((u.shape[1], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_simple_complex(self): + a = [[1, 2, 3], [1, 2j, 3], [2, 5, 6]] + for full_matrices in (True, False): + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.conj().T @ u, eye(u.shape[1])) + assert_array_almost_equal(vh.conj().T @ vh, eye(vh.shape[0])) + sigma = zeros((u.shape[0], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_random_complex(self): + rng = np.random.RandomState(1234) + n = 20 + m = 15 + for i in range(3): + for full_matrices in (True, False): + for a in [rng.random([n, m]), rng.random([m, n])]: + a = a + 1j*rng.random(list(a.shape)) + u, s, vh = svd(a, full_matrices=full_matrices, + lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.conj().T @ u, + eye(u.shape[1])) + # This fails when [m,n] + # assert_array_almost_equal(vh.conj().T @ vh, + # eye(len(vh),dtype=vh.dtype.char)) + sigma = zeros((u.shape[1], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_crash_1580(self): + rng = np.random.RandomState(1234) + sizes = [(13, 23), (30, 50), (60, 100)] + for sz in sizes: + for dt in [np.float32, np.float64, np.complex64, np.complex128]: + a = rng.rand(*sz).astype(dt) + # should not crash + svd(a, lapack_driver=self.lapack_driver) + + def test_check_finite(self): + a = [[1, 2, 3], [1, 20, 3], [2, 5, 6]] + u, s, vh = svd(a, check_finite=False, lapack_driver=self.lapack_driver) + assert_array_almost_equal(u.T @ u, eye(3)) + assert_array_almost_equal(vh.T @ vh, eye(3)) + sigma = zeros((u.shape[0], vh.shape[0]), s.dtype.char) + for i in range(len(s)): + sigma[i, i] = s[i] + assert_array_almost_equal(u @ sigma @ vh, a) + + def test_gh_5039(self): + # This is a smoke test for https://github.com/scipy/scipy/issues/5039 + # + # The following is reported to raise "ValueError: On entry to DGESDD + # parameter number 12 had an illegal value". + # `interp1d([1,2,3,4], [1,2,3,4], kind='cubic')` + # This is reported to only show up on LAPACK 3.0.3. + # + # The matrix below is taken from the call to + # `B = _fitpack._bsplmat(order, xk)` in interpolate._find_smoothest + b = np.array( + [[0.16666667, 0.66666667, 0.16666667, 0., 0., 0.], + [0., 0.16666667, 0.66666667, 0.16666667, 0., 0.], + [0., 0., 0.16666667, 0.66666667, 0.16666667, 0.], + [0., 0., 0., 0.16666667, 0.66666667, 0.16666667]]) + svd(b, lapack_driver=self.lapack_driver) + + @pytest.mark.skipif(not HAS_ILP64, reason="64-bit LAPACK required") + @pytest.mark.slow + def test_large_matrix(self): + check_free_memory(free_mb=17000) + A = np.zeros([1, 2**31], dtype=np.float32) + A[0, -1] = 1 + u, s, vh = svd(A, full_matrices=False) + assert_allclose(s[0], 1.0) + assert_allclose(u[0, 0] * vh[0, -1], 1.0) + + @pytest.mark.parametrize("m", [0, 1, 2]) + @pytest.mark.parametrize("n", [0, 1, 2]) + @pytest.mark.parametrize('dtype', DTYPES) + def test_shape_dtype(self, m, n, dtype): + a = np.zeros((m, n), dtype=dtype) + k = min(m, n) + dchar = a.dtype.char + real_dchar = dchar.lower() if dchar in 'FD' else dchar + + u, s, v = svd(a) + assert_equal(u.shape, (m, m)) + assert_equal(u.dtype, dtype) + assert_equal(s.shape, (k,)) + assert_equal(s.dtype, np.dtype(real_dchar)) + assert_equal(v.shape, (n, n)) + assert_equal(v.dtype, dtype) + + u, s, v = svd(a, full_matrices=False) + assert_equal(u.shape, (m, k)) + assert_equal(u.dtype, dtype) + assert_equal(s.shape, (k,)) + assert_equal(s.dtype, np.dtype(real_dchar)) + assert_equal(v.shape, (k, n)) + assert_equal(v.dtype, dtype) + + s = svd(a, compute_uv=False) + assert_equal(s.shape, (k,)) + assert_equal(s.dtype, np.dtype(real_dchar)) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize(("m", "n"), [(0, 0), (0, 2), (2, 0)]) + def test_empty(self, dt, m, n): + a0 = np.eye(3, dtype=dt) + u0, s0, v0 = svd(a0) + + a = np.empty((m, n), dtype=dt) + u, s, v = svd(a) + assert_allclose(u, np.identity(m)) + assert_allclose(s, np.empty((0,))) + assert_allclose(v, np.identity(n)) + + assert u.dtype == u0.dtype + assert v.dtype == v0.dtype + assert s.dtype == s0.dtype + + u, s, v = svd(a, full_matrices=False) + assert_allclose(u, np.empty((m, 0))) + assert_allclose(s, np.empty((0,))) + assert_allclose(v, np.empty((0, n))) + + assert u.dtype == u0.dtype + assert v.dtype == v0.dtype + assert s.dtype == s0.dtype + + s = svd(a, compute_uv=False) + assert_allclose(s, np.empty((0,))) + + assert s.dtype == s0.dtype + +class TestSVD_GESVD(TestSVD_GESDD): + lapack_driver = 'gesvd' + + +# Allocating an array of such a size leads to _ArrayMemoryError(s) +# since the maximum memory that can be in 32-bit (WASM) is 4GB +@pytest.mark.skipif(IS_WASM, reason="out of memory in WASM") +@pytest.mark.xfail_on_32bit("out of memory in 32-bit CI workflow") +@pytest.mark.parallel_threads_limit(2) # 1.9 GiB per thread RAM usage +@pytest.mark.fail_slow(10) +def test_svd_gesdd_nofegfault(): + # svd(a) with {U,VT}.size > INT_MAX does not segfault + # cf https://github.com/scipy/scipy/issues/14001 + df=np.ones((4799, 53130), dtype=np.float64) + with assert_raises(ValueError): + svd(df) + + +def test_gesdd_nan_error_message(): + A = np.eye(2) + A[0, 0] = np.nan + with pytest.raises(ValueError, match="NaN"): + svd(A, check_finite=False) + + +class TestSVDVals: + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + for a in [[]], np.empty((2, 0)), np.ones((0, 3)): + a = np.array(a, dtype=dt) + s = svdvals(a) + assert_equal(s, np.empty(0)) + + s0 = svdvals(np.eye(2, dtype=dt)) + assert s.dtype == s0.dtype + + def test_simple(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + s = svdvals(a) + assert_(len(s) == 3) + assert_(s[0] >= s[1] >= s[2]) + + def test_simple_underdet(self): + a = [[1, 2, 3], [4, 5, 6]] + s = svdvals(a) + assert_(len(s) == 2) + assert_(s[0] >= s[1]) + + def test_simple_overdet(self): + a = [[1, 2], [4, 5], [3, 4]] + s = svdvals(a) + assert_(len(s) == 2) + assert_(s[0] >= s[1]) + + def test_simple_complex(self): + a = [[1, 2, 3], [1, 20, 3j], [2, 5, 6]] + s = svdvals(a) + assert_(len(s) == 3) + assert_(s[0] >= s[1] >= s[2]) + + def test_simple_underdet_complex(self): + a = [[1, 2, 3], [4, 5j, 6]] + s = svdvals(a) + assert_(len(s) == 2) + assert_(s[0] >= s[1]) + + def test_simple_overdet_complex(self): + a = [[1, 2], [4, 5], [3j, 4]] + s = svdvals(a) + assert_(len(s) == 2) + assert_(s[0] >= s[1]) + + def test_check_finite(self): + a = [[1, 2, 3], [1, 2, 3], [2, 5, 6]] + s = svdvals(a, check_finite=False) + assert_(len(s) == 3) + assert_(s[0] >= s[1] >= s[2]) + + @pytest.mark.slow + def test_crash_2609(self): + rng = np.random.default_rng(1234) + a = rng.random((1500, 2800)) + # Shouldn't crash: + svdvals(a) + + +class TestDiagSVD: + + def test_simple(self): + assert_array_almost_equal(diagsvd([1, 0, 0], 3, 3), + [[1, 0, 0], [0, 0, 0], [0, 0, 0]]) + + +class TestQR: + def test_simple(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(q @ r, a) + + def test_simple_left(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r = qr(a) + c = [1, 2, 3] + qc, r2 = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + assert_array_almost_equal(r, r2) + qc, r2 = qr_multiply(a, eye(3), "left") + assert_array_almost_equal(q, qc) + + def test_simple_right(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r = qr(a) + c = [1, 2, 3] + qc, r2 = qr_multiply(a, c) + assert_array_almost_equal(c @ q, qc) + assert_array_almost_equal(r, r2) + qc, r = qr_multiply(a, eye(3)) + assert_array_almost_equal(q, qc) + + def test_simple_pivoting(self): + a = np.asarray([[8, 2, 3], [2, 9, 3], [5, 3, 6]]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_left_pivoting(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r, jpvt = qr(a, pivoting=True) + c = [1, 2, 3] + qc, r, jpvt = qr_multiply(a, c, "left", True) + assert_array_almost_equal(q @ c, qc) + + def test_simple_right_pivoting(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r, jpvt = qr(a, pivoting=True) + c = [1, 2, 3] + qc, r, jpvt = qr_multiply(a, c, pivoting=True) + assert_array_almost_equal(c @ q, qc) + + def test_simple_trap(self): + a = [[8, 2, 3], [2, 9, 3]] + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a) + + def test_simple_trap_pivoting(self): + a = np.asarray([[8, 2, 3], [2, 9, 3]]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_tall(self): + # full version + a = [[8, 2], [2, 9], [5, 3]] + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(q @ r, a) + + def test_simple_tall_pivoting(self): + # full version pivoting + a = np.asarray([[8, 2], [2, 9], [5, 3]]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_tall_e(self): + # economy version + a = [[8, 2], [2, 9], [5, 3]] + q, r = qr(a, mode='economic') + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a) + assert_equal(q.shape, (3, 2)) + assert_equal(r.shape, (2, 2)) + + def test_simple_tall_e_pivoting(self): + # economy version pivoting + a = np.asarray([[8, 2], [2, 9], [5, 3]]) + q, r, p = qr(a, pivoting=True, mode='economic') + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p], mode='economic') + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_tall_left(self): + a = [[8, 2], [2, 9], [5, 3]] + q, r = qr(a, mode="economic") + c = [1, 2] + qc, r2 = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + assert_array_almost_equal(r, r2) + c = array([1, 2, 0]) + qc, r2 = qr_multiply(a, c, "left", overwrite_c=True) + assert_array_almost_equal(q @ c[:2], qc) + qc, r = qr_multiply(a, eye(2), "left") + assert_array_almost_equal(qc, q) + + def test_simple_tall_left_pivoting(self): + a = [[8, 2], [2, 9], [5, 3]] + q, r, jpvt = qr(a, mode="economic", pivoting=True) + c = [1, 2] + qc, r, kpvt = qr_multiply(a, c, "left", True) + assert_array_equal(jpvt, kpvt) + assert_array_almost_equal(q @ c, qc) + qc, r, jpvt = qr_multiply(a, eye(2), "left", True) + assert_array_almost_equal(qc, q) + + def test_simple_tall_right(self): + a = [[8, 2], [2, 9], [5, 3]] + q, r = qr(a, mode="economic") + c = [1, 2, 3] + cq, r2 = qr_multiply(a, c) + assert_array_almost_equal(c @ q, cq) + assert_array_almost_equal(r, r2) + cq, r = qr_multiply(a, eye(3)) + assert_array_almost_equal(cq, q) + + def test_simple_tall_right_pivoting(self): + a = [[8, 2], [2, 9], [5, 3]] + q, r, jpvt = qr(a, pivoting=True, mode="economic") + c = [1, 2, 3] + cq, r, jpvt = qr_multiply(a, c, pivoting=True) + assert_array_almost_equal(c @ q, cq) + cq, r, jpvt = qr_multiply(a, eye(3), pivoting=True) + assert_array_almost_equal(cq, q) + + def test_simple_fat(self): + # full version + a = [[8, 2, 5], [2, 9, 3]] + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a) + assert_equal(q.shape, (2, 2)) + assert_equal(r.shape, (2, 3)) + + def test_simple_fat_pivoting(self): + # full version pivoting + a = np.asarray([[8, 2, 5], [2, 9, 3]]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a[:, p]) + assert_equal(q.shape, (2, 2)) + assert_equal(r.shape, (2, 3)) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_fat_e(self): + # economy version + a = [[8, 2, 3], [2, 9, 5]] + q, r = qr(a, mode='economic') + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a) + assert_equal(q.shape, (2, 2)) + assert_equal(r.shape, (2, 3)) + + def test_simple_fat_e_pivoting(self): + # economy version pivoting + a = np.asarray([[8, 2, 3], [2, 9, 5]]) + q, r, p = qr(a, pivoting=True, mode='economic') + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(q @ r, a[:, p]) + assert_equal(q.shape, (2, 2)) + assert_equal(r.shape, (2, 3)) + q2, r2 = qr(a[:, p], mode='economic') + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_fat_left(self): + a = [[8, 2, 3], [2, 9, 5]] + q, r = qr(a, mode="economic") + c = [1, 2] + qc, r2 = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + assert_array_almost_equal(r, r2) + qc, r = qr_multiply(a, eye(2), "left") + assert_array_almost_equal(qc, q) + + def test_simple_fat_left_pivoting(self): + a = [[8, 2, 3], [2, 9, 5]] + q, r, jpvt = qr(a, mode="economic", pivoting=True) + c = [1, 2] + qc, r, jpvt = qr_multiply(a, c, "left", True) + assert_array_almost_equal(q @ c, qc) + qc, r, jpvt = qr_multiply(a, eye(2), "left", True) + assert_array_almost_equal(qc, q) + + def test_simple_fat_right(self): + a = [[8, 2, 3], [2, 9, 5]] + q, r = qr(a, mode="economic") + c = [1, 2] + cq, r2 = qr_multiply(a, c) + assert_array_almost_equal(c @ q, cq) + assert_array_almost_equal(r, r2) + cq, r = qr_multiply(a, eye(2)) + assert_array_almost_equal(cq, q) + + def test_simple_fat_right_pivoting(self): + a = [[8, 2, 3], [2, 9, 5]] + q, r, jpvt = qr(a, pivoting=True, mode="economic") + c = [1, 2] + cq, r, jpvt = qr_multiply(a, c, pivoting=True) + assert_array_almost_equal(c @ q, cq) + cq, r, jpvt = qr_multiply(a, eye(2), pivoting=True) + assert_array_almost_equal(cq, q) + + def test_simple_complex(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + q, r = qr(a) + assert_array_almost_equal(q.conj().T @ q, eye(3)) + assert_array_almost_equal(q @ r, a) + + def test_simple_complex_left(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + q, r = qr(a) + c = [1, 2, 3+4j] + qc, r = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + qc, r = qr_multiply(a, eye(3), "left") + assert_array_almost_equal(q, qc) + + def test_simple_complex_right(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + q, r = qr(a) + c = [1, 2, 3+4j] + qc, r = qr_multiply(a, c) + assert_array_almost_equal(c @ q, qc) + qc, r = qr_multiply(a, eye(3)) + assert_array_almost_equal(q, qc) + + def test_simple_tall_complex_left(self): + a = [[8, 2+3j], [2, 9], [5+7j, 3]] + q, r = qr(a, mode="economic") + c = [1, 2+2j] + qc, r2 = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + assert_array_almost_equal(r, r2) + c = array([1, 2, 0]) + qc, r2 = qr_multiply(a, c, "left", overwrite_c=True) + assert_array_almost_equal(q @ c[:2], qc) + qc, r = qr_multiply(a, eye(2), "left") + assert_array_almost_equal(qc, q) + + def test_simple_complex_left_conjugate(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + q, r = qr(a) + c = [1, 2, 3+4j] + qc, r = qr_multiply(a, c, "left", conjugate=True) + assert_array_almost_equal(q.conj() @ c, qc) + + def test_simple_complex_tall_left_conjugate(self): + a = [[3, 3+4j], [5, 2+2j], [3, 2]] + q, r = qr(a, mode='economic') + c = [1, 3+4j] + qc, r = qr_multiply(a, c, "left", conjugate=True) + assert_array_almost_equal(q.conj() @ c, qc) + + def test_simple_complex_right_conjugate(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + q, r = qr(a) + c = np.array([1, 2, 3+4j]) + qc, r = qr_multiply(a, c, conjugate=True) + assert_array_almost_equal(c @ q.conj(), qc) + + def test_simple_complex_pivoting(self): + a = array([[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.conj().T @ q, eye(3)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_simple_complex_left_pivoting(self): + a = array([[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]]) + q, r, jpvt = qr(a, pivoting=True) + c = [1, 2, 3+4j] + qc, r, jpvt = qr_multiply(a, c, "left", True) + assert_array_almost_equal(q @ c, qc) + + def test_simple_complex_right_pivoting(self): + a = array([[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]]) + q, r, jpvt = qr(a, pivoting=True) + c = [1, 2, 3+4j] + qc, r, jpvt = qr_multiply(a, c, pivoting=True) + assert_array_almost_equal(c @ q, qc) + + def test_random(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(n)) + assert_array_almost_equal(q @ r, a) + + def test_random_left(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + q, r = qr(a) + c = rng.random([n]) + qc, r = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + qc, r = qr_multiply(a, eye(n), "left") + assert_array_almost_equal(q, qc) + + def test_random_right(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + q, r = qr(a) + c = rng.random([n]) + cq, r = qr_multiply(a, c) + assert_array_almost_equal(c @ q, cq) + cq, r = qr_multiply(a, eye(n)) + assert_array_almost_equal(q, cq) + + def test_random_pivoting(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(n)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_random_tall(self): + rng = np.random.RandomState(1234) + # full version + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(m)) + assert_array_almost_equal(q @ r, a) + + def test_random_tall_left(self): + rng = np.random.RandomState(1234) + # full version + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r = qr(a, mode="economic") + c = rng.random([n]) + qc, r = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + qc, r = qr_multiply(a, eye(n), "left") + assert_array_almost_equal(qc, q) + + def test_random_tall_right(self): + rng = np.random.RandomState(1234) + # full version + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r = qr(a, mode="economic") + c = rng.random([m]) + cq, r = qr_multiply(a, c) + assert_array_almost_equal(c @ q, cq) + cq, r = qr_multiply(a, eye(m)) + assert_array_almost_equal(cq, q) + + def test_random_tall_pivoting(self): + rng = np.random.RandomState(1234) + # full version pivoting + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(m)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_random_tall_e(self): + rng = np.random.RandomState(1234) + # economy version + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r = qr(a, mode='economic') + assert_array_almost_equal(q.T @ q, eye(n)) + assert_array_almost_equal(q @ r, a) + assert_equal(q.shape, (m, n)) + assert_equal(r.shape, (n, n)) + + def test_random_tall_e_pivoting(self): + rng = np.random.RandomState(1234) + # economy version pivoting + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + q, r, p = qr(a, pivoting=True, mode='economic') + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(n)) + assert_array_almost_equal(q @ r, a[:, p]) + assert_equal(q.shape, (m, n)) + assert_equal(r.shape, (n, n)) + q2, r2 = qr(a[:, p], mode='economic') + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_random_trap(self): + rng = np.random.RandomState(1234) + m = 100 + n = 200 + for k in range(2): + a = rng.random([m, n]) + q, r = qr(a) + assert_array_almost_equal(q.T @ q, eye(m)) + assert_array_almost_equal(q @ r, a) + + def test_random_trap_pivoting(self): + rng = np.random.RandomState(1234) + m = 100 + n = 200 + for k in range(2): + a = rng.random([m, n]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.T @ q, eye(m)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_random_complex(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + q, r = qr(a) + assert_array_almost_equal(q.conj().T @ q, eye(n)) + assert_array_almost_equal(q @ r, a) + + def test_random_complex_left(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + q, r = qr(a) + c = rng.random([n]) + 1j*rng.random([n]) + qc, r = qr_multiply(a, c, "left") + assert_array_almost_equal(q @ c, qc) + qc, r = qr_multiply(a, eye(n), "left") + assert_array_almost_equal(q, qc) + + def test_random_complex_right(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + q, r = qr(a) + c = rng.random([n]) + 1j*rng.random([n]) + cq, r = qr_multiply(a, c) + assert_array_almost_equal(c @ q, cq) + cq, r = qr_multiply(a, eye(n)) + assert_array_almost_equal(q, cq) + + def test_random_complex_pivoting(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + q, r, p = qr(a, pivoting=True) + d = abs(diag(r)) + assert_(np.all(d[1:] <= d[:-1])) + assert_array_almost_equal(q.conj().T @ q, eye(n)) + assert_array_almost_equal(q @ r, a[:, p]) + q2, r2 = qr(a[:, p]) + assert_array_almost_equal(q, q2) + assert_array_almost_equal(r, r2) + + def test_check_finite(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + q, r = qr(a, check_finite=False) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(q @ r, a) + + def test_lwork(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + # Get comparison values + q, r = qr(a, lwork=None) + + # Test against minimum valid lwork + q2, r2 = qr(a, lwork=3) + assert_array_almost_equal(q2, q) + assert_array_almost_equal(r2, r) + + # Test against larger lwork + q3, r3 = qr(a, lwork=10) + assert_array_almost_equal(q3, q) + assert_array_almost_equal(r3, r) + + # Test against explicit lwork=-1 + q4, r4 = qr(a, lwork=-1) + assert_array_almost_equal(q4, q) + assert_array_almost_equal(r4, r) + + # Test against invalid lwork + assert_raises(Exception, qr, (a,), {'lwork': 0}) + assert_raises(Exception, qr, (a,), {'lwork': 2}) + + @pytest.mark.parametrize("m", [0, 1, 2]) + @pytest.mark.parametrize("n", [0, 1, 2]) + @pytest.mark.parametrize("pivoting", [False, True]) + @pytest.mark.parametrize('dtype', DTYPES) + def test_shape_dtype(self, m, n, pivoting, dtype): + k = min(m, n) + + a = np.zeros((m, n), dtype=dtype) + q, r, *other = qr(a, pivoting=pivoting) + assert_equal(q.shape, (m, m)) + assert_equal(q.dtype, dtype) + assert_equal(r.shape, (m, n)) + assert_equal(r.dtype, dtype) + assert len(other) == (1 if pivoting else 0) + if pivoting: + p, = other + assert_equal(p.shape, (n,)) + assert_equal(p.dtype, np.int32) + + r, *other = qr(a, mode='r', pivoting=pivoting) + assert_equal(r.shape, (m, n)) + assert_equal(r.dtype, dtype) + assert len(other) == (1 if pivoting else 0) + if pivoting: + p, = other + assert_equal(p.shape, (n,)) + assert_equal(p.dtype, np.int32) + + q, r, *other = qr(a, mode='economic', pivoting=pivoting) + assert_equal(q.shape, (m, k)) + assert_equal(q.dtype, dtype) + assert_equal(r.shape, (k, n)) + assert_equal(r.dtype, dtype) + assert len(other) == (1 if pivoting else 0) + if pivoting: + p, = other + assert_equal(p.shape, (n,)) + assert_equal(p.dtype, np.int32) + + (raw, tau), r, *other = qr(a, mode='raw', pivoting=pivoting) + assert_equal(raw.shape, (m, n)) + assert_equal(raw.dtype, dtype) + assert_equal(tau.shape, (k,)) + assert_equal(tau.dtype, dtype) + assert_equal(r.shape, (k, n)) + assert_equal(r.dtype, dtype) + assert len(other) == (1 if pivoting else 0) + if pivoting: + p, = other + assert_equal(p.shape, (n,)) + assert_equal(p.dtype, np.int32) + + @pytest.mark.parametrize(("m", "n"), [(0, 0), (0, 2), (2, 0)]) + def test_empty(self, m, n): + k = min(m, n) + + a = np.empty((m, n)) + q, r = qr(a) + assert_allclose(q, np.identity(m)) + assert_allclose(r, np.empty((m, n))) + + q, r, p = qr(a, pivoting=True) + assert_allclose(q, np.identity(m)) + assert_allclose(r, np.empty((m, n))) + assert_allclose(p, np.arange(n)) + + r, = qr(a, mode='r') + assert_allclose(r, np.empty((m, n))) + + q, r = qr(a, mode='economic') + assert_allclose(q, np.empty((m, k))) + assert_allclose(r, np.empty((k, n))) + + (raw, tau), r = qr(a, mode='raw') + assert_allclose(raw, np.empty((m, n))) + assert_allclose(tau, np.empty((k,))) + assert_allclose(r, np.empty((k, n))) + + def test_multiply_empty(self): + a = np.empty((0, 0)) + c = np.empty((0, 0)) + cq, r = qr_multiply(a, c) + assert_allclose(cq, np.empty((0, 0))) + + a = np.empty((0, 2)) + c = np.empty((2, 0)) + cq, r = qr_multiply(a, c) + assert_allclose(cq, np.empty((2, 0))) + + a = np.empty((2, 0)) + c = np.empty((0, 2)) + cq, r = qr_multiply(a, c) + assert_allclose(cq, np.empty((0, 2))) + + +class TestRQ: + def test_simple(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + r, q = rq(a) + assert_array_almost_equal(q @ q.T, eye(3)) + assert_array_almost_equal(r @ q, a) + + def test_r(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + r, q = rq(a) + r2 = rq(a, mode='r') + assert_array_almost_equal(r, r2) + + def test_random(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + r, q = rq(a) + assert_array_almost_equal(q @ q.T, eye(n)) + assert_array_almost_equal(r @ q, a) + + def test_simple_trap(self): + a = [[8, 2, 3], [2, 9, 3]] + r, q = rq(a) + assert_array_almost_equal(q.T @ q, eye(3)) + assert_array_almost_equal(r @ q, a) + + def test_simple_tall(self): + a = [[8, 2], [2, 9], [5, 3]] + r, q = rq(a) + assert_array_almost_equal(q.T @ q, eye(2)) + assert_array_almost_equal(r @ q, a) + + def test_simple_fat(self): + a = [[8, 2, 5], [2, 9, 3]] + r, q = rq(a) + assert_array_almost_equal(q @ q.T, eye(3)) + assert_array_almost_equal(r @ q, a) + + def test_simple_complex(self): + a = [[3, 3+4j, 5], [5, 2, 2+7j], [3, 2, 7]] + r, q = rq(a) + assert_array_almost_equal(q @ q.conj().T, eye(3)) + assert_array_almost_equal(r @ q, a) + + def test_random_tall(self): + rng = np.random.RandomState(1234) + m = 200 + n = 100 + for k in range(2): + a = rng.random([m, n]) + r, q = rq(a) + assert_array_almost_equal(q @ q.T, eye(n)) + assert_array_almost_equal(r @ q, a) + + def test_random_trap(self): + rng = np.random.RandomState(1234) + m = 100 + n = 200 + for k in range(2): + a = rng.random([m, n]) + r, q = rq(a) + assert_array_almost_equal(q @ q.T, eye(n)) + assert_array_almost_equal(r @ q, a) + + def test_random_trap_economic(self): + rng = np.random.RandomState(1234) + m = 100 + n = 200 + for k in range(2): + a = rng.random([m, n]) + r, q = rq(a, mode='economic') + assert_array_almost_equal(q @ q.T, eye(m)) + assert_array_almost_equal(r @ q, a) + assert_equal(q.shape, (m, n)) + assert_equal(r.shape, (m, m)) + + def test_random_complex(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + r, q = rq(a) + assert_array_almost_equal(q @ q.conj().T, eye(n)) + assert_array_almost_equal(r @ q, a) + + def test_random_complex_economic(self): + rng = np.random.RandomState(1234) + m = 100 + n = 200 + for k in range(2): + a = rng.random([m, n]) + 1j*rng.random([m, n]) + r, q = rq(a, mode='economic') + assert_array_almost_equal(q @ q.conj().T, eye(m)) + assert_array_almost_equal(r @ q, a) + assert_equal(q.shape, (m, n)) + assert_equal(r.shape, (m, m)) + + def test_check_finite(self): + a = [[8, 2, 3], [2, 9, 3], [5, 3, 6]] + r, q = rq(a, check_finite=False) + assert_array_almost_equal(q @ q.T, eye(3)) + assert_array_almost_equal(r @ q, a) + + @pytest.mark.parametrize("m", [0, 1, 2]) + @pytest.mark.parametrize("n", [0, 1, 2]) + @pytest.mark.parametrize('dtype', DTYPES) + def test_shape_dtype(self, m, n, dtype): + k = min(m, n) + + a = np.zeros((m, n), dtype=dtype) + r, q = rq(a) + assert_equal(q.shape, (n, n)) + assert_equal(r.shape, (m, n)) + assert_equal(r.dtype, dtype) + assert_equal(q.dtype, dtype) + + r = rq(a, mode='r') + assert_equal(r.shape, (m, n)) + assert_equal(r.dtype, dtype) + + r, q = rq(a, mode='economic') + assert_equal(r.shape, (m, k)) + assert_equal(r.dtype, dtype) + assert_equal(q.shape, (k, n)) + assert_equal(q.dtype, dtype) + + @pytest.mark.parametrize(("m", "n"), [(0, 0), (0, 2), (2, 0)]) + def test_empty(self, m, n): + k = min(m, n) + + a = np.empty((m, n)) + r, q = rq(a) + assert_allclose(r, np.empty((m, n))) + assert_allclose(q, np.identity(n)) + + r = rq(a, mode='r') + assert_allclose(r, np.empty((m, n))) + + r, q = rq(a, mode='economic') + assert_allclose(r, np.empty((m, k))) + assert_allclose(q, np.empty((k, n))) + + +class TestSchur: + + def check_schur(self, a, t, u, rtol, atol): + # Check that the Schur decomposition is correct. + assert_allclose(u @ t @ u.conj().T, a, rtol=rtol, atol=atol, + err_msg="Schur decomposition does not match 'a'") + # The expected value of u @ u.H - I is all zeros, so test + # with absolute tolerance only. + assert_allclose(u @ u.conj().T - np.eye(len(u)), 0, rtol=0, atol=atol, + err_msg="u is not unitary") + + def test_simple(self): + a = [[8, 12, 3], [2, 9, 3], [10, 3, 6]] + t, z = schur(a) + self.check_schur(a, t, z, rtol=1e-14, atol=5e-15) + tc, zc = schur(a, 'complex') + assert_(np.any(ravel(iscomplex(zc))) and np.any(ravel(iscomplex(tc)))) + self.check_schur(a, tc, zc, rtol=1e-14, atol=5e-15) + tc2, zc2 = rsf2csf(tc, zc) + self.check_schur(a, tc2, zc2, rtol=1e-14, atol=5e-15) + + @pytest.mark.parametrize( + 'sort, expected_diag', + [('lhp', [-np.sqrt(2), -0.5, np.sqrt(2), 0.5]), + ('rhp', [np.sqrt(2), 0.5, -np.sqrt(2), -0.5]), + ('iuc', [-0.5, 0.5, np.sqrt(2), -np.sqrt(2)]), + ('ouc', [np.sqrt(2), -np.sqrt(2), -0.5, 0.5]), + (lambda x: x >= 0.0, [np.sqrt(2), 0.5, -np.sqrt(2), -0.5])] + ) + def test_sort(self, sort, expected_diag): + # The exact eigenvalues of this matrix are + # -sqrt(2), sqrt(2), -1/2, 1/2. + a = [[4., 3., 1., -1.], + [-4.5, -3.5, -1., 1.], + [9., 6., -4., 4.5], + [6., 4., -3., 3.5]] + t, u, sdim = schur(a, sort=sort) + self.check_schur(a, t, u, rtol=1e-14, atol=5e-15) + assert_allclose(np.diag(t), expected_diag, rtol=1e-12) + assert_equal(2, sdim) + + def test_sort_errors(self): + a = [[4., 3., 1., -1.], + [-4.5, -3.5, -1., 1.], + [9., 6., -4., 4.5], + [6., 4., -3., 3.5]] + assert_raises(ValueError, schur, a, sort='unsupported') + assert_raises(ValueError, schur, a, sort=1) + + def test_check_finite(self): + a = [[8, 12, 3], [2, 9, 3], [10, 3, 6]] + t, z = schur(a, check_finite=False) + assert_array_almost_equal(z @ t @ z.conj().T, a) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + t, z = schur(a) + t0, z0 = schur(np.eye(2, dtype=dt)) + assert_allclose(t, np.empty((0, 0))) + assert_allclose(z, np.empty((0, 0))) + assert t.dtype == t0.dtype + assert z.dtype == z0.dtype + + t, z, sdim = schur(a, sort='lhp') + assert_allclose(t, np.empty((0, 0))) + assert_allclose(z, np.empty((0, 0))) + assert_equal(sdim, 0) + assert t.dtype == t0.dtype + assert z.dtype == z0.dtype + + @pytest.mark.parametrize('sort', ['iuc', 'ouc']) + @pytest.mark.parametrize('output', ['real', 'complex']) + @pytest.mark.parametrize('dtype', [np.float32, np.float64, + np.complex64, np.complex128]) + def test_gh_13137_sort_str(self, sort, output, dtype): + # gh-13137 reported that sort values 'iuc' and 'ouc' were not + # correct because the callables assumed that the eigenvalues would + # always be expressed as a single complex number. + # In fact, when `output='real'` and the dtype is real, the + # eigenvalues are passed as separate real and imaginary components + # (yet no error is raised if the callable accepts only one argument). + # + # This tests these sort values by counting the number of eigenvalues + # `schur` reports as being inside/outside the unit circle. + + # Real matrix with eigenvalues 0.1 +- 2j + A = np.asarray([[0.1, -2], [2, 0.1]]) + + # Previously, this would fail for `output='real'` with real dtypes + sdim = schur(A.astype(dtype), sort=sort, output=output)[-1] + assert sdim == 0 if sort == 'iuc' else sdim == 2 + + @pytest.mark.parametrize('output', ['real', 'complex']) + @pytest.mark.parametrize('dtype', [np.float32, np.float64, + np.complex64, np.complex128]) + def test_gh_13137_sort_custom(self, output, dtype): + # This simply tests our understanding of how eigenvalues are + # passed to a sort callable. If `output='real'` and the dtype is real, + # real and imaginary parts are passed as separate real arguments; + # otherwise, they are passed a single complex argument. + # Also, if `output='real'` and the dtype is real, when either + # eigenvalue in a complex conjugate pair satisfies the sort condition, + # `sdim` is incremented by TWO. + + # Real matrix with eigenvalues 0.1 +- 2j + A = np.asarray([[0.1, -2], [2, 0.1]]) + + all_real = output=='real' and dtype in {np.float32, np.float64} + + def sort(x, y=None): + if all_real: + assert not np.iscomplexobj(x) + assert y is not None and np.isreal(y) + z = x + y*1j + else: + assert np.iscomplexobj(x) + assert y is None + z = x + return z.imag > 1e-15 + + # Only one complex eigenvalue satisfies the condition, but when + # `all_real` applies, both eigenvalues in the complex conjugate pair + # are counted. + sdim = schur(A.astype(dtype), sort=sort, output=output)[-1] + assert sdim == 2 if all_real else sdim == 1 + + +class TestHessenberg: + + def test_simple(self): + a = [[-149, -50, -154], + [537, 180, 546], + [-27, -9, -25]] + h1 = [[-149.0000, 42.2037, -156.3165], + [-537.6783, 152.5511, -554.9272], + [0, 0.0728, 2.4489]] + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.T @ a @ q, h) + assert_array_almost_equal(h, h1, decimal=4) + + def test_simple_complex(self): + a = [[-149, -50, -154], + [537, 180j, 546], + [-27j, -9, -25]] + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.conj().T @ a @ q, h) + + def test_simple2(self): + a = [[1, 2, 3, 4, 5, 6, 7], + [0, 2, 3, 4, 6, 7, 2], + [0, 2, 2, 3, 0, 3, 2], + [0, 0, 2, 8, 0, 0, 2], + [0, 3, 1, 2, 0, 1, 2], + [0, 1, 2, 3, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 2]] + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.T @ a @ q, h) + + def test_simple3(self): + a = np.eye(3) + a[-1, 0] = 2 + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.T @ a @ q, h) + + def test_random(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.T @ a @ q, h) + + def test_random_complex(self): + rng = np.random.RandomState(1234) + n = 20 + for k in range(2): + a = rng.random([n, n]) + 1j*rng.random([n, n]) + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q.conj().T @ a @ q, h) + + def test_check_finite(self): + a = [[-149, -50, -154], + [537, 180, 546], + [-27, -9, -25]] + h1 = [[-149.0000, 42.2037, -156.3165], + [-537.6783, 152.5511, -554.9272], + [0, 0.0728, 2.4489]] + h, q = hessenberg(a, calc_q=1, check_finite=False) + assert_array_almost_equal(q.T @ a @ q, h) + assert_array_almost_equal(h, h1, decimal=4) + + def test_2x2(self): + a = [[2, 1], [7, 12]] + + h, q = hessenberg(a, calc_q=1) + assert_array_almost_equal(q, np.eye(2)) + assert_array_almost_equal(h, a) + + b = [[2-7j, 1+2j], [7+3j, 12-2j]] + h2, q2 = hessenberg(b, calc_q=1) + assert_array_almost_equal(q2, np.eye(2)) + assert_array_almost_equal(h2, b) + + @pytest.mark.parametrize('dt', [int, float, float32, complex, complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + h = hessenberg(a) + assert h.shape == (0, 0) + assert h.dtype == hessenberg(np.eye(3, dtype=dt)).dtype + + h, q = hessenberg(a, calc_q=True) + h3, q3 = hessenberg(a, calc_q=True) + assert h.shape == (0, 0) + assert h.dtype == h3.dtype + + assert q.shape == (0, 0) + assert q.dtype == q3.dtype + + +blas_provider = blas_version = None +blas_provider = CONFIG['Build Dependencies']['blas']['name'] +blas_version = CONFIG['Build Dependencies']['blas']['version'] + + +class TestQZ: + def test_qz_single(self): + rng = np.random.RandomState(12345) + n = 5 + A = rng.random([n, n]).astype(float32) + B = rng.random([n, n]).astype(float32) + AA, BB, Q, Z = qz(A, B) + assert_array_almost_equal(Q @ AA @ Z.T, A, decimal=5) + assert_array_almost_equal(Q @ BB @ Z.T, B, decimal=5) + assert_array_almost_equal(Q @ Q.T, eye(n), decimal=5) + assert_array_almost_equal(Z @ Z.T, eye(n), decimal=5) + assert_(np.all(diag(BB) >= 0)) + + def test_qz_double(self): + rng = np.random.RandomState(12345) + n = 5 + A = rng.random([n, n]) + B = rng.random([n, n]) + AA, BB, Q, Z = qz(A, B) + assert_array_almost_equal(Q @ AA @ Z.T, A) + assert_array_almost_equal(Q @ BB @ Z.T, B) + assert_array_almost_equal(Q @ Q.T, eye(n)) + assert_array_almost_equal(Z @ Z.T, eye(n)) + assert_(np.all(diag(BB) >= 0)) + + def test_qz_complex(self): + rng = np.random.RandomState(12345) + n = 5 + A = rng.random([n, n]) + 1j*rng.random([n, n]) + B = rng.random([n, n]) + 1j*rng.random([n, n]) + AA, BB, Q, Z = qz(A, B) + assert_array_almost_equal(Q @ AA @ Z.conj().T, A) + assert_array_almost_equal(Q @ BB @ Z.conj().T, B) + assert_array_almost_equal(Q @ Q.conj().T, eye(n)) + assert_array_almost_equal(Z @ Z.conj().T, eye(n)) + assert_(np.all(diag(BB) >= 0)) + assert_(np.all(diag(BB).imag == 0)) + + def test_qz_complex64(self): + rng = np.random.RandomState(12345) + n = 5 + A = (rng.random([n, n]) + 1j*rng.random([n, n])).astype(complex64) + B = (rng.random([n, n]) + 1j*rng.random([n, n])).astype(complex64) + AA, BB, Q, Z = qz(A, B) + assert_array_almost_equal(Q @ AA @ Z.conj().T, A, decimal=5) + assert_array_almost_equal(Q @ BB @ Z.conj().T, B, decimal=5) + assert_array_almost_equal(Q @ Q.conj().T, eye(n), decimal=5) + assert_array_almost_equal(Z @ Z.conj().T, eye(n), decimal=5) + assert_(np.all(diag(BB) >= 0)) + assert_(np.all(diag(BB).imag == 0)) + + def test_qz_double_complex(self): + rng = np.random.RandomState(12345) + n = 5 + A = rng.random([n, n]) + B = rng.random([n, n]) + AA, BB, Q, Z = qz(A, B, output='complex') + aa = Q @ AA @ Z.conj().T + assert_array_almost_equal(aa.real, A) + assert_array_almost_equal(aa.imag, 0) + bb = Q @ BB @ Z.conj().T + assert_array_almost_equal(bb.real, B) + assert_array_almost_equal(bb.imag, 0) + assert_array_almost_equal(Q @ Q.conj().T, eye(n)) + assert_array_almost_equal(Z @ Z.conj().T, eye(n)) + assert_(np.all(diag(BB) >= 0)) + + def test_qz_double_sort(self): + # from https://www.nag.com/lapack-ex/node119.html + # NOTE: These matrices may be ill-conditioned and lead to a + # seg fault on certain python versions when compiled with + # sse2 or sse3 older ATLAS/LAPACK binaries for windows + # A = np.array([[3.9, 12.5, -34.5, -0.5], + # [ 4.3, 21.5, -47.5, 7.5], + # [ 4.3, 21.5, -43.5, 3.5], + # [ 4.4, 26.0, -46.0, 6.0 ]]) + + # B = np.array([[ 1.0, 2.0, -3.0, 1.0], + # [1.0, 3.0, -5.0, 4.0], + # [1.0, 3.0, -4.0, 3.0], + # [1.0, 3.0, -4.0, 4.0]]) + A = np.array([[3.9, 12.5, -34.5, 2.5], + [4.3, 21.5, -47.5, 7.5], + [4.3, 1.5, -43.5, 3.5], + [4.4, 6.0, -46.0, 6.0]]) + + B = np.array([[1.0, 1.0, -3.0, 1.0], + [1.0, 3.0, -5.0, 4.4], + [1.0, 2.0, -4.0, 1.0], + [1.2, 3.0, -4.0, 4.0]]) + + assert_raises(ValueError, qz, A, B, sort=lambda ar, ai, beta: ai == 0) + if False: + AA, BB, Q, Z, sdim = qz(A, B, sort=lambda ar, ai, beta: ai == 0) + # assert_(sdim == 2) + assert_(sdim == 4) + assert_array_almost_equal(Q @ AA @ Z.T, A) + assert_array_almost_equal(Q @ BB @ Z.T, B) + + # test absolute values bc the sign is ambiguous and + # might be platform dependent + assert_array_almost_equal(np.abs(AA), np.abs(np.array( + [[35.7864, -80.9061, -12.0629, -9.498], + [0., 2.7638, -2.3505, 7.3256], + [0., 0., 0.6258, -0.0398], + [0., 0., 0., -12.8217]])), 4) + assert_array_almost_equal(np.abs(BB), np.abs(np.array( + [[4.5324, -8.7878, 3.2357, -3.5526], + [0., 1.4314, -2.1894, 0.9709], + [0., 0., 1.3126, -0.3468], + [0., 0., 0., 0.559]])), 4) + assert_array_almost_equal(np.abs(Q), np.abs(np.array( + [[-0.4193, -0.605, -0.1894, -0.6498], + [-0.5495, 0.6987, 0.2654, -0.3734], + [-0.4973, -0.3682, 0.6194, 0.4832], + [-0.5243, 0.1008, -0.7142, 0.4526]])), 4) + assert_array_almost_equal(np.abs(Z), np.abs(np.array( + [[-0.9471, -0.2971, -0.1217, 0.0055], + [-0.0367, 0.1209, 0.0358, 0.9913], + [0.3171, -0.9041, -0.2547, 0.1312], + [0.0346, 0.2824, -0.9587, 0.0014]])), 4) + + # test absolute values bc the sign is ambiguous and might be platform + # dependent + # assert_array_almost_equal(abs(AA), abs(np.array([ + # [3.8009, -69.4505, 50.3135, -43.2884], + # [0.0000, 9.2033, -0.2001, 5.9881], + # [0.0000, 0.0000, 1.4279, 4.4453], + # [0.0000, 0.0000, 0.9019, -1.1962]])), 4) + # assert_array_almost_equal(abs(BB), abs(np.array([ + # [1.9005, -10.2285, 0.8658, -5.2134], + # [0.0000, 2.3008, 0.7915, 0.4262], + # [0.0000, 0.0000, 0.8101, 0.0000], + # [0.0000, 0.0000, 0.0000, -0.2823]])), 4) + # assert_array_almost_equal(abs(Q), abs(np.array([ + # [0.4642, 0.7886, 0.2915, -0.2786], + # [0.5002, -0.5986, 0.5638, -0.2713], + # [0.5002, 0.0154, -0.0107, 0.8657], + # [0.5331, -0.1395, -0.7727, -0.3151]])), 4) + # assert_array_almost_equal(dot(Q,Q.T), eye(4)) + # assert_array_almost_equal(abs(Z), abs(np.array([ + # [0.9961, -0.0014, 0.0887, -0.0026], + # [0.0057, -0.0404, -0.0938, -0.9948], + # [0.0626, 0.7194, -0.6908, 0.0363], + # [0.0626, -0.6934, -0.7114, 0.0956]])), 4) + # assert_array_almost_equal(dot(Z,Z.T), eye(4)) + + # def test_qz_complex_sort(self): + # cA = np.array([ + # [-21.10+22.50*1j, 53.50+-50.50*1j, -34.50+127.50*1j, 7.50+ 0.50*1j], + # [-0.46+ -7.78*1j, -3.50+-37.50*1j, -15.50+ 58.50*1j,-10.50+ -1.50*1j], + # [ 4.30+ -5.50*1j, 39.70+-17.10*1j, -68.50+ 12.50*1j, -7.50+ -3.50*1j], + # [ 5.50+ 4.40*1j, 14.40+ 43.30*1j, -32.50+-46.00*1j,-19.00+-32.50*1j]]) + + # cB = np.array([ + # [1.00+ -5.00*1j, 1.60+ 1.20*1j,-3.00+ 0.00*1j, 0.00+ -1.00*1j], + # [0.80+ -0.60*1j, 3.00+ -5.00*1j,-4.00+ 3.00*1j,-2.40+ -3.20*1j], + # [1.00+ 0.00*1j, 2.40+ 1.80*1j,-4.00+ -5.00*1j, 0.00+ -3.00*1j], + # [0.00+ 1.00*1j,-1.80+ 2.40*1j, 0.00+ -4.00*1j, 4.00+ -5.00*1j]]) + + # AAS,BBS,QS,ZS,sdim = qz(cA,cB,sort='lhp') + + # eigenvalues = diag(AAS)/diag(BBS) + # assert_(np.all(np.real(eigenvalues[:sdim] < 0))) + # assert_(np.all(np.real(eigenvalues[sdim:] > 0))) + + def test_check_finite(self): + rng = np.random.RandomState(12345) + n = 5 + A = rng.random([n, n]) + B = rng.random([n, n]) + AA, BB, Q, Z = qz(A, B, check_finite=False) + assert_array_almost_equal(Q @ AA @ Z.T, A) + assert_array_almost_equal(Q @ BB @ Z.T, B) + assert_array_almost_equal(Q @ Q.T, eye(n)) + assert_array_almost_equal(Z @ Z.T, eye(n)) + assert_(np.all(diag(BB) >= 0)) + + +class TestOrdQZ: + @classmethod + def setup_class(cls): + # https://www.nag.com/lapack-ex/node119.html + A1 = np.array([[-21.10 - 22.50j, 53.5 - 50.5j, -34.5 + 127.5j, + 7.5 + 0.5j], + [-0.46 - 7.78j, -3.5 - 37.5j, -15.5 + 58.5j, + -10.5 - 1.5j], + [4.30 - 5.50j, 39.7 - 17.1j, -68.5 + 12.5j, + -7.5 - 3.5j], + [5.50 + 4.40j, 14.4 + 43.3j, -32.5 - 46.0j, + -19.0 - 32.5j]]) + + B1 = np.array([[1.0 - 5.0j, 1.6 + 1.2j, -3 + 0j, 0.0 - 1.0j], + [0.8 - 0.6j, .0 - 5.0j, -4 + 3j, -2.4 - 3.2j], + [1.0 + 0.0j, 2.4 + 1.8j, -4 - 5j, 0.0 - 3.0j], + [0.0 + 1.0j, -1.8 + 2.4j, 0 - 4j, 4.0 - 5.0j]]) + + # https://www.nag.com/numeric/fl/nagdoc_fl23/xhtml/F08/f08yuf.xml + A2 = np.array([[3.9, 12.5, -34.5, -0.5], + [4.3, 21.5, -47.5, 7.5], + [4.3, 21.5, -43.5, 3.5], + [4.4, 26.0, -46.0, 6.0]]) + + B2 = np.array([[1, 2, -3, 1], + [1, 3, -5, 4], + [1, 3, -4, 3], + [1, 3, -4, 4]]) + + # example with the eigenvalues + # -0.33891648, 1.61217396+0.74013521j, 1.61217396-0.74013521j, + # 0.61244091 + # thus featuring: + # * one complex conjugate eigenvalue pair, + # * one eigenvalue in the lhp + # * 2 eigenvalues in the unit circle + # * 2 non-real eigenvalues + A3 = np.array([[5., 1., 3., 3.], + [4., 4., 2., 7.], + [7., 4., 1., 3.], + [0., 4., 8., 7.]]) + B3 = np.array([[8., 10., 6., 10.], + [7., 7., 2., 9.], + [9., 1., 6., 6.], + [5., 1., 4., 7.]]) + + # example with infinite eigenvalues + A4 = np.eye(2) + B4 = np.diag([0, 1]) + + # example with (alpha, beta) = (0, 0) + A5 = np.diag([1, 0]) + + cls.A = [A1, A2, A3, A4, A5] + cls.B = [B1, B2, B3, B4, A5] + + def qz_decomp(self, sort): + with np.errstate(all='raise'): + ret = [ordqz(Ai, Bi, sort=sort) for Ai, Bi in zip(self.A, self.B)] + return tuple(ret) + + def check(self, A, B, sort, AA, BB, alpha, beta, Q, Z): + Id = np.eye(*A.shape) + # make sure Q and Z are orthogonal + assert_array_almost_equal(Q @ Q.T.conj(), Id) + assert_array_almost_equal(Z @ Z.T.conj(), Id) + # check factorization + assert_array_almost_equal(Q @ AA, A @ Z) + assert_array_almost_equal(Q @ BB, B @ Z) + # check shape of AA and BB + assert_array_equal(np.tril(AA, -2), np.zeros(AA.shape)) + assert_array_equal(np.tril(BB, -1), np.zeros(BB.shape)) + # check eigenvalues + for i in range(A.shape[0]): + # does the current diagonal element belong to a 2-by-2 block + # that was already checked? + if i > 0 and A[i, i - 1] != 0: + continue + # take care of 2-by-2 blocks + if i < AA.shape[0] - 1 and AA[i + 1, i] != 0: + evals, _ = eig(AA[i:i + 2, i:i + 2], BB[i:i + 2, i:i + 2]) + # make sure the pair of complex conjugate eigenvalues + # is ordered consistently (positive imaginary part first) + if evals[0].imag < 0: + evals = evals[[1, 0]] + tmp = alpha[i:i + 2]/beta[i:i + 2] + if tmp[0].imag < 0: + tmp = tmp[[1, 0]] + assert_array_almost_equal(evals, tmp) + else: + if alpha[i] == 0 and beta[i] == 0: + assert_equal(AA[i, i], 0) + assert_equal(BB[i, i], 0) + elif beta[i] == 0: + assert_equal(BB[i, i], 0) + else: + assert_almost_equal(AA[i, i]/BB[i, i], alpha[i]/beta[i]) + sortfun = _select_function(sort) + lastsort = True + for i in range(A.shape[0]): + cursort = sortfun(np.array([alpha[i]]), np.array([beta[i]])) + # once the sorting criterion was not matched all subsequent + # eigenvalues also shouldn't match + if not lastsort: + assert not cursort + lastsort = cursort + + def check_all(self, sort): + ret = self.qz_decomp(sort) + + for reti, Ai, Bi in zip(ret, self.A, self.B): + self.check(Ai, Bi, sort, *reti) + + def test_lhp(self): + self.check_all('lhp') + + def test_rhp(self): + self.check_all('rhp') + + def test_iuc(self): + self.check_all('iuc') + + def test_ouc(self): + self.check_all('ouc') + + def test_ref(self): + # real eigenvalues first (top-left corner) + def sort(x, y): + out = np.empty_like(x, dtype=bool) + nonzero = (y != 0) + out[~nonzero] = False + out[nonzero] = (x[nonzero]/y[nonzero]).imag == 0 + return out + + self.check_all(sort) + + def test_cef(self): + # complex eigenvalues first (top-left corner) + def sort(x, y): + out = np.empty_like(x, dtype=bool) + nonzero = (y != 0) + out[~nonzero] = False + out[nonzero] = (x[nonzero]/y[nonzero]).imag != 0 + return out + + self.check_all(sort) + + def test_diff_input_types(self): + ret = ordqz(self.A[1], self.B[2], sort='lhp') + self.check(self.A[1], self.B[2], 'lhp', *ret) + + ret = ordqz(self.B[2], self.A[1], sort='lhp') + self.check(self.B[2], self.A[1], 'lhp', *ret) + + def test_sort_explicit(self): + # Test order of the eigenvalues in the 2 x 2 case where we can + # explicitly compute the solution + A1 = np.eye(2) + B1 = np.diag([-2, 0.5]) + expected1 = [('lhp', [-0.5, 2]), + ('rhp', [2, -0.5]), + ('iuc', [-0.5, 2]), + ('ouc', [2, -0.5])] + A2 = np.eye(2) + B2 = np.diag([-2 + 1j, 0.5 + 0.5j]) + expected2 = [('lhp', [1/(-2 + 1j), 1/(0.5 + 0.5j)]), + ('rhp', [1/(0.5 + 0.5j), 1/(-2 + 1j)]), + ('iuc', [1/(-2 + 1j), 1/(0.5 + 0.5j)]), + ('ouc', [1/(0.5 + 0.5j), 1/(-2 + 1j)])] + # 'lhp' is ambiguous so don't test it + A3 = np.eye(2) + B3 = np.diag([2, 0]) + expected3 = [('rhp', [0.5, np.inf]), + ('iuc', [0.5, np.inf]), + ('ouc', [np.inf, 0.5])] + # 'rhp' is ambiguous so don't test it + A4 = np.eye(2) + B4 = np.diag([-2, 0]) + expected4 = [('lhp', [-0.5, np.inf]), + ('iuc', [-0.5, np.inf]), + ('ouc', [np.inf, -0.5])] + A5 = np.diag([0, 1]) + B5 = np.diag([0, 0.5]) + # 'lhp' and 'iuc' are ambiguous so don't test them + expected5 = [('rhp', [2, np.nan]), + ('ouc', [2, np.nan])] + + A = [A1, A2, A3, A4, A5] + B = [B1, B2, B3, B4, B5] + expected = [expected1, expected2, expected3, expected4, expected5] + for Ai, Bi, expectedi in zip(A, B, expected): + for sortstr, expected_eigvals in expectedi: + _, _, alpha, beta, _, _ = ordqz(Ai, Bi, sort=sortstr) + azero = (alpha == 0) + bzero = (beta == 0) + x = np.empty_like(alpha) + x[azero & bzero] = np.nan + x[~azero & bzero] = np.inf + x[~bzero] = alpha[~bzero]/beta[~bzero] + assert_allclose(expected_eigvals, x) + + +class TestOrdQZWorkspaceSize: + @pytest.mark.fail_slow(5) + def test_decompose(self): + rng = np.random.RandomState(12345) + N = 202 + # raises error if lwork parameter to dtrsen is too small + for ddtype in [np.float32, np.float64]: + A = rng.random((N, N)).astype(ddtype) + B = rng.random((N, N)).astype(ddtype) + # sort = lambda ar, ai, b: ar**2 + ai**2 < b**2 + _ = ordqz(A, B, sort=lambda alpha, beta: alpha < beta, + output='real') + + for ddtype in [np.complex128, np.complex64]: + A = rng.random((N, N)).astype(ddtype) + B = rng.random((N, N)).astype(ddtype) + _ = ordqz(A, B, sort=lambda alpha, beta: alpha < beta, + output='complex') + + @pytest.mark.slow + def test_decompose_ouc(self): + rng = np.random.RandomState(12345) + N = 202 + # segfaults if lwork parameter to dtrsen is too small + for ddtype in [np.float32, np.float64, np.complex128, np.complex64]: + A = rng.random((N, N)).astype(ddtype) + B = rng.random((N, N)).astype(ddtype) + S, T, alpha, beta, U, V = ordqz(A, B, sort='ouc') + + +class TestDatacopied: + + def test_datacopied(self): + from scipy.linalg._decomp import _datacopied + + M = matrix([[0, 1], [2, 3]]) + A = asarray(M) + L = M.tolist() + M2 = M.copy() + + class Fake1: + def __array__(self, dtype=None, copy=None): + return A + + class Fake2: + __array_interface__ = A.__array_interface__ + + F1 = Fake1() + F2 = Fake2() + + for item, status in [(M, False), (A, False), (L, True), + (M2, False), (F1, False), (F2, False)]: + arr = asarray(item) + assert_equal(_datacopied(arr, item), status, + err_msg=repr(item)) + + +def test_aligned_mem_float(): + """Check linalg works with non-aligned memory (float32)""" + # Allocate 402 bytes of memory (allocated on boundary) + a = arange(402, dtype=np.uint8) + + # Create an array with boundary offset 4 + z = np.frombuffer(a.data, offset=2, count=100, dtype=float32) + z = z.reshape((10, 10)) + + eig(z, overwrite_a=True) + eig(z.T, overwrite_a=True) + + +@pytest.mark.skipif(platform.machine() == 'ppc64le', + reason="crashes on ppc64le") +def test_aligned_mem(): + """Check linalg works with non-aligned memory (float64)""" + # Allocate 804 bytes of memory (allocated on boundary) + a = arange(804, dtype=np.uint8) + + # Create an array with boundary offset 4 + z = np.frombuffer(a.data, offset=4, count=100, dtype=float) + z = z.reshape((10, 10)) + + eig(z, overwrite_a=True) + eig(z.T, overwrite_a=True) + + +def test_aligned_mem_complex(): + """Check that complex objects don't need to be completely aligned""" + # Allocate 1608 bytes of memory (allocated on boundary) + a = zeros(1608, dtype=np.uint8) + + # Create an array with boundary offset 8 + z = np.frombuffer(a.data, offset=8, count=100, dtype=complex) + z = z.reshape((10, 10)) + + eig(z, overwrite_a=True) + # This does not need special handling + eig(z.T, overwrite_a=True) + + +def check_lapack_misaligned(func, args, kwargs): + args = list(args) + for i in range(len(args)): + a = args[:] + if isinstance(a[i], np.ndarray): + # Try misaligning a[i] + aa = np.zeros(a[i].size*a[i].dtype.itemsize+8, dtype=np.uint8) + aa = np.frombuffer(aa.data, offset=4, count=a[i].size, + dtype=a[i].dtype) + aa = aa.reshape(a[i].shape) + aa[...] = a[i] + a[i] = aa + func(*a, **kwargs) + if len(a[i].shape) > 1: + a[i] = a[i].T + func(*a, **kwargs) + + +@pytest.mark.xfail(run=False, + reason="Ticket #1152, triggers a segfault in rare cases.") +def test_lapack_misaligned(): + M = np.eye(10, dtype=float) + R = np.arange(100).reshape((10, 10)) + S = np.arange(20000, dtype=np.uint8) + S = np.frombuffer(S.data, offset=4, count=100, dtype=float) + S = S.reshape((10, 10)) + b = np.ones(10) + LU, piv = lu_factor(S) + for (func, args, kwargs) in [ + (eig, (S,), dict(overwrite_a=True)), # crash + (eigvals, (S,), dict(overwrite_a=True)), # no crash + (lu, (S,), dict(overwrite_a=True)), # no crash + (lu_factor, (S,), dict(overwrite_a=True)), # no crash + (lu_solve, ((LU, piv), b), dict(overwrite_b=True)), + (solve, (S, b), dict(overwrite_a=True, overwrite_b=True)), + (svd, (M,), dict(overwrite_a=True)), # no crash + (svd, (R,), dict(overwrite_a=True)), # no crash + (svd, (S,), dict(overwrite_a=True)), # crash + (svdvals, (S,), dict()), # no crash + (svdvals, (S,), dict(overwrite_a=True)), # crash + (cholesky, (M,), dict(overwrite_a=True)), # no crash + (qr, (S,), dict(overwrite_a=True)), # crash + (rq, (S,), dict(overwrite_a=True)), # crash + (hessenberg, (S,), dict(overwrite_a=True)), # crash + (schur, (S,), dict(overwrite_a=True)), # crash + ]: + check_lapack_misaligned(func, args, kwargs) +# not properly tested +# cholesky, rsf2csf, lu_solve, solve, eig_banded, eigvals_banded, eigh, diagsvd + + +class TestOverwrite: + def test_eig(self): + assert_no_overwrite(eig, [(3, 3)]) + assert_no_overwrite(eig, [(3, 3), (3, 3)]) + + def test_eigh(self): + assert_no_overwrite(eigh, [(3, 3)]) + assert_no_overwrite(eigh, [(3, 3), (3, 3)]) + + def test_eig_banded(self): + assert_no_overwrite(eig_banded, [(3, 2)]) + + def test_eigvals(self): + assert_no_overwrite(eigvals, [(3, 3)]) + + def test_eigvalsh(self): + assert_no_overwrite(eigvalsh, [(3, 3)]) + + def test_eigvals_banded(self): + assert_no_overwrite(eigvals_banded, [(3, 2)]) + + def test_hessenberg(self): + assert_no_overwrite(hessenberg, [(3, 3)]) + + def test_lu_factor(self): + assert_no_overwrite(lu_factor, [(3, 3)]) + + def test_lu_solve(self): + x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 8]]) + xlu = lu_factor(x) + assert_no_overwrite(lambda b: lu_solve(xlu, b), [(3,)]) + + def test_lu(self): + assert_no_overwrite(lu, [(3, 3)]) + + def test_qr(self): + assert_no_overwrite(qr, [(3, 3)]) + + def test_rq(self): + assert_no_overwrite(rq, [(3, 3)]) + + def test_schur(self): + assert_no_overwrite(schur, [(3, 3)]) + + def test_schur_complex(self): + assert_no_overwrite(lambda a: schur(a, 'complex'), [(3, 3)], + dtypes=[np.float32, np.float64]) + + def test_svd(self): + assert_no_overwrite(svd, [(3, 3)]) + assert_no_overwrite(lambda a: svd(a, lapack_driver='gesvd'), [(3, 3)]) + + def test_svdvals(self): + assert_no_overwrite(svdvals, [(3, 3)]) + + +def _check_orth(n, dtype, skip_big=False): + X = np.ones((n, 2), dtype=float).astype(dtype) + + eps = np.finfo(dtype).eps + tol = 1000 * eps + + Y = orth(X) + assert_equal(Y.shape, (n, 1)) + assert_allclose(Y, Y.mean(), atol=tol, rtol=1.4e-7) + + Y = orth(X.T) + assert_equal(Y.shape, (2, 1)) + assert_allclose(Y, Y.mean(), atol=tol) + + if n > 5 and not skip_big: + rng = np.random.RandomState(1) + X = rng.rand(n, 5) @ rng.rand(5, n) + X = X + 1e-4 * rng.rand(n, 1) @ rng.rand(1, n) + X = X.astype(dtype) + + Y = orth(X, rcond=1e-3) + assert_equal(Y.shape, (n, 5)) + + Y = orth(X, rcond=1e-6) + assert_equal(Y.shape, (n, 5 + 1)) + + +@pytest.mark.slow +@pytest.mark.skipif(np.dtype(np.intp).itemsize < 8, + reason="test only on 64-bit, else too slow") +def test_orth_memory_efficiency(): + # Pick n so that 16*n bytes is reasonable but 8*n*n bytes is unreasonable. + # Keep in mind that @pytest.mark.slow tests are likely to be running + # under configurations that support 4Gb+ memory for tests related to + # 32 bit overflow. + n = 10*1000*1000 + try: + _check_orth(n, np.float64, skip_big=True) + except MemoryError as e: + raise AssertionError( + 'memory error perhaps caused by orth regression' + ) from e + + +def test_orth(): + dtypes = [np.float32, np.float64, np.complex64, np.complex128] + sizes = [1, 2, 3, 10, 100] + for dt, n in itertools.product(dtypes, sizes): + _check_orth(n, dt) + +@pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) +def test_orth_empty(dt): + a = np.empty((0, 0), dtype=dt) + a0 = np.eye(2, dtype=dt) + + oa = orth(a) + assert oa.dtype == orth(a0).dtype + assert oa.shape == (0, 0) + + +class TestNullSpace: + def test_null_space(self): + rng = np.random.RandomState(1) + + dtypes = [np.float32, np.float64, np.complex64, np.complex128] + sizes = [1, 2, 3, 10, 100] + + for dt, n in itertools.product(dtypes, sizes): + X = np.ones((2, n), dtype=dt) + + eps = np.finfo(dt).eps + tol = 1000 * eps + + Y = null_space(X) + assert_equal(Y.shape, (n, n-1)) + assert_allclose(X @ Y, 0, atol=tol) + + Y = null_space(X.T) + assert_equal(Y.shape, (2, 1)) + assert_allclose(X.T @ Y, 0, atol=tol) + + X = rng.randn(1 + n//2, n) + Y = null_space(X) + assert_equal(Y.shape, (n, n - 1 - n//2)) + assert_allclose(X @ Y, 0, atol=tol) + + if n > 5: + rng = np.random.RandomState(1) + X = rng.rand(n, 5) @ rng.rand(5, n) + X = X + 1e-4 * rng.rand(n, 1) @ rng.rand(1, n) + X = X.astype(dt) + + Y = null_space(X, rcond=1e-3) + assert_equal(Y.shape, (n, n - 5)) + + Y = null_space(X, rcond=1e-6) + assert_equal(Y.shape, (n, n - 6)) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_null_space_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + a0 = np.eye(2, dtype=dt) + nsa = null_space(a) + + assert nsa.shape == (0, 0) + assert nsa.dtype == null_space(a0).dtype + + @pytest.mark.parametrize("overwrite_a", [True, False]) + @pytest.mark.parametrize("check_finite", [True, False]) + @pytest.mark.parametrize("lapack_driver", ["gesdd", "gesvd"]) + def test_null_space_options(self, overwrite_a, check_finite, lapack_driver): + rng = np.random.default_rng(42887289350573064398746) + n = 10 + X = rng.standard_normal((1 + n//2, n)) + Y = null_space(X.copy(), overwrite_a=overwrite_a, check_finite=check_finite, + lapack_driver=lapack_driver) + assert_allclose(X @ Y, 0, atol=np.finfo(X.dtype).eps*100) + + +def test_subspace_angles(): + H = hadamard(8, float) + A = H[:, :3] + B = H[:, 3:] + assert_allclose(subspace_angles(A, B), [np.pi / 2.] * 3, atol=1e-14) + assert_allclose(subspace_angles(B, A), [np.pi / 2.] * 3, atol=1e-14) + for x in (A, B): + assert_allclose(subspace_angles(x, x), np.zeros(x.shape[1]), + atol=1e-14) + # From MATLAB function "subspace", which effectively only returns the + # last value that we calculate + x = np.array( + [[0.537667139546100, 0.318765239858981, 3.578396939725760, 0.725404224946106], # noqa: E501 + [1.833885014595086, -1.307688296305273, 2.769437029884877, -0.063054873189656], # noqa: E501 + [-2.258846861003648, -0.433592022305684, -1.349886940156521, 0.714742903826096], # noqa: E501 + [0.862173320368121, 0.342624466538650, 3.034923466331855, -0.204966058299775]]) # noqa: E501 + expected = 1.481454682101605 + assert_allclose(subspace_angles(x[:, :2], x[:, 2:])[0], expected, + rtol=1e-12) + assert_allclose(subspace_angles(x[:, 2:], x[:, :2])[0], expected, + rtol=1e-12) + expected = 0.746361174247302 + assert_allclose(subspace_angles(x[:, :2], x[:, [2]]), expected, rtol=1e-12) + assert_allclose(subspace_angles(x[:, [2]], x[:, :2]), expected, rtol=1e-12) + expected = 0.487163718534313 + assert_allclose(subspace_angles(x[:, :3], x[:, [3]]), expected, rtol=1e-12) + assert_allclose(subspace_angles(x[:, [3]], x[:, :3]), expected, rtol=1e-12) + expected = 0.328950515907756 + assert_allclose(subspace_angles(x[:, :2], x[:, 1:]), [expected, 0], + atol=1e-12) + # Degenerate conditions + assert_raises(ValueError, subspace_angles, x[0], x) + assert_raises(ValueError, subspace_angles, x, x[0]) + assert_raises(ValueError, subspace_angles, x[:-1], x) + + # Test branch if mask.any is True: + A = np.array([[1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [0, 0, 0], + [0, 0, 0]]) + B = np.array([[1, 0, 0], + [0, 1, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 1]]) + expected = np.array([np.pi/2, 0, 0]) + assert_allclose(subspace_angles(A, B), expected, rtol=1e-12) + + # Complex + # second column in "b" does not affect result, just there so that + # b can have more cols than a, and vice-versa (both conditional code paths) + a = [[1 + 1j], [0]] + b = [[1 - 1j, 0], [0, 1]] + assert_allclose(subspace_angles(a, b), 0., atol=1e-14) + assert_allclose(subspace_angles(b, a), 0., atol=1e-14) + + # Empty + a = np.empty((0, 0)) + b = np.empty((0, 0)) + assert_allclose(subspace_angles(a, b), np.empty((0,))) + a = np.empty((2, 0)) + b = np.empty((2, 0)) + assert_allclose(subspace_angles(a, b), np.empty((0,))) + a = np.empty((0, 2)) + b = np.empty((0, 3)) + assert_allclose(subspace_angles(a, b), np.empty((0,))) + + +class TestCDF2RDF: + + def matmul(self, a, b): + return np.einsum('...ij,...jk->...ik', a, b) + + def assert_eig_valid(self, w, v, x): + assert_array_almost_equal( + self.matmul(v, w), + self.matmul(x, v) + ) + + def test_single_array0x0real(self): + # eig doesn't support 0x0 in old versions of numpy + X = np.empty((0, 0)) + w, v = np.empty(0), np.empty((0, 0)) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_single_array2x2_real(self): + X = np.array([[1, 2], [3, -1]]) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_single_array2x2_complex(self): + X = np.array([[1, 2], [-2, 1]]) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_single_array3x3_real(self): + X = np.array([[1, 2, 3], [1, 2, 3], [2, 5, 6]]) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_single_array3x3_complex(self): + X = np.array([[1, 2, 3], [0, 4, 5], [0, -5, 4]]) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_random_1d_stacked_arrays(self): + rng = np.random.default_rng(1234) + # cannot test M == 0 due to bug in old numpy + for M in range(1, 7): + X = rng.random((100, M, M)) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_random_2d_stacked_arrays(self): + rng = np.random.default_rng(1234) + # cannot test M == 0 due to bug in old numpy + for M in range(1, 7): + X = rng.random((10, 10, M, M)) + w, v = np.linalg.eig(X) + wr, vr = cdf2rdf(w, v) + self.assert_eig_valid(wr, vr, X) + + def test_low_dimensionality_error(self): + w, v = np.empty(()), np.array((2,)) + assert_raises(ValueError, cdf2rdf, w, v) + + def test_not_square_error(self): + # Check that passing a non-square array raises a ValueError. + w, v = np.arange(3), np.arange(6).reshape(3, 2) + assert_raises(ValueError, cdf2rdf, w, v) + + def test_swapped_v_w_error(self): + # Check that exchanging places of w and v raises ValueError. + X = np.array([[1, 2, 3], [0, 4, 5], [0, -5, 4]]) + w, v = np.linalg.eig(X) + assert_raises(ValueError, cdf2rdf, v, w) + + def test_non_associated_error(self): + # Check that passing non-associated eigenvectors raises a ValueError. + w, v = np.arange(3), np.arange(16).reshape(4, 4) + assert_raises(ValueError, cdf2rdf, w, v) + + def test_not_conjugate_pairs(self): + # Check that passing non-conjugate pairs raises a ValueError. + X = np.array([[1, 2, 3], [1, 2, 3], [2, 5, 6+1j]]) + w, v = np.linalg.eig(X) + assert_raises(ValueError, cdf2rdf, w, v) + + # different arrays in the stack, so not conjugate + X = np.array([ + [[1, 2, 3], [1, 2, 3], [2, 5, 6+1j]], + [[1, 2, 3], [1, 2, 3], [2, 5, 6-1j]], + ]) + w, v = np.linalg.eig(X) + assert_raises(ValueError, cdf2rdf, w, v) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cholesky.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cholesky.py new file mode 100644 index 0000000000000000000000000000000000000000..61bbc7e544f10fc834034fbadd7141f6deb1d423 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cholesky.py @@ -0,0 +1,268 @@ +import pytest +import numpy as np +from numpy.testing import assert_array_almost_equal +from pytest import raises as assert_raises + +from numpy import array, transpose, dot, conjugate, zeros_like, empty +from numpy.random import random +from scipy.linalg import (cholesky, cholesky_banded, cho_solve_banded, + cho_factor, cho_solve) + +from scipy.linalg._testutils import assert_no_overwrite + + +class TestCholesky: + + def test_simple(self): + a = [[8, 2, 3], [2, 9, 3], [3, 3, 6]] + c = cholesky(a) + assert_array_almost_equal(dot(transpose(c), c), a) + c = transpose(c) + a = dot(c, transpose(c)) + assert_array_almost_equal(cholesky(a, lower=1), c) + + def test_check_finite(self): + a = [[8, 2, 3], [2, 9, 3], [3, 3, 6]] + c = cholesky(a, check_finite=False) + assert_array_almost_equal(dot(transpose(c), c), a) + c = transpose(c) + a = dot(c, transpose(c)) + assert_array_almost_equal(cholesky(a, lower=1, check_finite=False), c) + + def test_simple_complex(self): + m = array([[3+1j, 3+4j, 5], [0, 2+2j, 2+7j], [0, 0, 7+4j]]) + a = dot(transpose(conjugate(m)), m) + c = cholesky(a) + a1 = dot(transpose(conjugate(c)), c) + assert_array_almost_equal(a, a1) + c = transpose(c) + a = dot(c, transpose(conjugate(c))) + assert_array_almost_equal(cholesky(a, lower=1), c) + + def test_random(self): + n = 20 + for k in range(2): + m = random([n, n]) + for i in range(n): + m[i, i] = 20*(.1+m[i, i]) + a = dot(transpose(m), m) + c = cholesky(a) + a1 = dot(transpose(c), c) + assert_array_almost_equal(a, a1) + c = transpose(c) + a = dot(c, transpose(c)) + assert_array_almost_equal(cholesky(a, lower=1), c) + + def test_random_complex(self): + n = 20 + for k in range(2): + m = random([n, n])+1j*random([n, n]) + for i in range(n): + m[i, i] = 20*(.1+abs(m[i, i])) + a = dot(transpose(conjugate(m)), m) + c = cholesky(a) + a1 = dot(transpose(conjugate(c)), c) + assert_array_almost_equal(a, a1) + c = transpose(c) + a = dot(c, transpose(conjugate(c))) + assert_array_almost_equal(cholesky(a, lower=1), c) + + @pytest.mark.xslow + def test_int_overflow(self): + # regression test for + # https://github.com/scipy/scipy/issues/17436 + # the problem was an int overflow in zeroing out + # the unused triangular part + n = 47_000 + x = np.eye(n, dtype=np.float64, order='F') + x[:4, :4] = np.array([[4, -2, 3, -1], + [-2, 4, -3, 1], + [3, -3, 5, 0], + [-1, 1, 0, 5]]) + + cholesky(x, check_finite=False, overwrite_a=True) # should not segfault + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt, dt_b): + a = empty((0, 0), dtype=dt) + + c = cholesky(a) + assert c.shape == (0, 0) + assert c.dtype == cholesky(np.eye(2, dtype=dt)).dtype + + c_and_lower = (c, True) + b = np.asarray([], dtype=dt_b) + x = cho_solve(c_and_lower, b) + assert x.shape == (0,) + assert x.dtype == cho_solve((np.eye(2, dtype=dt), True), + np.ones(2, dtype=dt_b)).dtype + + b = empty((0, 0), dtype=dt_b) + x = cho_solve(c_and_lower, b) + assert x.shape == (0, 0) + assert x.dtype == cho_solve((np.eye(2, dtype=dt), True), + np.ones(2, dtype=dt_b)).dtype + + a1 = array([]) + a2 = array([[]]) + a3 = [] + a4 = [[]] + for x in ([a1, a2, a3, a4]): + assert_raises(ValueError, cholesky, x) + + +class TestCholeskyBanded: + """Tests for cholesky_banded() and cho_solve_banded.""" + + def test_check_finite(self): + # Symmetric positive definite banded matrix `a` + a = array([[4.0, 1.0, 0.0, 0.0], + [1.0, 4.0, 0.5, 0.0], + [0.0, 0.5, 4.0, 0.2], + [0.0, 0.0, 0.2, 4.0]]) + # Banded storage form of `a`. + ab = array([[-1.0, 1.0, 0.5, 0.2], + [4.0, 4.0, 4.0, 4.0]]) + c = cholesky_banded(ab, lower=False, check_finite=False) + ufac = zeros_like(a) + ufac[list(range(4)), list(range(4))] = c[-1] + ufac[(0, 1, 2), (1, 2, 3)] = c[0, 1:] + assert_array_almost_equal(a, dot(ufac.T, ufac)) + + b = array([0.0, 0.5, 4.2, 4.2]) + x = cho_solve_banded((c, False), b, check_finite=False) + assert_array_almost_equal(x, [0.0, 0.0, 1.0, 1.0]) + + def test_upper_real(self): + # Symmetric positive definite banded matrix `a` + a = array([[4.0, 1.0, 0.0, 0.0], + [1.0, 4.0, 0.5, 0.0], + [0.0, 0.5, 4.0, 0.2], + [0.0, 0.0, 0.2, 4.0]]) + # Banded storage form of `a`. + ab = array([[-1.0, 1.0, 0.5, 0.2], + [4.0, 4.0, 4.0, 4.0]]) + c = cholesky_banded(ab, lower=False) + ufac = zeros_like(a) + ufac[list(range(4)), list(range(4))] = c[-1] + ufac[(0, 1, 2), (1, 2, 3)] = c[0, 1:] + assert_array_almost_equal(a, dot(ufac.T, ufac)) + + b = array([0.0, 0.5, 4.2, 4.2]) + x = cho_solve_banded((c, False), b) + assert_array_almost_equal(x, [0.0, 0.0, 1.0, 1.0]) + + def test_upper_complex(self): + # Hermitian positive definite banded matrix `a` + a = array([[4.0, 1.0, 0.0, 0.0], + [1.0, 4.0, 0.5, 0.0], + [0.0, 0.5, 4.0, -0.2j], + [0.0, 0.0, 0.2j, 4.0]]) + # Banded storage form of `a`. + ab = array([[-1.0, 1.0, 0.5, -0.2j], + [4.0, 4.0, 4.0, 4.0]]) + c = cholesky_banded(ab, lower=False) + ufac = zeros_like(a) + ufac[list(range(4)), list(range(4))] = c[-1] + ufac[(0, 1, 2), (1, 2, 3)] = c[0, 1:] + assert_array_almost_equal(a, dot(ufac.conj().T, ufac)) + + b = array([0.0, 0.5, 4.0-0.2j, 0.2j + 4.0]) + x = cho_solve_banded((c, False), b) + assert_array_almost_equal(x, [0.0, 0.0, 1.0, 1.0]) + + def test_lower_real(self): + # Symmetric positive definite banded matrix `a` + a = array([[4.0, 1.0, 0.0, 0.0], + [1.0, 4.0, 0.5, 0.0], + [0.0, 0.5, 4.0, 0.2], + [0.0, 0.0, 0.2, 4.0]]) + # Banded storage form of `a`. + ab = array([[4.0, 4.0, 4.0, 4.0], + [1.0, 0.5, 0.2, -1.0]]) + c = cholesky_banded(ab, lower=True) + lfac = zeros_like(a) + lfac[list(range(4)), list(range(4))] = c[0] + lfac[(1, 2, 3), (0, 1, 2)] = c[1, :3] + assert_array_almost_equal(a, dot(lfac, lfac.T)) + + b = array([0.0, 0.5, 4.2, 4.2]) + x = cho_solve_banded((c, True), b) + assert_array_almost_equal(x, [0.0, 0.0, 1.0, 1.0]) + + def test_lower_complex(self): + # Hermitian positive definite banded matrix `a` + a = array([[4.0, 1.0, 0.0, 0.0], + [1.0, 4.0, 0.5, 0.0], + [0.0, 0.5, 4.0, -0.2j], + [0.0, 0.0, 0.2j, 4.0]]) + # Banded storage form of `a`. + ab = array([[4.0, 4.0, 4.0, 4.0], + [1.0, 0.5, 0.2j, -1.0]]) + c = cholesky_banded(ab, lower=True) + lfac = zeros_like(a) + lfac[list(range(4)), list(range(4))] = c[0] + lfac[(1, 2, 3), (0, 1, 2)] = c[1, :3] + assert_array_almost_equal(a, dot(lfac, lfac.conj().T)) + + b = array([0.0, 0.5j, 3.8j, 3.8]) + x = cho_solve_banded((c, True), b) + assert_array_almost_equal(x, [0.0, 0.0, 1.0j, 1.0]) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt, dt_b): + ab = empty((0, 0), dtype=dt) + + cb = cholesky_banded(ab) + assert cb.shape == (0, 0) + + m = cholesky_banded(np.array([[0, 0], [1, 1]], dtype=dt)) + assert cb.dtype == m.dtype + + cb_and_lower = (cb, True) + b = np.asarray([], dtype=dt_b) + x = cho_solve_banded(cb_and_lower, b) + assert x.shape == (0,) + + dtype_nonempty = cho_solve_banded((m, True), np.ones(2, dtype=dt_b)).dtype + assert x.dtype == dtype_nonempty + + b = empty((0, 0), dtype=dt_b) + x = cho_solve_banded(cb_and_lower, b) + assert x.shape == (0, 0) + assert x.dtype == dtype_nonempty + + +class TestOverwrite: + def test_cholesky(self): + assert_no_overwrite(cholesky, [(3, 3)]) + + def test_cho_factor(self): + assert_no_overwrite(cho_factor, [(3, 3)]) + + def test_cho_solve(self): + x = array([[2, -1, 0], [-1, 2, -1], [0, -1, 2]]) + xcho = cho_factor(x) + assert_no_overwrite(lambda b: cho_solve(xcho, b), [(3,)]) + + def test_cholesky_banded(self): + assert_no_overwrite(cholesky_banded, [(2, 3)]) + + def test_cho_solve_banded(self): + x = array([[0, -1, -1], [2, 2, 2]]) + xcho = cholesky_banded(x) + assert_no_overwrite(lambda b: cho_solve_banded((xcho, False), b), + [(3,)]) + +class TestChoFactor: + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + x, lower = cho_factor(a) + + assert x.shape == (0, 0) + + xx, lower = cho_factor(np.eye(2, dtype=dt)) + assert x.dtype == xx.dtype diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cossin.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cossin.py new file mode 100644 index 0000000000000000000000000000000000000000..c43d3e643d3ff6f8745386113d8a08091cb01c64 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_cossin.py @@ -0,0 +1,314 @@ +import pytest +import numpy as np +from numpy.random import default_rng +from numpy.testing import assert_allclose + +from scipy import linalg +from scipy.linalg.lapack import _compute_lwork +from scipy.stats import ortho_group, unitary_group +from scipy.linalg import cossin, get_lapack_funcs + +REAL_DTYPES = (np.float32, np.float64) +COMPLEX_DTYPES = (np.complex64, np.complex128) +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +@pytest.mark.parametrize('dtype_', DTYPES) +@pytest.mark.parametrize('m, p, q', + [ + (2, 1, 1), + (3, 2, 1), + (3, 1, 2), + (4, 2, 2), + (4, 1, 2), + (40, 12, 20), + (40, 30, 1), + (40, 1, 30), + (100, 50, 1), + (100, 50, 50), + ]) +@pytest.mark.parametrize('swap_sign', [True, False]) +def test_cossin(dtype_, m, p, q, swap_sign): + rng = default_rng(1708093570726217) + if dtype_ in COMPLEX_DTYPES: + x = np.array(unitary_group.rvs(m, random_state=rng), dtype=dtype_) + else: + x = np.array(ortho_group.rvs(m, random_state=rng), dtype=dtype_) + + u, cs, vh = cossin(x, p, q, + swap_sign=swap_sign) + assert_allclose(x, u @ cs @ vh, rtol=0., atol=m*1e3*np.finfo(dtype_).eps) + assert u.dtype == dtype_ + # Test for float32 or float 64 + assert cs.dtype == np.real(u).dtype + assert vh.dtype == dtype_ + + u, cs, vh = cossin([x[:p, :q], x[:p, q:], x[p:, :q], x[p:, q:]], + swap_sign=swap_sign) + assert_allclose(x, u @ cs @ vh, rtol=0., atol=m*1e3*np.finfo(dtype_).eps) + assert u.dtype == dtype_ + assert cs.dtype == np.real(u).dtype + assert vh.dtype == dtype_ + + _, cs2, vh2 = cossin(x, p, q, + compute_u=False, + swap_sign=swap_sign) + assert_allclose(cs, cs2, rtol=0., atol=10*np.finfo(dtype_).eps) + assert_allclose(vh, vh2, rtol=0., atol=10*np.finfo(dtype_).eps) + + u2, cs2, _ = cossin(x, p, q, + compute_vh=False, + swap_sign=swap_sign) + assert_allclose(u, u2, rtol=0., atol=10*np.finfo(dtype_).eps) + assert_allclose(cs, cs2, rtol=0., atol=10*np.finfo(dtype_).eps) + + _, cs2, _ = cossin(x, p, q, + compute_u=False, + compute_vh=False, + swap_sign=swap_sign) + assert_allclose(cs, cs2, rtol=0., atol=10*np.finfo(dtype_).eps) + + +def test_cossin_mixed_types(): + rng = default_rng(1708093736390459) + x = np.array(ortho_group.rvs(4, random_state=rng), dtype=np.float64) + u, cs, vh = cossin([x[:2, :2], + np.array(x[:2, 2:], dtype=np.complex128), + x[2:, :2], + x[2:, 2:]]) + + assert u.dtype == np.complex128 + assert cs.dtype == np.float64 + assert vh.dtype == np.complex128 + assert_allclose(x, u @ cs @ vh, rtol=0., + atol=1e4 * np.finfo(np.complex128).eps) + + +def test_cossin_error_incorrect_subblocks(): + with pytest.raises(ValueError, match="be due to missing p, q arguments."): + cossin(([1, 2], [3, 4, 5], [6, 7], [8, 9, 10])) + + +def test_cossin_error_empty_subblocks(): + with pytest.raises(ValueError, match="x11.*empty"): + cossin(([], [], [], [])) + with pytest.raises(ValueError, match="x12.*empty"): + cossin(([1, 2], [], [6, 7], [8, 9, 10])) + with pytest.raises(ValueError, match="x21.*empty"): + cossin(([1, 2], [3, 4, 5], [], [8, 9, 10])) + with pytest.raises(ValueError, match="x22.*empty"): + cossin(([1, 2], [3, 4, 5], [2], [])) + + +def test_cossin_error_missing_partitioning(): + with pytest.raises(ValueError, match=".*exactly four arrays.* got 2"): + cossin(unitary_group.rvs(2)) + + with pytest.raises(ValueError, match=".*might be due to missing p, q"): + cossin(unitary_group.rvs(4)) + + +def test_cossin_error_non_iterable(): + with pytest.raises(ValueError, match="containing the subblocks of X"): + cossin(12j) + +def test_cossin_error_invalid_shape(): + # Invalid x12 dimensions + p, q = 3, 4 + invalid_x12 = np.ones((p, q + 2)) + valid_ones = np.ones((p, q)) + with pytest.raises(ValueError, + match=r"Invalid x12 dimensions: desired \(3, 4\), got \(3, 6\)"): + cossin((valid_ones, invalid_x12, valid_ones, valid_ones)) + + # Invalid x21 dimensions + invalid_x21 = np.ones(p + 2) + with pytest.raises(ValueError, + match=r"Invalid x21 dimensions: desired \(3, 4\), got \(1, 5\)"): + cossin((valid_ones, valid_ones, invalid_x21, valid_ones)) + +def test_cossin_error_non_square(): + with pytest.raises(ValueError, match="only supports square"): + cossin(np.array([[1, 2]]), 1, 1) + + +def test_cossin_error_partitioning(): + x = np.array(ortho_group.rvs(4), dtype=np.float64) + with pytest.raises(ValueError, match="invalid p=0.*0= m) or (q >= m): + pytest.skip("`0 < p < m` and `0 < q < m` must hold") + + # Generate unitary input + rng = np.random.default_rng(329548272348596421) + X = unitary_group.rvs(m, random_state=rng) + np.testing.assert_allclose(X @ X.conj().T, np.eye(m), atol=1e-15) + + # Perform the decomposition + u0, cs0, vh0 = linalg.cossin(X, p=p, q=q, separate=True, swap_sign=swap_sign) + u1, u2 = u0 + v1, v2 = vh0 + v1, v2 = v1.conj().T, v2.conj().T + + # "U1, U2, V1, V2 are square orthogonal/unitary matrices + # of dimensions (p,p), (m-p,m-p), (q,q), and (m-q,m-q) respectively" + np.testing.assert_allclose(u1 @ u1.conj().T, np.eye(p), atol=1e-13) + np.testing.assert_allclose(u2 @ u2.conj().T, np.eye(m-p), atol=1e-13) + np.testing.assert_allclose(v1 @ v1.conj().T, np.eye(q), atol=1e-13) + np.testing.assert_allclose(v2 @ v2.conj().T, np.eye(m-q), atol=1e-13) + + # "and C and S are (r, r) nonnegative diagonal matrices..." + C = np.diag(np.cos(cs0)) + S = np.diag(np.sin(cs0)) + # "...satisfying C^2 + S^2 = I where r = min(p, m-p, q, m-q)." + r = min(p, m-p, q, m-q) + np.testing.assert_allclose(C**2 + S**2, np.eye(r)) + + # "Moreover, the rank of the identity matrices are + # min(p, q) - r, min(p, m - q) - r, min(m - p, q) - r, + # and min(m - p, m - q) - r respectively." + I11 = np.eye(min(p, q) - r) + I12 = np.eye(min(p, m - q) - r) + I21 = np.eye(min(m - p, q) - r) + I22 = np.eye(min(m - p, m - q) - r) + + # From: + # ┌ ┐ + # │ I 0 0 │ 0 0 0 │ + # ┌ ┐ ┌ ┐│ 0 C 0 │ 0 -S 0 │┌ ┐* + # │ X11 │ X12 │ │ U1 │ ││ 0 0 0 │ 0 0 -I ││ V1 │ │ + # │ ────┼──── │ = │────┼────││─────────┼─────────││────┼────│ + # │ X21 │ X22 │ │ │ U2 ││ 0 0 0 │ I 0 0 ││ │ V2 │ + # └ ┘ └ ┘│ 0 S 0 │ 0 C 0 │└ ┘ + # │ 0 0 I │ 0 0 0 │ + # └ ┘ + + # We can see that U and V are block diagonal matrices like so: + U = linalg.block_diag(u1, u2) + V = linalg.block_diag(v1, v2) + + # And the center matrix, which we'll call Q here, must be: + Q11 = np.zeros((u1.shape[1], v1.shape[0])) + IC11 = linalg.block_diag(I11, C) + Q11[:IC11.shape[0], :IC11.shape[1]] = IC11 + + Q12 = np.zeros((u1.shape[1], v2.shape[0])) + SI12 = linalg.block_diag(S, I12) if swap_sign else linalg.block_diag(-S, -I12) + Q12[-SI12.shape[0]:, -SI12.shape[1]:] = SI12 + + Q21 = np.zeros((u2.shape[1], v1.shape[0])) + SI21 = linalg.block_diag(-S, -I21) if swap_sign else linalg.block_diag(S, I21) + Q21[-SI21.shape[0]:, -SI21.shape[1]:] = SI21 + + Q22 = np.zeros((u2.shape[1], v2.shape[0])) + IC22 = linalg.block_diag(I22, C) + Q22[:IC22.shape[0], :IC22.shape[1]] = IC22 + + Q = np.block([[Q11, Q12], [Q21, Q22]]) + + # Confirm that `cossin` decomposes `X` as shown + np.testing.assert_allclose(X, U @ Q @ V.conj().T) + + # And check that `separate=False` agrees + U0, CS0, Vh0 = linalg.cossin(X, p=p, q=q, swap_sign=swap_sign) + np.testing.assert_allclose(U, U0) + np.testing.assert_allclose(Q, CS0) + np.testing.assert_allclose(V, Vh0.conj().T) + + # Confirm that `compute_u`/`compute_vh` don't affect the results + kwargs = dict(p=p, q=q, swap_sign=swap_sign) + + # `compute_u=False` + u, cs, vh = linalg.cossin(X, separate=True, compute_u=False, **kwargs) + assert u[0].shape == (0, 0) # probably not ideal, but this is what it does + assert u[1].shape == (0, 0) + assert_allclose(cs, cs0, rtol=1e-15) + assert_allclose(vh[0], vh0[0], rtol=1e-15) + assert_allclose(vh[1], vh0[1], rtol=1e-15) + + U, CS, Vh = linalg.cossin(X, compute_u=False, **kwargs) + assert U.shape == (0, 0) + assert_allclose(CS, CS0, rtol=1e-15) + assert_allclose(Vh, Vh0, rtol=1e-15) + + # `compute_vh=False` + u, cs, vh = linalg.cossin(X, separate=True, compute_vh=False, **kwargs) + assert_allclose(u[0], u[0], rtol=1e-15) + assert_allclose(u[1], u[1], rtol=1e-15) + assert_allclose(cs, cs0, rtol=1e-15) + assert vh[0].shape == (0, 0) + assert vh[1].shape == (0, 0) + + U, CS, Vh = linalg.cossin(X, compute_vh=False, **kwargs) + assert_allclose(U, U0, rtol=1e-15) + assert_allclose(CS, CS0, rtol=1e-15) + assert Vh.shape == (0, 0) + + # `compute_u=False, compute_vh=False` + u, cs, vh = linalg.cossin(X, separate=True, compute_u=False, + compute_vh=False, **kwargs) + assert u[0].shape == (0, 0) + assert u[1].shape == (0, 0) + assert_allclose(cs, cs0, rtol=1e-15) + assert vh[0].shape == (0, 0) + assert vh[1].shape == (0, 0) + + U, CS, Vh = linalg.cossin(X, compute_u=False, compute_vh=False, **kwargs) + assert U.shape == (0, 0) + assert_allclose(CS, CS0, rtol=1e-15) + assert Vh.shape == (0, 0) + + +def test_indexing_bug_gh19365(): + # Regression test for gh-19365, which reported a bug with `separate=False` + rng = np.random.default_rng(32954827234421) + m = rng.integers(50, high=100) + p = rng.integers(10, 40) # always p < m + q = rng.integers(m - p + 1, m - 1) # always m-p < q < m + X = unitary_group.rvs(m, random_state=rng) # random unitary matrix + U, D, Vt = linalg.cossin(X, p=p, q=q, separate=False) + assert np.allclose(U @ D @ Vt, X) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_ldl.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_ldl.py new file mode 100644 index 0000000000000000000000000000000000000000..878b405096e353334d027edd07d799c3704f0a74 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_ldl.py @@ -0,0 +1,136 @@ +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_allclose, assert_ +from numpy import (array, eye, zeros, empty_like, empty, tril_indices_from, + tril, triu_indices_from, spacing, float32, float64, + complex64, complex128) +from numpy.exceptions import ComplexWarning +from scipy.linalg import ldl +import pytest + + +def test_args(): + A = eye(3) + # Nonsquare array + with pytest.raises(ValueError): + ldl(A[:, :2]) + # Complex matrix with imaginary diagonal entries with "hermitian=True" + with pytest.warns(ComplexWarning): + ldl(A*1j) + + +def test_empty_array(): + a = empty((0, 0), dtype=complex) + l, d, p = ldl(empty((0, 0))) + assert_array_almost_equal(l, empty_like(a)) + assert_array_almost_equal(d, empty_like(a)) + assert_array_almost_equal(p, array([], dtype=int)) + + +def test_simple(): + a = array([[-0.39-0.71j, 5.14-0.64j, -7.86-2.96j, 3.80+0.92j], + [5.14-0.64j, 8.86+1.81j, -3.52+0.58j, 5.32-1.59j], + [-7.86-2.96j, -3.52+0.58j, -2.83-0.03j, -1.54-2.86j], + [3.80+0.92j, 5.32-1.59j, -1.54-2.86j, -0.56+0.12j]]) + b = array([[5., 10, 1, 18], + [10., 2, 11, 1], + [1., 11, 19, 9], + [18., 1, 9, 0]]) + c = array([[52., 97, 112, 107, 50], + [97., 114, 89, 98, 13], + [112., 89, 64, 33, 6], + [107., 98, 33, 60, 73], + [50., 13, 6, 73, 77]]) + + d = array([[2., 2, -4, 0, 4], + [2., -2, -2, 10, -8], + [-4., -2, 6, -8, -4], + [0., 10, -8, 6, -6], + [4., -8, -4, -6, 10]]) + e = array([[-1.36+0.00j, 0+0j, 0+0j, 0+0j], + [1.58-0.90j, -8.87+0j, 0+0j, 0+0j], + [2.21+0.21j, -1.84+0.03j, -4.63+0j, 0+0j], + [3.91-1.50j, -1.78-1.18j, 0.11-0.11j, -1.84+0.00j]]) + for x in (b, c, d): + l, d, p = ldl(x) + assert_allclose(l.dot(d).dot(l.T), x, atol=spacing(1000.), rtol=0) + + u, d, p = ldl(x, lower=False) + assert_allclose(u.dot(d).dot(u.T), x, atol=spacing(1000.), rtol=0) + + l, d, p = ldl(a, hermitian=False) + assert_allclose(l.dot(d).dot(l.T), a, atol=spacing(1000.), rtol=0) + + u, d, p = ldl(a, lower=False, hermitian=False) + assert_allclose(u.dot(d).dot(u.T), a, atol=spacing(1000.), rtol=0) + + # Use upper part for the computation and use the lower part for comparison + l, d, p = ldl(e.conj().T, lower=0) + assert_allclose(tril(l.dot(d).dot(l.conj().T)-e), zeros((4, 4)), + atol=spacing(1000.), rtol=0) + + +def test_permutations(): + rng = np.random.default_rng(1234) + for _ in range(10): + n = rng.integers(1, 100) + # Random real/complex array + x = rng.random((n, n)) + 0 if rng.integers(2) else rng.random((n, n))*1j + x = x + x.conj().T + x += eye(n)*rng.integers(5, 1e6) + l_ind = tril_indices_from(x, k=-1) + u_ind = triu_indices_from(x, k=1) + + # Test whether permutations lead to a triangular array + u, d, p = ldl(x, lower=0) + # lower part should be zero + assert_(not any(u[p, :][l_ind]), f'Spin {_} failed') + + l, d, p = ldl(x, lower=1) + # upper part should be zero + assert_(not any(l[p, :][u_ind]), f'Spin {_} failed') + + +@pytest.mark.parametrize("dtype", [float32, float64]) +@pytest.mark.parametrize("n", [30, 150]) +def test_ldl_type_size_combinations_real(n, dtype): + rng = np.random.default_rng(1234) + msg = (f"Failed for size: {n}, dtype: {dtype}") + + x = rng.random((n, n)).astype(dtype) + x = x + x.T + x += eye(n, dtype=dtype)*dtype(rng.integers(5, 1e6)) + + l, d1, p = ldl(x) + u, d2, p = ldl(x, lower=0) + rtol = 1e-4 if dtype is float32 else 1e-10 + assert_allclose(l.dot(d1).dot(l.T), x, rtol=rtol, err_msg=msg) + assert_allclose(u.dot(d2).dot(u.T), x, rtol=rtol, err_msg=msg) + + +@pytest.mark.parametrize("dtype", [complex64, complex128]) +@pytest.mark.parametrize("n", [30, 150]) +def test_ldl_type_size_combinations_complex(n, dtype): + rng = np.random.default_rng(1234) + msg1 = (f"Her failed for size: {n}, dtype: {dtype}") + msg2 = (f"Sym failed for size: {n}, dtype: {dtype}") + + # Complex hermitian upper/lower + x = (rng.random((n, n))+1j*rng.random((n, n))).astype(dtype) + x = x+x.conj().T + x += eye(n, dtype=dtype)*dtype(rng.integers(5, 1e6)) + + l, d1, p = ldl(x) + u, d2, p = ldl(x, lower=0) + rtol = 2e-4 if dtype is complex64 else 1e-10 + assert_allclose(l.dot(d1).dot(l.conj().T), x, rtol=rtol, err_msg=msg1) + assert_allclose(u.dot(d2).dot(u.conj().T), x, rtol=rtol, err_msg=msg1) + + # Complex symmetric upper/lower + x = (rng.random((n, n))+1j*rng.random((n, n))).astype(dtype) + x = x+x.T + x += eye(n, dtype=dtype)*dtype(rng.integers(5, 1e6)) + + l, d1, p = ldl(x, hermitian=0) + u, d2, p = ldl(x, lower=0, hermitian=0) + assert_allclose(l.dot(d1).dot(l.T), x, rtol=rtol, err_msg=msg2) + assert_allclose(u.dot(d2).dot(u.T), x, rtol=rtol, err_msg=msg2) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_lu.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_lu.py new file mode 100644 index 0000000000000000000000000000000000000000..da0beccf1f0e66baf4ac4ec80d7ff7129b2df345 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_lu.py @@ -0,0 +1,308 @@ +import pytest +from pytest import raises as assert_raises + +import numpy as np +from scipy.linalg import lu, lu_factor, lu_solve, get_lapack_funcs, solve +from numpy.testing import assert_allclose, assert_array_equal, assert_equal + + +REAL_DTYPES = [np.float32, np.float64] +COMPLEX_DTYPES = [np.complex64, np.complex128] +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +class TestLU: + def setup_method(self): + self.rng = np.random.default_rng(1682281250228846) + + def test_old_lu_smoke_tests(self): + "Tests from old fortran based lu test suite" + a = np.array([[1, 2, 3], [1, 2, 3], [2, 5, 6]]) + p, l, u = lu(a) + result_lu = np.array([[2., 5., 6.], [0.5, -0.5, 0.], [0.5, 1., 0.]]) + assert_allclose(p, np.rot90(np.eye(3))) + assert_allclose(l, np.tril(result_lu, k=-1)+np.eye(3)) + assert_allclose(u, np.triu(result_lu)) + + a = np.array([[1, 2, 3], [1, 2, 3], [2, 5j, 6]]) + p, l, u = lu(a) + result_lu = np.array([[2., 5.j, 6.], [0.5, 2-2.5j, 0.], [0.5, 1., 0.]]) + assert_allclose(p, np.rot90(np.eye(3))) + assert_allclose(l, np.tril(result_lu, k=-1)+np.eye(3)) + assert_allclose(u, np.triu(result_lu)) + + b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + p, l, u = lu(b) + assert_allclose(p, np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])) + assert_allclose(l, np.array([[1, 0, 0], [1/7, 1, 0], [4/7, 0.5, 1]])) + assert_allclose(u, np.array([[7, 8, 9], [0, 6/7, 12/7], [0, 0, 0]]), + rtol=0., atol=1e-14) + + cb = np.array([[1.j, 2.j, 3.j], [4j, 5j, 6j], [7j, 8j, 9j]]) + p, l, u = lu(cb) + assert_allclose(p, np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])) + assert_allclose(l, np.array([[1, 0, 0], [1/7, 1, 0], [4/7, 0.5, 1]])) + assert_allclose(u, np.array([[7, 8, 9], [0, 6/7, 12/7], [0, 0, 0]])*1j, + rtol=0., atol=1e-14) + + # Rectangular matrices + hrect = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 12]]) + p, l, u = lu(hrect) + assert_allclose(p, np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])) + assert_allclose(l, np.array([[1, 0, 0], [1/9, 1, 0], [5/9, 0.5, 1]])) + assert_allclose(u, np.array([[9, 10, 12, 12], [0, 8/9, 15/9, 24/9], + [0, 0, -0.5, 0]]), rtol=0., atol=1e-14) + + chrect = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 12]])*1.j + p, l, u = lu(chrect) + assert_allclose(p, np.array([[0, 1, 0], [0, 0, 1], [1, 0, 0]])) + assert_allclose(l, np.array([[1, 0, 0], [1/9, 1, 0], [5/9, 0.5, 1]])) + assert_allclose(u, np.array([[9, 10, 12, 12], [0, 8/9, 15/9, 24/9], + [0, 0, -0.5, 0]])*1j, rtol=0., atol=1e-14) + + vrect = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 12, 12]]) + p, l, u = lu(vrect) + assert_allclose(p, np.eye(4)[[1, 3, 2, 0], :]) + assert_allclose(l, np.array([[1., 0, 0], [0.1, 1, 0], [0.7, -0.5, 1], + [0.4, 0.25, 0.5]])) + assert_allclose(u, np.array([[10, 12, 12], + [0, 0.8, 1.8], + [0, 0, 1.5]])) + + cvrect = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 12, 12]])*1j + p, l, u = lu(cvrect) + assert_allclose(p, np.eye(4)[[1, 3, 2, 0], :]) + assert_allclose(l, np.array([[1., 0, 0], + [0.1, 1, 0], + [0.7, -0.5, 1], + [0.4, 0.25, 0.5]])) + assert_allclose(u, np.array([[10, 12, 12], + [0, 0.8, 1.8], + [0, 0, 1.5]])*1j) + + @pytest.mark.parametrize('shape', [[2, 2], [2, 4], [4, 2], [20, 20], + [20, 4], [4, 20], [3, 2, 9, 9], + [2, 2, 17, 5], [2, 2, 11, 7]]) + def test_simple_lu_shapes_real_complex(self, shape): + a = self.rng.uniform(-10., 10., size=shape) + p, l, u = lu(a) + assert_allclose(a, p @ l @ u) + pl, u = lu(a, permute_l=True) + assert_allclose(a, pl @ u) + + b = self.rng.uniform(-10., 10., size=shape)*1j + b += self.rng.uniform(-10, 10, size=shape) + pl, u = lu(b, permute_l=True) + assert_allclose(b, pl @ u) + + @pytest.mark.parametrize('shape', [[2, 2], [2, 4], [4, 2], [20, 20], + [20, 4], [4, 20]]) + def test_simple_lu_shapes_real_complex_2d_indices(self, shape): + a = self.rng.uniform(-10., 10., size=shape) + p, l, u = lu(a, p_indices=True) + assert_allclose(a, l[p, :] @ u) + + def test_1by1_input_output(self): + a = self.rng.random([4, 5, 1, 1], dtype=np.float32) + p, l, u = lu(a, p_indices=True) + assert_allclose(p, np.zeros(shape=(4, 5, 1), dtype=int)) + assert_allclose(l, np.ones(shape=(4, 5, 1, 1), dtype=np.float32)) + assert_allclose(u, a) + + a = self.rng.random([4, 5, 1, 1], dtype=np.float32) + p, l, u = lu(a) + assert_allclose(p, np.ones(shape=(4, 5, 1, 1), dtype=np.float32)) + assert_allclose(l, np.ones(shape=(4, 5, 1, 1), dtype=np.float32)) + assert_allclose(u, a) + + pl, u = lu(a, permute_l=True) + assert_allclose(pl, np.ones(shape=(4, 5, 1, 1), dtype=np.float32)) + assert_allclose(u, a) + + a = self.rng.random([4, 5, 1, 1], dtype=np.float32)*np.complex64(1.j) + p, l, u = lu(a) + assert_allclose(p, np.ones(shape=(4, 5, 1, 1), dtype=np.complex64)) + assert_allclose(l, np.ones(shape=(4, 5, 1, 1), dtype=np.complex64)) + assert_allclose(u, a) + + def test_empty_edge_cases(self): + a = np.empty([0, 0]) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(0, 0), dtype=np.float64)) + assert_allclose(l, np.empty(shape=(0, 0), dtype=np.float64)) + assert_allclose(u, np.empty(shape=(0, 0), dtype=np.float64)) + + a = np.empty([0, 3], dtype=np.float16) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(0, 0), dtype=np.float32)) + assert_allclose(l, np.empty(shape=(0, 0), dtype=np.float32)) + assert_allclose(u, np.empty(shape=(0, 3), dtype=np.float32)) + + a = np.empty([3, 0], dtype=np.complex64) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(0, 0), dtype=np.float32)) + assert_allclose(l, np.empty(shape=(3, 0), dtype=np.complex64)) + assert_allclose(u, np.empty(shape=(0, 0), dtype=np.complex64)) + p, l, u = lu(a, p_indices=True) + assert_allclose(p, np.empty(shape=(0,), dtype=int)) + assert_allclose(l, np.empty(shape=(3, 0), dtype=np.complex64)) + assert_allclose(u, np.empty(shape=(0, 0), dtype=np.complex64)) + pl, u = lu(a, permute_l=True) + assert_allclose(pl, np.empty(shape=(3, 0), dtype=np.complex64)) + assert_allclose(u, np.empty(shape=(0, 0), dtype=np.complex64)) + + a = np.empty([3, 0, 0], dtype=np.complex64) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(3, 0, 0), dtype=np.float32)) + assert_allclose(l, np.empty(shape=(3, 0, 0), dtype=np.complex64)) + assert_allclose(u, np.empty(shape=(3, 0, 0), dtype=np.complex64)) + + a = np.empty([0, 0, 3]) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(0, 0, 0))) + assert_allclose(l, np.empty(shape=(0, 0, 0))) + assert_allclose(u, np.empty(shape=(0, 0, 3))) + + with assert_raises(ValueError, match='at least two-dimensional'): + lu(np.array([])) + + a = np.array([[]]) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(0, 0))) + assert_allclose(l, np.empty(shape=(1, 0))) + assert_allclose(u, np.empty(shape=(0, 0))) + + a = np.array([[[]]]) + p, l, u = lu(a) + assert_allclose(p, np.empty(shape=(1, 0, 0))) + assert_allclose(l, np.empty(shape=(1, 1, 0))) + assert_allclose(u, np.empty(shape=(1, 0, 0))) + + +class TestLUFactor: + def setup_method(self): + self.rng = np.random.default_rng(1682281250228846) + + self.a = np.array([[1, 2, 3], [1, 2, 3], [2, 5, 6]]) + self.ca = np.array([[1, 2, 3], [1, 2, 3], [2, 5j, 6]]) + # Those matrices are more robust to detect problems in permutation + # matrices than the ones above + self.b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + self.cb = np.array([[1j, 2j, 3j], [4j, 5j, 6j], [7j, 8j, 9j]]) + + # Rectangular matrices + self.hrect = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 12, 12]]) + self.chrect = np.array([[1, 2, 3, 4], [5, 6, 7, 8], + [9, 10, 12, 12]]) * 1.j + + self.vrect = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 12, 12]]) + self.cvrect = 1.j * np.array([[1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 12, 12]]) + + # Medium sizes matrices + self.med = self.rng.random((30, 40)) + self.cmed = self.rng.random((30, 40)) + 1.j*self.rng.random((30, 40)) + + def _test_common_lu_factor(self, data): + l_and_u1, piv1 = lu_factor(data) + (getrf,) = get_lapack_funcs(("getrf",), (data,)) + l_and_u2, piv2, _ = getrf(data, overwrite_a=False) + assert_allclose(l_and_u1, l_and_u2) + assert_allclose(piv1, piv2) + + # Simple tests. + # For lu_factor gives a LinAlgWarning because these matrices are singular + def test_hrectangular(self): + self._test_common_lu_factor(self.hrect) + + def test_vrectangular(self): + self._test_common_lu_factor(self.vrect) + + def test_hrectangular_complex(self): + self._test_common_lu_factor(self.chrect) + + def test_vrectangular_complex(self): + self._test_common_lu_factor(self.cvrect) + + # Bigger matrices + def test_medium1(self): + """Check lu decomposition on medium size, rectangular matrix.""" + self._test_common_lu_factor(self.med) + + def test_medium1_complex(self): + """Check lu decomposition on medium size, rectangular matrix.""" + self._test_common_lu_factor(self.cmed) + + def test_check_finite(self): + p, l, u = lu(self.a, check_finite=False) + assert_allclose(p @ l @ u, self.a) + + def test_simple_known(self): + # Ticket #1458 + for order in ['C', 'F']: + A = np.array([[2, 1], [0, 1.]], order=order) + LU, P = lu_factor(A) + assert_allclose(LU, np.array([[2, 1], [0, 1]])) + assert_array_equal(P, np.array([0, 1])) + + @pytest.mark.parametrize("m", [0, 1, 2]) + @pytest.mark.parametrize("n", [0, 1, 2]) + @pytest.mark.parametrize('dtype', DTYPES) + def test_shape_dtype(self, m, n, dtype): + k = min(m, n) + + a = np.eye(m, n, dtype=dtype) + lu, p = lu_factor(a) + assert_equal(lu.shape, (m, n)) + assert_equal(lu.dtype, dtype) + assert_equal(p.shape, (k,)) + assert_equal(p.dtype, np.int32) + + @pytest.mark.parametrize(("m", "n"), [(0, 0), (0, 2), (2, 0)]) + def test_empty(self, m, n): + a = np.zeros((m, n)) + lu, p = lu_factor(a) + assert_allclose(lu, np.empty((m, n))) + assert_allclose(p, np.arange(0)) + + +class TestLUSolve: + def setup_method(self): + self.rng = np.random.default_rng(1682281250228846) + + def test_lu(self): + a0 = self.rng.random((10, 10)) + b = self.rng.random((10,)) + + for order in ['C', 'F']: + a = np.array(a0, order=order) + x1 = solve(a, b) + lu_a = lu_factor(a) + x2 = lu_solve(lu_a, b) + assert_allclose(x1, x2) + + def test_check_finite(self): + a = self.rng.random((10, 10)) + b = self.rng.random((10,)) + x1 = solve(a, b) + lu_a = lu_factor(a, check_finite=False) + x2 = lu_solve(lu_a, b, check_finite=False) + assert_allclose(x1, x2) + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt, dt_b): + lu_and_piv = (np.empty((0, 0), dtype=dt), np.array([])) + b = np.asarray([], dtype=dt_b) + x = lu_solve(lu_and_piv, b) + assert x.shape == (0,) + + m = lu_solve((np.eye(2, dtype=dt), [0, 1]), np.ones(2, dtype=dt_b)) + assert x.dtype == m.dtype + + b = np.empty((0, 0), dtype=dt_b) + x = lu_solve(lu_and_piv, b) + assert x.shape == (0, 0) + assert x.dtype == m.dtype diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_polar.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_polar.py new file mode 100644 index 0000000000000000000000000000000000000000..607238842b3cc643d9665e40f29e41b15d8951a1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_polar.py @@ -0,0 +1,110 @@ +import pytest +import numpy as np +from numpy.linalg import norm +from numpy.testing import (assert_, assert_allclose, assert_equal) +from scipy.linalg import polar, eigh + + +diag2 = np.array([[2, 0], [0, 3]]) +a13 = np.array([[1, 2, 2]]) + +precomputed_cases = [ + [[[0]], 'right', [[1]], [[0]]], + [[[0]], 'left', [[1]], [[0]]], + [[[9]], 'right', [[1]], [[9]]], + [[[9]], 'left', [[1]], [[9]]], + [diag2, 'right', np.eye(2), diag2], + [diag2, 'left', np.eye(2), diag2], + [a13, 'right', a13/norm(a13[0]), a13.T.dot(a13)/norm(a13[0])], +] + +verify_cases = [ + [[1, 2], [3, 4]], + [[1, 2, 3]], + [[1], [2], [3]], + [[1, 2, 3], [3, 4, 0]], + [[1, 2], [3, 4], [5, 5]], + [[1, 2], [3, 4+5j]], + [[1, 2, 3j]], + [[1], [2], [3j]], + [[1, 2, 3+2j], [3, 4-1j, -4j]], + [[1, 2], [3-2j, 4+0.5j], [5, 5]], + [[10000, 10, 1], [-1, 2, 3j], [0, 1, 2]], + np.empty((0, 0)), + np.empty((0, 2)), + np.empty((2, 0)), +] + + +def check_precomputed_polar(a, side, expected_u, expected_p): + # Compare the result of the polar decomposition to a + # precomputed result. + u, p = polar(a, side=side) + assert_allclose(u, expected_u, atol=1e-15) + assert_allclose(p, expected_p, atol=1e-15) + + +def verify_polar(a): + # Compute the polar decomposition, and then verify that + # the result has all the expected properties. + product_atol = np.sqrt(np.finfo(float).eps) + + aa = np.asarray(a) + m, n = aa.shape + + u, p = polar(a, side='right') + assert_equal(u.shape, (m, n)) + assert_equal(p.shape, (n, n)) + # a = up + assert_allclose(u.dot(p), a, atol=product_atol) + if m >= n: + assert_allclose(u.conj().T.dot(u), np.eye(n), atol=1e-15) + else: + assert_allclose(u.dot(u.conj().T), np.eye(m), atol=1e-15) + # p is Hermitian positive semidefinite. + assert_allclose(p.conj().T, p) + evals = eigh(p, eigvals_only=True) + nonzero_evals = evals[abs(evals) > 1e-14] + assert_((nonzero_evals >= 0).all()) + + u, p = polar(a, side='left') + assert_equal(u.shape, (m, n)) + assert_equal(p.shape, (m, m)) + # a = pu + assert_allclose(p.dot(u), a, atol=product_atol) + if m >= n: + assert_allclose(u.conj().T.dot(u), np.eye(n), atol=1e-15) + else: + assert_allclose(u.dot(u.conj().T), np.eye(m), atol=1e-15) + # p is Hermitian positive semidefinite. + assert_allclose(p.conj().T, p) + evals = eigh(p, eigvals_only=True) + nonzero_evals = evals[abs(evals) > 1e-14] + assert_((nonzero_evals >= 0).all()) + + +def test_precomputed_cases(): + for a, side, expected_u, expected_p in precomputed_cases: + check_precomputed_polar(a, side, expected_u, expected_p) + + +def test_verify_cases(): + for a in verify_cases: + verify_polar(a) + +@pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) +@pytest.mark.parametrize('shape', [(0, 0), (0, 2), (2, 0)]) +@pytest.mark.parametrize('side', ['left', 'right']) +def test_empty(dt, shape, side): + a = np.empty(shape, dtype=dt) + m, n = shape + p_shape = (m, m) if side == 'left' else (n, n) + + u, p = polar(a, side=side) + u_n, p_n = polar(np.eye(5, dtype=dt)) + + assert_equal(u.dtype, u_n.dtype) + assert_equal(p.dtype, p_n.dtype) + assert u.shape == shape + assert p.shape == p_shape + assert np.all(p == 0) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_update.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_update.py new file mode 100644 index 0000000000000000000000000000000000000000..41b14eddbbde94b0929bc6eb91861b08fd382b22 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_decomp_update.py @@ -0,0 +1,1700 @@ +import itertools + +import numpy as np +from numpy.testing import assert_, assert_allclose, assert_equal +from pytest import raises as assert_raises +from scipy import linalg +import scipy.linalg._decomp_update as _decomp_update +from scipy.linalg._decomp_update import qr_delete, qr_update, qr_insert + +def assert_unitary(a, rtol=None, atol=None, assert_sqr=True): + if rtol is None: + rtol = 10.0 ** -(np.finfo(a.dtype).precision-2) + if atol is None: + atol = 10*np.finfo(a.dtype).eps + + if assert_sqr: + assert_(a.shape[0] == a.shape[1], 'unitary matrices must be square') + aTa = np.dot(a.T.conj(), a) + assert_allclose(aTa, np.eye(a.shape[1]), rtol=rtol, atol=atol) + +def assert_upper_tri(a, rtol=None, atol=None): + if rtol is None: + rtol = 10.0 ** -(np.finfo(a.dtype).precision-2) + if atol is None: + atol = 2*np.finfo(a.dtype).eps + mask = np.tri(a.shape[0], a.shape[1], -1, np.bool_) + assert_allclose(a[mask], 0.0, rtol=rtol, atol=atol) + +def check_qr(q, r, a, rtol, atol, assert_sqr=True): + assert_unitary(q, rtol, atol, assert_sqr) + assert_upper_tri(r, rtol, atol) + assert_allclose(q.dot(r), a, rtol=rtol, atol=atol) + +def make_strided(arrs): + strides = [(3, 7), (2, 2), (3, 4), (4, 2), (5, 4), (2, 3), (2, 1), (4, 5)] + kmax = len(strides) + k = 0 + ret = [] + for a in arrs: + if a.ndim == 1: + s = strides[k % kmax] + k += 1 + base = np.zeros(s[0]*a.shape[0]+s[1], a.dtype) + view = base[s[1]::s[0]] + view[...] = a + elif a.ndim == 2: + s = strides[k % kmax] + t = strides[(k+1) % kmax] + k += 2 + base = np.zeros((s[0]*a.shape[0]+s[1], t[0]*a.shape[1]+t[1]), + a.dtype) + view = base[s[1]::s[0], t[1]::t[0]] + view[...] = a + else: + raise ValueError('make_strided only works for ndim = 1 or' + ' 2 arrays') + ret.append(view) + return ret + +def negate_strides(arrs): + ret = [] + for a in arrs: + b = np.zeros_like(a) + if b.ndim == 2: + b = b[::-1, ::-1] + elif b.ndim == 1: + b = b[::-1] + else: + raise ValueError('negate_strides only works for ndim = 1 or' + ' 2 arrays') + b[...] = a + ret.append(b) + return ret + +def nonitemsize_strides(arrs): + out = [] + for a in arrs: + a_dtype = a.dtype + b = np.zeros(a.shape, [('a', a_dtype), ('junk', 'S1')]) + c = b.getfield(a_dtype) + c[...] = a + out.append(c) + return out + + +def make_nonnative(arrs): + return [a.astype(a.dtype.newbyteorder()) for a in arrs] + + +class BaseQRdeltas: + def setup_method(self): + self.rtol = 10.0 ** -(np.finfo(self.dtype).precision-2) + self.atol = 10 * np.finfo(self.dtype).eps + + def generate(self, type, mode='full'): + rng = np.random.default_rng(29382) + shape = {'sqr': (8, 8), 'tall': (12, 7), 'fat': (7, 12), + 'Mx1': (8, 1), '1xN': (1, 8), '1x1': (1, 1)}[type] + a = rng.random(shape) + if np.iscomplexobj(self.dtype.type(1)): + b = rng.random(shape) + a = a + 1j * b + a = a.astype(self.dtype) + q, r = linalg.qr(a, mode=mode) + return a, q, r + +class BaseQRdelete(BaseQRdeltas): + def test_sqr_1_row(self): + a, q, r = self.generate('sqr') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_p_row(self): + a, q, r = self.generate('sqr') + for ndel in range(2, 6): + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_1_col(self): + a, q, r = self.generate('sqr') + for col in range(r.shape[1]): + q1, r1 = qr_delete(q, r, col, which='col', overwrite_qr=False) + a1 = np.delete(a, col, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_p_col(self): + a, q, r = self.generate('sqr') + for ndel in range(2, 6): + for col in range(r.shape[1]-ndel): + q1, r1 = qr_delete(q, r, col, ndel, which='col', + overwrite_qr=False) + a1 = np.delete(a, slice(col, col+ndel), 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_1_row(self): + a, q, r = self.generate('tall') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_p_row(self): + a, q, r = self.generate('tall') + for ndel in range(2, 6): + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_1_col(self): + a, q, r = self.generate('tall') + for col in range(r.shape[1]): + q1, r1 = qr_delete(q, r, col, which='col', overwrite_qr=False) + a1 = np.delete(a, col, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_p_col(self): + a, q, r = self.generate('tall') + for ndel in range(2, 6): + for col in range(r.shape[1]-ndel): + q1, r1 = qr_delete(q, r, col, ndel, which='col', + overwrite_qr=False) + a1 = np.delete(a, slice(col, col+ndel), 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_1_row(self): + a, q, r = self.generate('fat') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_p_row(self): + a, q, r = self.generate('fat') + for ndel in range(2, 6): + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_1_col(self): + a, q, r = self.generate('fat') + for col in range(r.shape[1]): + q1, r1 = qr_delete(q, r, col, which='col', overwrite_qr=False) + a1 = np.delete(a, col, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_p_col(self): + a, q, r = self.generate('fat') + for ndel in range(2, 6): + for col in range(r.shape[1]-ndel): + q1, r1 = qr_delete(q, r, col, ndel, which='col', + overwrite_qr=False) + a1 = np.delete(a, slice(col, col+ndel), 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_economic_1_row(self): + # this test always starts and ends with an economic decomp. + a, q, r = self.generate('tall', 'economic') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + # for economic row deletes + # eco - prow = eco + # eco - prow = sqr + # eco - prow = fat + def base_economic_p_row_xxx(self, ndel): + a, q, r = self.generate('tall', 'economic') + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_p_row_economic(self): + # (12, 7) - (3, 7) = (9,7) --> stays economic + self.base_economic_p_row_xxx(3) + + def test_economic_p_row_sqr(self): + # (12, 7) - (5, 7) = (7, 7) --> becomes square + self.base_economic_p_row_xxx(5) + + def test_economic_p_row_fat(self): + # (12, 7) - (7,7) = (5, 7) --> becomes fat + self.base_economic_p_row_xxx(7) + + def test_economic_1_col(self): + a, q, r = self.generate('tall', 'economic') + for col in range(r.shape[1]): + q1, r1 = qr_delete(q, r, col, which='col', overwrite_qr=False) + a1 = np.delete(a, col, 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_p_col(self): + a, q, r = self.generate('tall', 'economic') + for ndel in range(2, 6): + for col in range(r.shape[1]-ndel): + q1, r1 = qr_delete(q, r, col, ndel, which='col', + overwrite_qr=False) + a1 = np.delete(a, slice(col, col+ndel), 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_1_row(self): + a, q, r = self.generate('Mx1') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_p_row(self): + a, q, r = self.generate('Mx1') + for ndel in range(2, 6): + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_1_col(self): + a, q, r = self.generate('1xN') + for col in range(r.shape[1]): + q1, r1 = qr_delete(q, r, col, which='col', overwrite_qr=False) + a1 = np.delete(a, col, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_p_col(self): + a, q, r = self.generate('1xN') + for ndel in range(2, 6): + for col in range(r.shape[1]-ndel): + q1, r1 = qr_delete(q, r, col, ndel, which='col', + overwrite_qr=False) + a1 = np.delete(a, slice(col, col+ndel), 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_economic_1_row(self): + a, q, r = self.generate('Mx1', 'economic') + for row in range(r.shape[0]): + q1, r1 = qr_delete(q, r, row, overwrite_qr=False) + a1 = np.delete(a, row, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_economic_p_row(self): + a, q, r = self.generate('Mx1', 'economic') + for ndel in range(2, 6): + for row in range(a.shape[0]-ndel): + q1, r1 = qr_delete(q, r, row, ndel, overwrite_qr=False) + a1 = np.delete(a, slice(row, row+ndel), 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_delete_last_1_row(self): + # full and eco are the same for 1xN + a, q, r = self.generate('1xN') + q1, r1 = qr_delete(q, r, 0, 1, 'row') + assert_equal(q1, np.ndarray(shape=(0, 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, r.shape[1]), dtype=r.dtype)) + + def test_delete_last_p_row(self): + a, q, r = self.generate('tall', 'full') + q1, r1 = qr_delete(q, r, 0, a.shape[0], 'row') + assert_equal(q1, np.ndarray(shape=(0, 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, r.shape[1]), dtype=r.dtype)) + + a, q, r = self.generate('tall', 'economic') + q1, r1 = qr_delete(q, r, 0, a.shape[0], 'row') + assert_equal(q1, np.ndarray(shape=(0, 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, r.shape[1]), dtype=r.dtype)) + + def test_delete_last_1_col(self): + a, q, r = self.generate('Mx1', 'economic') + q1, r1 = qr_delete(q, r, 0, 1, 'col') + assert_equal(q1, np.ndarray(shape=(q.shape[0], 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, 0), dtype=r.dtype)) + + a, q, r = self.generate('Mx1', 'full') + q1, r1 = qr_delete(q, r, 0, 1, 'col') + assert_unitary(q1) + assert_(q1.dtype == q.dtype) + assert_(q1.shape == q.shape) + assert_equal(r1, np.ndarray(shape=(r.shape[0], 0), dtype=r.dtype)) + + def test_delete_last_p_col(self): + a, q, r = self.generate('tall', 'full') + q1, r1 = qr_delete(q, r, 0, a.shape[1], 'col') + assert_unitary(q1) + assert_(q1.dtype == q.dtype) + assert_(q1.shape == q.shape) + assert_equal(r1, np.ndarray(shape=(r.shape[0], 0), dtype=r.dtype)) + + a, q, r = self.generate('tall', 'economic') + q1, r1 = qr_delete(q, r, 0, a.shape[1], 'col') + assert_equal(q1, np.ndarray(shape=(q.shape[0], 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, 0), dtype=r.dtype)) + + def test_delete_1x1_row_col(self): + a, q, r = self.generate('1x1') + q1, r1 = qr_delete(q, r, 0, 1, 'row') + assert_equal(q1, np.ndarray(shape=(0, 0), dtype=q.dtype)) + assert_equal(r1, np.ndarray(shape=(0, r.shape[1]), dtype=r.dtype)) + + a, q, r = self.generate('1x1') + q1, r1 = qr_delete(q, r, 0, 1, 'col') + assert_unitary(q1) + assert_(q1.dtype == q.dtype) + assert_(q1.shape == q.shape) + assert_equal(r1, np.ndarray(shape=(r.shape[0], 0), dtype=r.dtype)) + + # all full qr, row deletes and single column deletes should be able to + # handle any non negative strides. (only row and column vector + # operations are used.) p column delete require fortran ordered + # Q and R and will make a copy as necessary. Economic qr row deletes + # require a contiguous q. + + def base_non_simple_strides(self, adjust_strides, ks, p, which, + overwriteable): + if which == 'row': + qind = (slice(p,None), slice(p,None)) + rind = (slice(p,None), slice(None)) + else: + qind = (slice(None), slice(None)) + rind = (slice(None), slice(None,-p)) + + for type, k in itertools.product(['sqr', 'tall', 'fat'], ks): + a, q0, r0, = self.generate(type) + qs, rs = adjust_strides((q0, r0)) + if p == 1: + a1 = np.delete(a, k, 0 if which == 'row' else 1) + else: + s = slice(k,k+p) + if k < 0: + s = slice(k, k + p + + (a.shape[0] if which == 'row' else a.shape[1])) + a1 = np.delete(a, s, 0 if which == 'row' else 1) + + # for each variable, q, r we try with it strided and + # overwrite=False. Then we try with overwrite=True, and make + # sure that q and r are still overwritten. + + q = q0.copy('F') + r = r0.copy('F') + q1, r1 = qr_delete(qs, r, k, p, which, False) + check_qr(q1, r1, a1, self.rtol, self.atol) + q1o, r1o = qr_delete(qs, r, k, p, which, True) + check_qr(q1o, r1o, a1, self.rtol, self.atol) + if overwriteable: + assert_allclose(q1o, qs[qind], rtol=self.rtol, atol=self.atol) + assert_allclose(r1o, r[rind], rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + q2, r2 = qr_delete(q, rs, k, p, which, False) + check_qr(q2, r2, a1, self.rtol, self.atol) + q2o, r2o = qr_delete(q, rs, k, p, which, True) + check_qr(q2o, r2o, a1, self.rtol, self.atol) + if overwriteable: + assert_allclose(q2o, q[qind], rtol=self.rtol, atol=self.atol) + assert_allclose(r2o, rs[rind], rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + # since some of these were consumed above + qs, rs = adjust_strides((q, r)) + q3, r3 = qr_delete(qs, rs, k, p, which, False) + check_qr(q3, r3, a1, self.rtol, self.atol) + q3o, r3o = qr_delete(qs, rs, k, p, which, True) + check_qr(q3o, r3o, a1, self.rtol, self.atol) + if overwriteable: + assert_allclose(q2o, qs[qind], rtol=self.rtol, atol=self.atol) + assert_allclose(r3o, rs[rind], rtol=self.rtol, atol=self.atol) + + def test_non_unit_strides_1_row(self): + self.base_non_simple_strides(make_strided, [0], 1, 'row', True) + + def test_non_unit_strides_p_row(self): + self.base_non_simple_strides(make_strided, [0], 3, 'row', True) + + def test_non_unit_strides_1_col(self): + self.base_non_simple_strides(make_strided, [0], 1, 'col', True) + + def test_non_unit_strides_p_col(self): + self.base_non_simple_strides(make_strided, [0], 3, 'col', False) + + def test_neg_strides_1_row(self): + self.base_non_simple_strides(negate_strides, [0], 1, 'row', False) + + def test_neg_strides_p_row(self): + self.base_non_simple_strides(negate_strides, [0], 3, 'row', False) + + def test_neg_strides_1_col(self): + self.base_non_simple_strides(negate_strides, [0], 1, 'col', False) + + def test_neg_strides_p_col(self): + self.base_non_simple_strides(negate_strides, [0], 3, 'col', False) + + def test_non_itemize_strides_1_row(self): + self.base_non_simple_strides(nonitemsize_strides, [0], 1, 'row', False) + + def test_non_itemize_strides_p_row(self): + self.base_non_simple_strides(nonitemsize_strides, [0], 3, 'row', False) + + def test_non_itemize_strides_1_col(self): + self.base_non_simple_strides(nonitemsize_strides, [0], 1, 'col', False) + + def test_non_itemize_strides_p_col(self): + self.base_non_simple_strides(nonitemsize_strides, [0], 3, 'col', False) + + def test_non_native_byte_order_1_row(self): + self.base_non_simple_strides(make_nonnative, [0], 1, 'row', False) + + def test_non_native_byte_order_p_row(self): + self.base_non_simple_strides(make_nonnative, [0], 3, 'row', False) + + def test_non_native_byte_order_1_col(self): + self.base_non_simple_strides(make_nonnative, [0], 1, 'col', False) + + def test_non_native_byte_order_p_col(self): + self.base_non_simple_strides(make_nonnative, [0], 3, 'col', False) + + def test_neg_k(self): + a, q, r = self.generate('sqr') + for k, p, w in itertools.product([-3, -7], [1, 3], ['row', 'col']): + q1, r1 = qr_delete(q, r, k, p, w, overwrite_qr=False) + if w == 'row': + a1 = np.delete(a, slice(k+a.shape[0], k+p+a.shape[0]), 0) + else: + a1 = np.delete(a, slice(k+a.shape[0], k+p+a.shape[1]), 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def base_overwrite_qr(self, which, p, test_C, test_F, mode='full'): + assert_sqr = True if mode == 'full' else False + if which == 'row': + qind = (slice(p,None), slice(p,None)) + rind = (slice(p,None), slice(None)) + else: + qind = (slice(None), slice(None)) + rind = (slice(None), slice(None,-p)) + a, q0, r0 = self.generate('sqr', mode) + if p == 1: + a1 = np.delete(a, 3, 0 if which == 'row' else 1) + else: + a1 = np.delete(a, slice(3, 3+p), 0 if which == 'row' else 1) + + # don't overwrite + q = q0.copy('F') + r = r0.copy('F') + q1, r1 = qr_delete(q, r, 3, p, which, False) + check_qr(q1, r1, a1, self.rtol, self.atol, assert_sqr) + check_qr(q, r, a, self.rtol, self.atol, assert_sqr) + + if test_F: + q = q0.copy('F') + r = r0.copy('F') + q2, r2 = qr_delete(q, r, 3, p, which, True) + check_qr(q2, r2, a1, self.rtol, self.atol, assert_sqr) + # verify the overwriting + assert_allclose(q2, q[qind], rtol=self.rtol, atol=self.atol) + assert_allclose(r2, r[rind], rtol=self.rtol, atol=self.atol) + + if test_C: + q = q0.copy('C') + r = r0.copy('C') + q3, r3 = qr_delete(q, r, 3, p, which, True) + check_qr(q3, r3, a1, self.rtol, self.atol, assert_sqr) + assert_allclose(q3, q[qind], rtol=self.rtol, atol=self.atol) + assert_allclose(r3, r[rind], rtol=self.rtol, atol=self.atol) + + def test_overwrite_qr_1_row(self): + # any positively strided q and r. + self.base_overwrite_qr('row', 1, True, True) + + def test_overwrite_economic_qr_1_row(self): + # Any contiguous q and positively strided r. + self.base_overwrite_qr('row', 1, True, True, 'economic') + + def test_overwrite_qr_1_col(self): + # any positively strided q and r. + # full and eco share code paths + self.base_overwrite_qr('col', 1, True, True) + + def test_overwrite_qr_p_row(self): + # any positively strided q and r. + self.base_overwrite_qr('row', 3, True, True) + + def test_overwrite_economic_qr_p_row(self): + # any contiguous q and positively strided r + self.base_overwrite_qr('row', 3, True, True, 'economic') + + def test_overwrite_qr_p_col(self): + # only F ordered q and r can be overwritten for cols + # full and eco share code paths + self.base_overwrite_qr('col', 3, False, True) + + def test_bad_which(self): + a, q, r = self.generate('sqr') + assert_raises(ValueError, qr_delete, q, r, 0, which='foo') + + def test_bad_k(self): + a, q, r = self.generate('tall') + assert_raises(ValueError, qr_delete, q, r, q.shape[0], 1) + assert_raises(ValueError, qr_delete, q, r, -q.shape[0]-1, 1) + assert_raises(ValueError, qr_delete, q, r, r.shape[0], 1, 'col') + assert_raises(ValueError, qr_delete, q, r, -r.shape[0]-1, 1, 'col') + + def test_bad_p(self): + a, q, r = self.generate('tall') + # p must be positive + assert_raises(ValueError, qr_delete, q, r, 0, -1) + assert_raises(ValueError, qr_delete, q, r, 0, -1, 'col') + + # and nonzero + assert_raises(ValueError, qr_delete, q, r, 0, 0) + assert_raises(ValueError, qr_delete, q, r, 0, 0, 'col') + + # must have at least k+p rows or cols, depending. + assert_raises(ValueError, qr_delete, q, r, 3, q.shape[0]-2) + assert_raises(ValueError, qr_delete, q, r, 3, r.shape[1]-2, 'col') + + def test_empty_q(self): + a, q, r = self.generate('tall') + # same code path for 'row' and 'col' + assert_raises(ValueError, qr_delete, np.array([]), r, 0, 1) + + def test_empty_r(self): + a, q, r = self.generate('tall') + # same code path for 'row' and 'col' + assert_raises(ValueError, qr_delete, q, np.array([]), 0, 1) + + def test_mismatched_q_and_r(self): + a, q, r = self.generate('tall') + r = r[1:] + assert_raises(ValueError, qr_delete, q, r, 0, 1) + + def test_unsupported_dtypes(self): + dts = ['int8', 'int16', 'int32', 'int64', + 'uint8', 'uint16', 'uint32', 'uint64', + 'float16', 'longdouble', 'clongdouble', + 'bool'] + a, q0, r0 = self.generate('tall') + for dtype in dts: + q = q0.real.astype(dtype) + with np.errstate(invalid="ignore"): + r = r0.real.astype(dtype) + assert_raises(ValueError, qr_delete, q, r0, 0, 1, 'row') + assert_raises(ValueError, qr_delete, q, r0, 0, 2, 'row') + assert_raises(ValueError, qr_delete, q, r0, 0, 1, 'col') + assert_raises(ValueError, qr_delete, q, r0, 0, 2, 'col') + + assert_raises(ValueError, qr_delete, q0, r, 0, 1, 'row') + assert_raises(ValueError, qr_delete, q0, r, 0, 2, 'row') + assert_raises(ValueError, qr_delete, q0, r, 0, 1, 'col') + assert_raises(ValueError, qr_delete, q0, r, 0, 2, 'col') + + def test_check_finite(self): + a0, q0, r0 = self.generate('tall') + + q = q0.copy('F') + q[1,1] = np.nan + assert_raises(ValueError, qr_delete, q, r0, 0, 1, 'row') + assert_raises(ValueError, qr_delete, q, r0, 0, 3, 'row') + assert_raises(ValueError, qr_delete, q, r0, 0, 1, 'col') + assert_raises(ValueError, qr_delete, q, r0, 0, 3, 'col') + + r = r0.copy('F') + r[1,1] = np.nan + assert_raises(ValueError, qr_delete, q0, r, 0, 1, 'row') + assert_raises(ValueError, qr_delete, q0, r, 0, 3, 'row') + assert_raises(ValueError, qr_delete, q0, r, 0, 1, 'col') + assert_raises(ValueError, qr_delete, q0, r, 0, 3, 'col') + + def test_qr_scalar(self): + a, q, r = self.generate('1x1') + assert_raises(ValueError, qr_delete, q[0, 0], r, 0, 1, 'row') + assert_raises(ValueError, qr_delete, q, r[0, 0], 0, 1, 'row') + assert_raises(ValueError, qr_delete, q[0, 0], r, 0, 1, 'col') + assert_raises(ValueError, qr_delete, q, r[0, 0], 0, 1, 'col') + +class TestQRdelete_f(BaseQRdelete): + dtype = np.dtype('f') + +class TestQRdelete_F(BaseQRdelete): + dtype = np.dtype('F') + +class TestQRdelete_d(BaseQRdelete): + dtype = np.dtype('d') + +class TestQRdelete_D(BaseQRdelete): + dtype = np.dtype('D') + +class BaseQRinsert(BaseQRdeltas): + def generate(self, type, mode='full', which='row', p=1): + a, q, r = super().generate(type, mode) + + assert_(p > 0) + rng = np.random.default_rng(1234) + + if which == 'row': + if p == 1: + u = rng.random(a.shape[1]) + else: + u = rng.random((p, a.shape[1])) + elif which == 'col': + if p == 1: + u = rng.random(a.shape[0]) + else: + u = rng.random((a.shape[0], p)) + else: + raise ValueError('which should be either "row" or "col"') + + if np.iscomplexobj(self.dtype.type(1)): + b = rng.random(u.shape) + u = u + 1j * b + + u = u.astype(self.dtype) + return a, q, r, u + + def test_sqr_1_row(self): + a, q, r, u = self.generate('sqr', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_p_row(self): + # sqr + rows --> fat always + a, q, r, u = self.generate('sqr', which='row', p=3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_1_col(self): + a, q, r, u = self.generate('sqr', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_p_col(self): + # sqr + cols --> fat always + a, q, r, u = self.generate('sqr', which='col', p=3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_1_row(self): + a, q, r, u = self.generate('tall', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_p_row(self): + # tall + rows --> tall always + a, q, r, u = self.generate('tall', which='row', p=3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_1_col(self): + a, q, r, u = self.generate('tall', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + # for column adds to tall matrices there are three cases to test + # tall + pcol --> tall + # tall + pcol --> sqr + # tall + pcol --> fat + def base_tall_p_col_xxx(self, p): + a, q, r, u = self.generate('tall', which='col', p=p) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(p, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_p_col_tall(self): + # 12x7 + 12x3 = 12x10 --> stays tall + self.base_tall_p_col_xxx(3) + + def test_tall_p_col_sqr(self): + # 12x7 + 12x5 = 12x12 --> becomes sqr + self.base_tall_p_col_xxx(5) + + def test_tall_p_col_fat(self): + # 12x7 + 12x7 = 12x14 --> becomes fat + self.base_tall_p_col_xxx(7) + + def test_fat_1_row(self): + a, q, r, u = self.generate('fat', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + # for row adds to fat matrices there are three cases to test + # fat + prow --> fat + # fat + prow --> sqr + # fat + prow --> tall + def base_fat_p_row_xxx(self, p): + a, q, r, u = self.generate('fat', which='row', p=p) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(p, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_p_row_fat(self): + # 7x12 + 3x12 = 10x12 --> stays fat + self.base_fat_p_row_xxx(3) + + def test_fat_p_row_sqr(self): + # 7x12 + 5x12 = 12x12 --> becomes sqr + self.base_fat_p_row_xxx(5) + + def test_fat_p_row_tall(self): + # 7x12 + 7x12 = 14x12 --> becomes tall + self.base_fat_p_row_xxx(7) + + def test_fat_1_col(self): + a, q, r, u = self.generate('fat', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_p_col(self): + # fat + cols --> fat always + a, q, r, u = self.generate('fat', which='col', p=3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_economic_1_row(self): + a, q, r, u = self.generate('tall', 'economic', 'row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row, overwrite_qru=False) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_p_row(self): + # tall + rows --> tall always + a, q, r, u = self.generate('tall', 'economic', 'row', 3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row, overwrite_qru=False) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_1_col(self): + a, q, r, u = self.generate('tall', 'economic', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u.copy(), col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_1_col_bad_update(self): + # When the column to be added lies in the span of Q, the update is + # not meaningful. This is detected, and a LinAlgError is issued. + q = np.eye(5, 3, dtype=self.dtype) + r = np.eye(3, dtype=self.dtype) + u = np.array([1, 0, 0, 0, 0], self.dtype) + assert_raises(linalg.LinAlgError, qr_insert, q, r, u, 0, 'col') + + # for column adds to economic matrices there are three cases to test + # eco + pcol --> eco + # eco + pcol --> sqr + # eco + pcol --> fat + def base_economic_p_col_xxx(self, p): + a, q, r, u = self.generate('tall', 'economic', which='col', p=p) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(p, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_p_col_eco(self): + # 12x7 + 12x3 = 12x10 --> stays eco + self.base_economic_p_col_xxx(3) + + def test_economic_p_col_sqr(self): + # 12x7 + 12x5 = 12x12 --> becomes sqr + self.base_economic_p_col_xxx(5) + + def test_economic_p_col_fat(self): + # 12x7 + 12x7 = 12x14 --> becomes fat + self.base_economic_p_col_xxx(7) + + def test_Mx1_1_row(self): + a, q, r, u = self.generate('Mx1', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_p_row(self): + a, q, r, u = self.generate('Mx1', which='row', p=3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_1_col(self): + a, q, r, u = self.generate('Mx1', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_p_col(self): + a, q, r, u = self.generate('Mx1', which='col', p=3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_economic_1_row(self): + a, q, r, u = self.generate('Mx1', 'economic', 'row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_economic_p_row(self): + a, q, r, u = self.generate('Mx1', 'economic', 'row', 3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_economic_1_col(self): + a, q, r, u = self.generate('Mx1', 'economic', 'col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_economic_p_col(self): + a, q, r, u = self.generate('Mx1', 'economic', 'col', 3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_1xN_1_row(self): + a, q, r, u = self.generate('1xN', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_p_row(self): + a, q, r, u = self.generate('1xN', which='row', p=3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_1_col(self): + a, q, r, u = self.generate('1xN', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_p_col(self): + a, q, r, u = self.generate('1xN', which='col', p=3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_1_row(self): + a, q, r, u = self.generate('1x1', which='row') + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, row, u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_p_row(self): + a, q, r, u = self.generate('1x1', which='row', p=3) + for row in range(r.shape[0] + 1): + q1, r1 = qr_insert(q, r, u, row) + a1 = np.insert(a, np.full(3, row, np.intp), u, 0) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_1_col(self): + a, q, r, u = self.generate('1x1', which='col') + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, col, u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_p_col(self): + a, q, r, u = self.generate('1x1', which='col', p=3) + for col in range(r.shape[1] + 1): + q1, r1 = qr_insert(q, r, u, col, 'col', overwrite_qru=False) + a1 = np.insert(a, np.full(3, col, np.intp), u, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_1_scalar(self): + a, q, r, u = self.generate('1x1', which='row') + assert_raises(ValueError, qr_insert, q[0, 0], r, u, 0, 'row') + assert_raises(ValueError, qr_insert, q, r[0, 0], u, 0, 'row') + assert_raises(ValueError, qr_insert, q, r, u[0], 0, 'row') + + assert_raises(ValueError, qr_insert, q[0, 0], r, u, 0, 'col') + assert_raises(ValueError, qr_insert, q, r[0, 0], u, 0, 'col') + assert_raises(ValueError, qr_insert, q, r, u[0], 0, 'col') + + def base_non_simple_strides(self, adjust_strides, k, p, which): + for type in ['sqr', 'tall', 'fat']: + a, q0, r0, u0 = self.generate(type, which=which, p=p) + qs, rs, us = adjust_strides((q0, r0, u0)) + if p == 1: + ai = np.insert(a, k, u0, 0 if which == 'row' else 1) + else: + ai = np.insert(a, np.full(p, k, np.intp), + u0 if which == 'row' else u0, + 0 if which == 'row' else 1) + + # for each variable, q, r, u we try with it strided and + # overwrite=False. Then we try with overwrite=True. Nothing + # is checked to see if it can be overwritten, since only + # F ordered Q can be overwritten when adding columns. + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + q1, r1 = qr_insert(qs, r, u, k, which, overwrite_qru=False) + check_qr(q1, r1, ai, self.rtol, self.atol) + q1o, r1o = qr_insert(qs, r, u, k, which, overwrite_qru=True) + check_qr(q1o, r1o, ai, self.rtol, self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + q2, r2 = qr_insert(q, rs, u, k, which, overwrite_qru=False) + check_qr(q2, r2, ai, self.rtol, self.atol) + q2o, r2o = qr_insert(q, rs, u, k, which, overwrite_qru=True) + check_qr(q2o, r2o, ai, self.rtol, self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + q3, r3 = qr_insert(q, r, us, k, which, overwrite_qru=False) + check_qr(q3, r3, ai, self.rtol, self.atol) + q3o, r3o = qr_insert(q, r, us, k, which, overwrite_qru=True) + check_qr(q3o, r3o, ai, self.rtol, self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + # since some of these were consumed above + qs, rs, us = adjust_strides((q, r, u)) + q5, r5 = qr_insert(qs, rs, us, k, which, overwrite_qru=False) + check_qr(q5, r5, ai, self.rtol, self.atol) + q5o, r5o = qr_insert(qs, rs, us, k, which, overwrite_qru=True) + check_qr(q5o, r5o, ai, self.rtol, self.atol) + + def test_non_unit_strides_1_row(self): + self.base_non_simple_strides(make_strided, 0, 1, 'row') + + def test_non_unit_strides_p_row(self): + self.base_non_simple_strides(make_strided, 0, 3, 'row') + + def test_non_unit_strides_1_col(self): + self.base_non_simple_strides(make_strided, 0, 1, 'col') + + def test_non_unit_strides_p_col(self): + self.base_non_simple_strides(make_strided, 0, 3, 'col') + + def test_neg_strides_1_row(self): + self.base_non_simple_strides(negate_strides, 0, 1, 'row') + + def test_neg_strides_p_row(self): + self.base_non_simple_strides(negate_strides, 0, 3, 'row') + + def test_neg_strides_1_col(self): + self.base_non_simple_strides(negate_strides, 0, 1, 'col') + + def test_neg_strides_p_col(self): + self.base_non_simple_strides(negate_strides, 0, 3, 'col') + + def test_non_itemsize_strides_1_row(self): + self.base_non_simple_strides(nonitemsize_strides, 0, 1, 'row') + + def test_non_itemsize_strides_p_row(self): + self.base_non_simple_strides(nonitemsize_strides, 0, 3, 'row') + + def test_non_itemsize_strides_1_col(self): + self.base_non_simple_strides(nonitemsize_strides, 0, 1, 'col') + + def test_non_itemsize_strides_p_col(self): + self.base_non_simple_strides(nonitemsize_strides, 0, 3, 'col') + + def test_non_native_byte_order_1_row(self): + self.base_non_simple_strides(make_nonnative, 0, 1, 'row') + + def test_non_native_byte_order_p_row(self): + self.base_non_simple_strides(make_nonnative, 0, 3, 'row') + + def test_non_native_byte_order_1_col(self): + self.base_non_simple_strides(make_nonnative, 0, 1, 'col') + + def test_non_native_byte_order_p_col(self): + self.base_non_simple_strides(make_nonnative, 0, 3, 'col') + + def test_overwrite_qu_rank_1(self): + # when inserting rows, the size of both Q and R change, so only + # column inserts can overwrite q. Only complex column inserts + # with C ordered Q overwrite u. Any contiguous Q is overwritten + # when inserting 1 column + a, q0, r, u, = self.generate('sqr', which='col', p=1) + q = q0.copy('C') + u0 = u.copy() + # don't overwrite + q1, r1 = qr_insert(q, r, u, 0, 'col', overwrite_qru=False) + a1 = np.insert(a, 0, u0, 1) + check_qr(q1, r1, a1, self.rtol, self.atol) + check_qr(q, r, a, self.rtol, self.atol) + + # try overwriting + q2, r2 = qr_insert(q, r, u, 0, 'col', overwrite_qru=True) + check_qr(q2, r2, a1, self.rtol, self.atol) + # verify the overwriting + assert_allclose(q2, q, rtol=self.rtol, atol=self.atol) + assert_allclose(u, u0.conj(), self.rtol, self.atol) + + # now try with a fortran ordered Q + qF = q0.copy('F') + u1 = u0.copy() + q3, r3 = qr_insert(qF, r, u1, 0, 'col', overwrite_qru=False) + check_qr(q3, r3, a1, self.rtol, self.atol) + check_qr(qF, r, a, self.rtol, self.atol) + + # try overwriting + q4, r4 = qr_insert(qF, r, u1, 0, 'col', overwrite_qru=True) + check_qr(q4, r4, a1, self.rtol, self.atol) + assert_allclose(q4, qF, rtol=self.rtol, atol=self.atol) + + def test_overwrite_qu_rank_p(self): + # when inserting rows, the size of both Q and R change, so only + # column inserts can potentially overwrite Q. In practice, only + # F ordered Q are overwritten with a rank p update. + a, q0, r, u, = self.generate('sqr', which='col', p=3) + q = q0.copy('F') + a1 = np.insert(a, np.zeros(3, np.intp), u, 1) + + # don't overwrite + q1, r1 = qr_insert(q, r, u, 0, 'col', overwrite_qru=False) + check_qr(q1, r1, a1, self.rtol, self.atol) + check_qr(q, r, a, self.rtol, self.atol) + + # try overwriting + q2, r2 = qr_insert(q, r, u, 0, 'col', overwrite_qru=True) + check_qr(q2, r2, a1, self.rtol, self.atol) + assert_allclose(q2, q, rtol=self.rtol, atol=self.atol) + + def test_empty_inputs(self): + a, q, r, u = self.generate('sqr', which='row') + assert_raises(ValueError, qr_insert, np.array([]), r, u, 0, 'row') + assert_raises(ValueError, qr_insert, q, np.array([]), u, 0, 'row') + assert_raises(ValueError, qr_insert, q, r, np.array([]), 0, 'row') + assert_raises(ValueError, qr_insert, np.array([]), r, u, 0, 'col') + assert_raises(ValueError, qr_insert, q, np.array([]), u, 0, 'col') + assert_raises(ValueError, qr_insert, q, r, np.array([]), 0, 'col') + + def test_mismatched_shapes(self): + a, q, r, u = self.generate('tall', which='row') + assert_raises(ValueError, qr_insert, q, r[1:], u, 0, 'row') + assert_raises(ValueError, qr_insert, q[:-2], r, u, 0, 'row') + assert_raises(ValueError, qr_insert, q, r, u[1:], 0, 'row') + assert_raises(ValueError, qr_insert, q, r[1:], u, 0, 'col') + assert_raises(ValueError, qr_insert, q[:-2], r, u, 0, 'col') + assert_raises(ValueError, qr_insert, q, r, u[1:], 0, 'col') + + def test_unsupported_dtypes(self): + dts = ['int8', 'int16', 'int32', 'int64', + 'uint8', 'uint16', 'uint32', 'uint64', + 'float16', 'longdouble', 'clongdouble', + 'bool'] + a, q0, r0, u0 = self.generate('sqr', which='row') + for dtype in dts: + q = q0.real.astype(dtype) + with np.errstate(invalid="ignore"): + r = r0.real.astype(dtype) + u = u0.real.astype(dtype) + assert_raises(ValueError, qr_insert, q, r0, u0, 0, 'row') + assert_raises(ValueError, qr_insert, q, r0, u0, 0, 'col') + assert_raises(ValueError, qr_insert, q0, r, u0, 0, 'row') + assert_raises(ValueError, qr_insert, q0, r, u0, 0, 'col') + assert_raises(ValueError, qr_insert, q0, r0, u, 0, 'row') + assert_raises(ValueError, qr_insert, q0, r0, u, 0, 'col') + + def test_check_finite(self): + a0, q0, r0, u0 = self.generate('sqr', which='row', p=3) + + q = q0.copy('F') + q[1,1] = np.nan + assert_raises(ValueError, qr_insert, q, r0, u0[:,0], 0, 'row') + assert_raises(ValueError, qr_insert, q, r0, u0, 0, 'row') + assert_raises(ValueError, qr_insert, q, r0, u0[:,0], 0, 'col') + assert_raises(ValueError, qr_insert, q, r0, u0, 0, 'col') + + r = r0.copy('F') + r[1,1] = np.nan + assert_raises(ValueError, qr_insert, q0, r, u0[:,0], 0, 'row') + assert_raises(ValueError, qr_insert, q0, r, u0, 0, 'row') + assert_raises(ValueError, qr_insert, q0, r, u0[:,0], 0, 'col') + assert_raises(ValueError, qr_insert, q0, r, u0, 0, 'col') + + u = u0.copy('F') + u[0,0] = np.nan + assert_raises(ValueError, qr_insert, q0, r0, u[:,0], 0, 'row') + assert_raises(ValueError, qr_insert, q0, r0, u, 0, 'row') + assert_raises(ValueError, qr_insert, q0, r0, u[:,0], 0, 'col') + assert_raises(ValueError, qr_insert, q0, r0, u, 0, 'col') + +class TestQRinsert_f(BaseQRinsert): + dtype = np.dtype('f') + +class TestQRinsert_F(BaseQRinsert): + dtype = np.dtype('F') + +class TestQRinsert_d(BaseQRinsert): + dtype = np.dtype('d') + +class TestQRinsert_D(BaseQRinsert): + dtype = np.dtype('D') + +class BaseQRupdate(BaseQRdeltas): + def generate(self, type, mode='full', p=1): + a, q, r = super().generate(type, mode) + + rng = np.random.default_rng(1234) + if p == 1: + u = rng.random(q.shape[0]) + v = rng.random(r.shape[1]) + else: + u = rng.random((q.shape[0], p)) + v = rng.random((r.shape[1], p)) + + if np.iscomplexobj(self.dtype.type(1)): + b = rng.random(u.shape) + u = u + 1j * b + + c = rng.random(v.shape) + v = v + 1j * c + + u = u.astype(self.dtype) + v = v.astype(self.dtype) + return a, q, r, u, v + + def test_sqr_rank_1(self): + a, q, r, u, v = self.generate('sqr') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_sqr_rank_p(self): + # test ndim = 2, rank 1 updates here too + for p in [1, 2, 3, 5]: + a, q, r, u, v = self.generate('sqr', p=p) + if p == 1: + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_rank_1(self): + a, q, r, u, v = self.generate('tall') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_tall_rank_p(self): + for p in [1, 2, 3, 5]: + a, q, r, u, v = self.generate('tall', p=p) + if p == 1: + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_rank_1(self): + a, q, r, u, v = self.generate('fat') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_fat_rank_p(self): + for p in [1, 2, 3, 5]: + a, q, r, u, v = self.generate('fat', p=p) + if p == 1: + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_economic_rank_1(self): + a, q, r, u, v = self.generate('tall', 'economic') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_economic_rank_p(self): + for p in [1, 2, 3, 5]: + a, q, r, u, v = self.generate('tall', 'economic', p) + if p == 1: + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_rank_1(self): + a, q, r, u, v = self.generate('Mx1') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_rank_p(self): + # when M or N == 1, only a rank 1 update is allowed. This isn't + # fundamental limitation, but the code does not support it. + a, q, r, u, v = self.generate('Mx1', p=1) + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_Mx1_economic_rank_1(self): + a, q, r, u, v = self.generate('Mx1', 'economic') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_Mx1_economic_rank_p(self): + # when M or N == 1, only a rank 1 update is allowed. This isn't + # fundamental limitation, but the code does not support it. + a, q, r, u, v = self.generate('Mx1', 'economic', p=1) + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + + def test_1xN_rank_1(self): + a, q, r, u, v = self.generate('1xN') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1xN_rank_p(self): + # when M or N == 1, only a rank 1 update is allowed. This isn't + # fundamental limitation, but the code does not support it. + a, q, r, u, v = self.generate('1xN', p=1) + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_rank_1(self): + a, q, r, u, v = self.generate('1x1') + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_rank_p(self): + # when M or N == 1, only a rank 1 update is allowed. This isn't + # fundamental limitation, but the code does not support it. + a, q, r, u, v = self.generate('1x1', p=1) + u = u.reshape(u.size, 1) + v = v.reshape(v.size, 1) + q1, r1 = qr_update(q, r, u, v, False) + a1 = a + np.dot(u, v.T.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol) + + def test_1x1_rank_1_scalar(self): + a, q, r, u, v = self.generate('1x1') + assert_raises(ValueError, qr_update, q[0, 0], r, u, v) + assert_raises(ValueError, qr_update, q, r[0, 0], u, v) + assert_raises(ValueError, qr_update, q, r, u[0], v) + assert_raises(ValueError, qr_update, q, r, u, v[0]) + + def base_non_simple_strides(self, adjust_strides, mode, p, overwriteable): + assert_sqr = False if mode == 'economic' else True + for type in ['sqr', 'tall', 'fat']: + a, q0, r0, u0, v0 = self.generate(type, mode, p) + qs, rs, us, vs = adjust_strides((q0, r0, u0, v0)) + if p == 1: + aup = a + np.outer(u0, v0.conj()) + else: + aup = a + np.dot(u0, v0.T.conj()) + + # for each variable, q, r, u, v we try with it strided and + # overwrite=False. Then we try with overwrite=True, and make + # sure that if p == 1, r and v are still overwritten. + # a strided q and u must always be copied. + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + q1, r1 = qr_update(qs, r, u, v, False) + check_qr(q1, r1, aup, self.rtol, self.atol, assert_sqr) + q1o, r1o = qr_update(qs, r, u, v, True) + check_qr(q1o, r1o, aup, self.rtol, self.atol, assert_sqr) + if overwriteable: + assert_allclose(r1o, r, rtol=self.rtol, atol=self.atol) + assert_allclose(v, v0.conj(), rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + q2, r2 = qr_update(q, rs, u, v, False) + check_qr(q2, r2, aup, self.rtol, self.atol, assert_sqr) + q2o, r2o = qr_update(q, rs, u, v, True) + check_qr(q2o, r2o, aup, self.rtol, self.atol, assert_sqr) + if overwriteable: + assert_allclose(r2o, rs, rtol=self.rtol, atol=self.atol) + assert_allclose(v, v0.conj(), rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + q3, r3 = qr_update(q, r, us, v, False) + check_qr(q3, r3, aup, self.rtol, self.atol, assert_sqr) + q3o, r3o = qr_update(q, r, us, v, True) + check_qr(q3o, r3o, aup, self.rtol, self.atol, assert_sqr) + if overwriteable: + assert_allclose(r3o, r, rtol=self.rtol, atol=self.atol) + assert_allclose(v, v0.conj(), rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + q4, r4 = qr_update(q, r, u, vs, False) + check_qr(q4, r4, aup, self.rtol, self.atol, assert_sqr) + q4o, r4o = qr_update(q, r, u, vs, True) + check_qr(q4o, r4o, aup, self.rtol, self.atol, assert_sqr) + if overwriteable: + assert_allclose(r4o, r, rtol=self.rtol, atol=self.atol) + assert_allclose(vs, v0.conj(), rtol=self.rtol, atol=self.atol) + + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + # since some of these were consumed above + qs, rs, us, vs = adjust_strides((q, r, u, v)) + q5, r5 = qr_update(qs, rs, us, vs, False) + check_qr(q5, r5, aup, self.rtol, self.atol, assert_sqr) + q5o, r5o = qr_update(qs, rs, us, vs, True) + check_qr(q5o, r5o, aup, self.rtol, self.atol, assert_sqr) + if overwriteable: + assert_allclose(r5o, rs, rtol=self.rtol, atol=self.atol) + assert_allclose(vs, v0.conj(), rtol=self.rtol, atol=self.atol) + + def test_non_unit_strides_rank_1(self): + self.base_non_simple_strides(make_strided, 'full', 1, True) + + def test_non_unit_strides_economic_rank_1(self): + self.base_non_simple_strides(make_strided, 'economic', 1, True) + + def test_non_unit_strides_rank_p(self): + self.base_non_simple_strides(make_strided, 'full', 3, False) + + def test_non_unit_strides_economic_rank_p(self): + self.base_non_simple_strides(make_strided, 'economic', 3, False) + + def test_neg_strides_rank_1(self): + self.base_non_simple_strides(negate_strides, 'full', 1, False) + + def test_neg_strides_economic_rank_1(self): + self.base_non_simple_strides(negate_strides, 'economic', 1, False) + + def test_neg_strides_rank_p(self): + self.base_non_simple_strides(negate_strides, 'full', 3, False) + + def test_neg_strides_economic_rank_p(self): + self.base_non_simple_strides(negate_strides, 'economic', 3, False) + + def test_non_itemsize_strides_rank_1(self): + self.base_non_simple_strides(nonitemsize_strides, 'full', 1, False) + + def test_non_itemsize_strides_economic_rank_1(self): + self.base_non_simple_strides(nonitemsize_strides, 'economic', 1, False) + + def test_non_itemsize_strides_rank_p(self): + self.base_non_simple_strides(nonitemsize_strides, 'full', 3, False) + + def test_non_itemsize_strides_economic_rank_p(self): + self.base_non_simple_strides(nonitemsize_strides, 'economic', 3, False) + + def test_non_native_byte_order_rank_1(self): + self.base_non_simple_strides(make_nonnative, 'full', 1, False) + + def test_non_native_byte_order_economic_rank_1(self): + self.base_non_simple_strides(make_nonnative, 'economic', 1, False) + + def test_non_native_byte_order_rank_p(self): + self.base_non_simple_strides(make_nonnative, 'full', 3, False) + + def test_non_native_byte_order_economic_rank_p(self): + self.base_non_simple_strides(make_nonnative, 'economic', 3, False) + + def test_overwrite_qruv_rank_1(self): + # Any positive strided q, r, u, and v can be overwritten for a rank 1 + # update, only checking C and F contiguous. + a, q0, r0, u0, v0 = self.generate('sqr') + a1 = a + np.outer(u0, v0.conj()) + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('F') + + # don't overwrite + q1, r1 = qr_update(q, r, u, v, False) + check_qr(q1, r1, a1, self.rtol, self.atol) + check_qr(q, r, a, self.rtol, self.atol) + + q2, r2 = qr_update(q, r, u, v, True) + check_qr(q2, r2, a1, self.rtol, self.atol) + # verify the overwriting, no good way to check u and v. + assert_allclose(q2, q, rtol=self.rtol, atol=self.atol) + assert_allclose(r2, r, rtol=self.rtol, atol=self.atol) + + q = q0.copy('C') + r = r0.copy('C') + u = u0.copy('C') + v = v0.copy('C') + q3, r3 = qr_update(q, r, u, v, True) + check_qr(q3, r3, a1, self.rtol, self.atol) + assert_allclose(q3, q, rtol=self.rtol, atol=self.atol) + assert_allclose(r3, r, rtol=self.rtol, atol=self.atol) + + def test_overwrite_qruv_rank_1_economic(self): + # updating economic decompositions can overwrite any contiguous r, + # and positively strided r and u. V is only ever read. + # only checking C and F contiguous. + a, q0, r0, u0, v0 = self.generate('tall', 'economic') + a1 = a + np.outer(u0, v0.conj()) + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('F') + + # don't overwrite + q1, r1 = qr_update(q, r, u, v, False) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + check_qr(q, r, a, self.rtol, self.atol, False) + + q2, r2 = qr_update(q, r, u, v, True) + check_qr(q2, r2, a1, self.rtol, self.atol, False) + # verify the overwriting, no good way to check u and v. + assert_allclose(q2, q, rtol=self.rtol, atol=self.atol) + assert_allclose(r2, r, rtol=self.rtol, atol=self.atol) + + q = q0.copy('C') + r = r0.copy('C') + u = u0.copy('C') + v = v0.copy('C') + q3, r3 = qr_update(q, r, u, v, True) + check_qr(q3, r3, a1, self.rtol, self.atol, False) + assert_allclose(q3, q, rtol=self.rtol, atol=self.atol) + assert_allclose(r3, r, rtol=self.rtol, atol=self.atol) + + def test_overwrite_qruv_rank_p(self): + # for rank p updates, q r must be F contiguous, v must be C (v.T --> F) + # and u can be C or F, but is only overwritten if Q is C and complex + a, q0, r0, u0, v0 = self.generate('sqr', p=3) + a1 = a + np.dot(u0, v0.T.conj()) + q = q0.copy('F') + r = r0.copy('F') + u = u0.copy('F') + v = v0.copy('C') + + # don't overwrite + q1, r1 = qr_update(q, r, u, v, False) + check_qr(q1, r1, a1, self.rtol, self.atol) + check_qr(q, r, a, self.rtol, self.atol) + + q2, r2 = qr_update(q, r, u, v, True) + check_qr(q2, r2, a1, self.rtol, self.atol) + # verify the overwriting, no good way to check u and v. + assert_allclose(q2, q, rtol=self.rtol, atol=self.atol) + assert_allclose(r2, r, rtol=self.rtol, atol=self.atol) + + def test_empty_inputs(self): + a, q, r, u, v = self.generate('tall') + assert_raises(ValueError, qr_update, np.array([]), r, u, v) + assert_raises(ValueError, qr_update, q, np.array([]), u, v) + assert_raises(ValueError, qr_update, q, r, np.array([]), v) + assert_raises(ValueError, qr_update, q, r, u, np.array([])) + + def test_mismatched_shapes(self): + a, q, r, u, v = self.generate('tall') + assert_raises(ValueError, qr_update, q, r[1:], u, v) + assert_raises(ValueError, qr_update, q[:-2], r, u, v) + assert_raises(ValueError, qr_update, q, r, u[1:], v) + assert_raises(ValueError, qr_update, q, r, u, v[1:]) + + def test_unsupported_dtypes(self): + dts = ['int8', 'int16', 'int32', 'int64', + 'uint8', 'uint16', 'uint32', 'uint64', + 'float16', 'longdouble', 'clongdouble', + 'bool'] + a, q0, r0, u0, v0 = self.generate('tall') + for dtype in dts: + q = q0.real.astype(dtype) + with np.errstate(invalid="ignore"): + r = r0.real.astype(dtype) + u = u0.real.astype(dtype) + v = v0.real.astype(dtype) + assert_raises(ValueError, qr_update, q, r0, u0, v0) + assert_raises(ValueError, qr_update, q0, r, u0, v0) + assert_raises(ValueError, qr_update, q0, r0, u, v0) + assert_raises(ValueError, qr_update, q0, r0, u0, v) + + def test_integer_input(self): + q = np.arange(16).reshape(4, 4) + r = q.copy() # doesn't matter + u = q[:, 0].copy() + v = r[0, :].copy() + assert_raises(ValueError, qr_update, q, r, u, v) + + def test_check_finite(self): + a0, q0, r0, u0, v0 = self.generate('tall', p=3) + + q = q0.copy('F') + q[1,1] = np.nan + assert_raises(ValueError, qr_update, q, r0, u0[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q, r0, u0, v0) + + r = r0.copy('F') + r[1,1] = np.nan + assert_raises(ValueError, qr_update, q0, r, u0[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q0, r, u0, v0) + + u = u0.copy('F') + u[0,0] = np.nan + assert_raises(ValueError, qr_update, q0, r0, u[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q0, r0, u, v0) + + v = v0.copy('F') + v[0,0] = np.nan + assert_raises(ValueError, qr_update, q0, r0, u[:,0], v[:,0]) + assert_raises(ValueError, qr_update, q0, r0, u, v) + + def test_economic_check_finite(self): + a0, q0, r0, u0, v0 = self.generate('tall', mode='economic', p=3) + + q = q0.copy('F') + q[1,1] = np.nan + assert_raises(ValueError, qr_update, q, r0, u0[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q, r0, u0, v0) + + r = r0.copy('F') + r[1,1] = np.nan + assert_raises(ValueError, qr_update, q0, r, u0[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q0, r, u0, v0) + + u = u0.copy('F') + u[0,0] = np.nan + assert_raises(ValueError, qr_update, q0, r0, u[:,0], v0[:,0]) + assert_raises(ValueError, qr_update, q0, r0, u, v0) + + v = v0.copy('F') + v[0,0] = np.nan + assert_raises(ValueError, qr_update, q0, r0, u[:,0], v[:,0]) + assert_raises(ValueError, qr_update, q0, r0, u, v) + + def test_u_exactly_in_span_q(self): + q = np.array([[0, 0], [0, 0], [1, 0], [0, 1]], self.dtype) + r = np.array([[1, 0], [0, 1]], self.dtype) + u = np.array([0, 0, 0, -1], self.dtype) + v = np.array([1, 2], self.dtype) + q1, r1 = qr_update(q, r, u, v) + a1 = np.dot(q, r) + np.outer(u, v.conj()) + check_qr(q1, r1, a1, self.rtol, self.atol, False) + +class TestQRupdate_f(BaseQRupdate): + dtype = np.dtype('f') + +class TestQRupdate_F(BaseQRupdate): + dtype = np.dtype('F') + +class TestQRupdate_d(BaseQRupdate): + dtype = np.dtype('d') + +class TestQRupdate_D(BaseQRupdate): + dtype = np.dtype('D') + +def test_form_qTu(): + # We want to ensure that all of the code paths through this function are + # tested. Most of them should be hit with the rest of test suite, but + # explicit tests make clear precisely what is being tested. + # + # This function expects that Q is either C or F contiguous and square. + # Economic mode decompositions (Q is (M, N), M != N) do not go through this + # function. U may have any positive strides. + # + # Some of these test are duplicates, since contiguous 1d arrays are both C + # and F. + + q_order = ['F', 'C'] + q_shape = [(8, 8), ] + u_order = ['F', 'C', 'A'] # here A means is not F not C + u_shape = [1, 3] + dtype = ['f', 'd', 'F', 'D'] + + for qo, qs, uo, us, d in \ + itertools.product(q_order, q_shape, u_order, u_shape, dtype): + if us == 1: + check_form_qTu(qo, qs, uo, us, 1, d) + check_form_qTu(qo, qs, uo, us, 2, d) + else: + check_form_qTu(qo, qs, uo, us, 2, d) + +def check_form_qTu(q_order, q_shape, u_order, u_shape, u_ndim, dtype): + rng = np.random.default_rng(47) + if u_shape == 1 and u_ndim == 1: + u_shape = (q_shape[0],) + else: + u_shape = (q_shape[0], u_shape) + dtype = np.dtype(dtype) + + if dtype.char in 'fd': + q = rng.random(q_shape) + u = rng.random(u_shape) + elif dtype.char in 'FD': + q = rng.random(q_shape) + 1j*rng.random(q_shape) + u = rng.random(u_shape) + 1j*rng.random(u_shape) + else: + raise ValueError("form_qTu doesn't support this dtype") + + q = np.require(q, dtype, q_order) + if u_order != 'A': + u = np.require(u, dtype, u_order) + else: + u, = make_strided((u.astype(dtype),)) + + rtol = 10.0 ** -(np.finfo(dtype).precision-2) + atol = 2*np.finfo(dtype).eps + + expected = np.dot(q.T.conj(), u) + res = _decomp_update._form_qTu(q, u) + assert_allclose(res, expected, rtol=rtol, atol=atol) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_extending.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_extending.py new file mode 100644 index 0000000000000000000000000000000000000000..8c9b24bc7351ed20bcaff809bd5fad22ec3ca323 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_extending.py @@ -0,0 +1,47 @@ +import os +import platform +import sysconfig + +import numpy as np +import pytest + +from scipy._lib._testutils import IS_EDITABLE, _test_cython_extension, cython +from scipy.linalg.blas import cdotu # type: ignore[attr-defined] +from scipy.linalg.lapack import dgtsv # type: ignore[attr-defined] + + +@pytest.mark.parallel_threads_limit(4) # 0.35 GiB per thread RAM usage +@pytest.mark.fail_slow(120) +# essential per https://github.com/scipy/scipy/pull/20487#discussion_r1567057247 +@pytest.mark.skipif(IS_EDITABLE, + reason='Editable install cannot find .pxd headers.') +@pytest.mark.skipif((platform.system() == 'Windows' and + sysconfig.get_config_var('Py_GIL_DISABLED')), + reason='gh-22039') +@pytest.mark.skipif(platform.machine() in ["wasm32", "wasm64"], + reason="Can't start subprocess") +@pytest.mark.skipif(cython is None, reason="requires cython") +def test_cython(tmp_path): + srcdir = os.path.dirname(os.path.dirname(__file__)) + extensions, extensions_cpp = _test_cython_extension(tmp_path, srcdir) + # actually test the cython c-extensions + a = np.ones(8) * 3 + b = np.ones(9) + c = np.ones(8) * 4 + x = np.ones(9) + _, _, _, x, _ = dgtsv(a, b, c, x) + a = np.ones(8) * 3 + b = np.ones(9) + c = np.ones(8) * 4 + x_c = np.ones(9) + extensions.tridiag(a, b, c, x_c) + a = np.ones(8) * 3 + b = np.ones(9) + c = np.ones(8) * 4 + x_cpp = np.ones(9) + extensions_cpp.tridiag(a, b, c, x_cpp) + np.testing.assert_array_equal(x, x_cpp) + cx = np.array([1-1j, 2+2j, 3-3j], dtype=np.complex64) + cy = np.array([4+4j, 5-5j, 6+6j], dtype=np.complex64) + np.testing.assert_array_equal(cdotu(cx, cy), extensions.complex_dot(cx, cy)) + np.testing.assert_array_equal(cdotu(cx, cy), extensions_cpp.complex_dot(cx, cy)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_fblas.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_fblas.py new file mode 100644 index 0000000000000000000000000000000000000000..9f38ef5403ee181f8522752a0f1a1a6df676c16a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_fblas.py @@ -0,0 +1,602 @@ +# Test interfaces to fortran blas. +# +# The tests are more of interface than they are of the underlying blas. +# Only very small matrices checked -- N=3 or so. +# +# !! Complex calculations really aren't checked that carefully. +# !! Only real valued complex numbers are used in tests. +from itertools import product +import sys + +import numpy as np +from numpy import float32, float64, complex64, complex128, arange, array, \ + zeros, shape, transpose, newaxis, common_type, conjugate + +from scipy.linalg import _fblas as fblas + +from numpy.testing import assert_array_equal, \ + assert_allclose, assert_array_almost_equal, assert_ + +import pytest + +# decimal accuracy to require between Python and LAPACK/BLAS calculations +accuracy = 5 + +# Since numpy.dot likely uses the same blas, use this routine +# to check. + + +def matrixmultiply(a, b): + if len(b.shape) == 1: + b_is_vector = True + b = b[:, newaxis] + else: + b_is_vector = False + assert_(a.shape[1] == b.shape[0]) + c = zeros((a.shape[0], b.shape[1]), common_type(a, b)) + for i in range(a.shape[0]): + for j in range(b.shape[1]): + s = 0 + for k in range(a.shape[1]): + s += a[i, k] * b[k, j] + c[i, j] = s + if b_is_vector: + c = c.reshape((a.shape[0],)) + return c + +################################################## +# Test blas ?axpy + + +class BaseAxpy: + ''' Mixin class for axpy tests ''' + + def test_default_a(self): + x = arange(3., dtype=self.dtype) + y = arange(3., dtype=x.dtype) + real_y = x*1.+y + y = self.blas_func(x, y) + assert_array_equal(real_y, y) + + def test_simple(self): + x = arange(3., dtype=self.dtype) + y = arange(3., dtype=x.dtype) + real_y = x*3.+y + y = self.blas_func(x, y, a=3.) + assert_array_equal(real_y, y) + + def test_x_stride(self): + x = arange(6., dtype=self.dtype) + y = zeros(3, x.dtype) + y = arange(3., dtype=x.dtype) + real_y = x[::2]*3.+y + y = self.blas_func(x, y, a=3., n=3, incx=2) + assert_array_equal(real_y, y) + + def test_y_stride(self): + x = arange(3., dtype=self.dtype) + y = zeros(6, x.dtype) + real_y = x*3.+y[::2] + y = self.blas_func(x, y, a=3., n=3, incy=2) + assert_array_equal(real_y, y[::2]) + + def test_x_and_y_stride(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + real_y = x[::4]*3.+y[::2] + y = self.blas_func(x, y, a=3., n=3, incx=4, incy=2) + assert_array_equal(real_y, y[::2]) + + def test_x_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=4, incx=5) + + def test_y_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=3, incy=5) + + +try: + class TestSaxpy(BaseAxpy): + blas_func = fblas.saxpy + dtype = float32 +except AttributeError: + class TestSaxpy: + pass + + +class TestDaxpy(BaseAxpy): + blas_func = fblas.daxpy + dtype = float64 + + +try: + class TestCaxpy(BaseAxpy): + blas_func = fblas.caxpy + dtype = complex64 +except AttributeError: + class TestCaxpy: + pass + + +class TestZaxpy(BaseAxpy): + blas_func = fblas.zaxpy + dtype = complex128 + + +################################################## +# Test blas ?scal + +class BaseScal: + ''' Mixin class for scal testing ''' + + def test_simple(self): + x = arange(3., dtype=self.dtype) + real_x = x*3. + x = self.blas_func(3., x) + assert_array_equal(real_x, x) + + def test_x_stride(self): + x = arange(6., dtype=self.dtype) + real_x = x.copy() + real_x[::2] = x[::2]*array(3., self.dtype) + x = self.blas_func(3., x, n=3, incx=2) + assert_array_equal(real_x, x) + + def test_x_bad_size(self): + x = arange(12., dtype=self.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(2., x, n=4, incx=5) + + +try: + class TestSscal(BaseScal): + blas_func = fblas.sscal + dtype = float32 +except AttributeError: + class TestSscal: + pass + + +class TestDscal(BaseScal): + blas_func = fblas.dscal + dtype = float64 + + +try: + class TestCscal(BaseScal): + blas_func = fblas.cscal + dtype = complex64 +except AttributeError: + class TestCscal: + pass + + +class TestZscal(BaseScal): + blas_func = fblas.zscal + dtype = complex128 + + +################################################## +# Test blas ?copy + +class BaseCopy: + ''' Mixin class for copy testing ''' + + def test_simple(self): + x = arange(3., dtype=self.dtype) + y = zeros(shape(x), x.dtype) + y = self.blas_func(x, y) + assert_array_equal(x, y) + + def test_x_stride(self): + x = arange(6., dtype=self.dtype) + y = zeros(3, x.dtype) + y = self.blas_func(x, y, n=3, incx=2) + assert_array_equal(x[::2], y) + + def test_y_stride(self): + x = arange(3., dtype=self.dtype) + y = zeros(6, x.dtype) + y = self.blas_func(x, y, n=3, incy=2) + assert_array_equal(x, y[::2]) + + def test_x_and_y_stride(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + y = self.blas_func(x, y, n=3, incx=4, incy=2) + assert_array_equal(x[::4], y[::2]) + + def test_x_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=4, incx=5) + + def test_y_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=3, incy=5) + + # def test_y_bad_type(self): + ## Hmmm. Should this work? What should be the output. + # x = arange(3.,dtype=self.dtype) + # y = zeros(shape(x)) + # self.blas_func(x,y) + # assert_array_equal(x,y) + + +try: + class TestScopy(BaseCopy): + blas_func = fblas.scopy + dtype = float32 +except AttributeError: + class TestScopy: + pass + + +class TestDcopy(BaseCopy): + blas_func = fblas.dcopy + dtype = float64 + + +try: + class TestCcopy(BaseCopy): + blas_func = fblas.ccopy + dtype = complex64 +except AttributeError: + class TestCcopy: + pass + + +class TestZcopy(BaseCopy): + blas_func = fblas.zcopy + dtype = complex128 + + +################################################## +# Test blas ?swap + +class BaseSwap: + ''' Mixin class for swap tests ''' + + def test_simple(self): + x = arange(3., dtype=self.dtype) + y = zeros(shape(x), x.dtype) + desired_x = y.copy() + desired_y = x.copy() + x, y = self.blas_func(x, y) + assert_array_equal(desired_x, x) + assert_array_equal(desired_y, y) + + def test_x_stride(self): + x = arange(6., dtype=self.dtype) + y = zeros(3, x.dtype) + desired_x = y.copy() + desired_y = x.copy()[::2] + x, y = self.blas_func(x, y, n=3, incx=2) + assert_array_equal(desired_x, x[::2]) + assert_array_equal(desired_y, y) + + def test_y_stride(self): + x = arange(3., dtype=self.dtype) + y = zeros(6, x.dtype) + desired_x = y.copy()[::2] + desired_y = x.copy() + x, y = self.blas_func(x, y, n=3, incy=2) + assert_array_equal(desired_x, x) + assert_array_equal(desired_y, y[::2]) + + def test_x_and_y_stride(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + desired_x = y.copy()[::2] + desired_y = x.copy()[::4] + x, y = self.blas_func(x, y, n=3, incx=4, incy=2) + assert_array_equal(desired_x, x[::4]) + assert_array_equal(desired_y, y[::2]) + + def test_x_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=4, incx=5) + + def test_y_bad_size(self): + x = arange(12., dtype=self.dtype) + y = zeros(6, x.dtype) + with pytest.raises(Exception, match='failed for 1st keyword'): + self.blas_func(x, y, n=3, incy=5) + + +try: + class TestSswap(BaseSwap): + blas_func = fblas.sswap + dtype = float32 +except AttributeError: + class TestSswap: + pass + + +class TestDswap(BaseSwap): + blas_func = fblas.dswap + dtype = float64 + + +try: + class TestCswap(BaseSwap): + blas_func = fblas.cswap + dtype = complex64 +except AttributeError: + class TestCswap: + pass + + +class TestZswap(BaseSwap): + blas_func = fblas.zswap + dtype = complex128 + +################################################## +# Test blas ?gemv +# This will be a mess to test all cases. + + +class BaseGemv: + ''' Mixin class for gemv tests ''' + + def get_data(self, x_stride=1, y_stride=1): + rng = np.random.default_rng(1234) + mult = array(1, dtype=self.dtype) + if self.dtype in [complex64, complex128]: + mult = array(1+1j, dtype=self.dtype) + alpha = array(1., dtype=self.dtype) * mult + beta = array(1., dtype=self.dtype) * mult + a = rng.normal(0., 1., (3, 3)).astype(self.dtype) * mult + x = arange(shape(a)[0]*x_stride, dtype=self.dtype) * mult + y = arange(shape(a)[1]*y_stride, dtype=self.dtype) * mult + return alpha, beta, a, x, y + + def test_simple(self): + alpha, beta, a, x, y = self.get_data() + desired_y = alpha*matrixmultiply(a, x)+beta*y + y = self.blas_func(alpha, a, x, beta, y) + assert_array_almost_equal(desired_y, y) + + def test_default_beta_y(self): + alpha, beta, a, x, y = self.get_data() + desired_y = matrixmultiply(a, x) + y = self.blas_func(1, a, x) + assert_array_almost_equal(desired_y, y) + + def test_simple_transpose(self): + alpha, beta, a, x, y = self.get_data() + desired_y = alpha*matrixmultiply(transpose(a), x)+beta*y + y = self.blas_func(alpha, a, x, beta, y, trans=1) + assert_array_almost_equal(desired_y, y) + + def test_simple_transpose_conj(self): + alpha, beta, a, x, y = self.get_data() + desired_y = alpha*matrixmultiply(transpose(conjugate(a)), x)+beta*y + y = self.blas_func(alpha, a, x, beta, y, trans=2) + assert_array_almost_equal(desired_y, y) + + def test_x_stride(self): + alpha, beta, a, x, y = self.get_data(x_stride=2) + desired_y = alpha*matrixmultiply(a, x[::2])+beta*y + y = self.blas_func(alpha, a, x, beta, y, incx=2) + assert_array_almost_equal(desired_y, y) + + def test_x_stride_transpose(self): + alpha, beta, a, x, y = self.get_data(x_stride=2) + desired_y = alpha*matrixmultiply(transpose(a), x[::2])+beta*y + y = self.blas_func(alpha, a, x, beta, y, trans=1, incx=2) + assert_array_almost_equal(desired_y, y) + + def test_x_stride_assert(self): + # What is the use of this test? + alpha, beta, a, x, y = self.get_data(x_stride=2) + with pytest.raises(Exception, match='failed for 3rd argument'): + y = self.blas_func(1, a, x, 1, y, trans=0, incx=3) + with pytest.raises(Exception, match='failed for 3rd argument'): + y = self.blas_func(1, a, x, 1, y, trans=1, incx=3) + + def test_y_stride(self): + alpha, beta, a, x, y = self.get_data(y_stride=2) + desired_y = y.copy() + desired_y[::2] = alpha*matrixmultiply(a, x)+beta*y[::2] + y = self.blas_func(alpha, a, x, beta, y, incy=2) + assert_array_almost_equal(desired_y, y) + + def test_y_stride_transpose(self): + alpha, beta, a, x, y = self.get_data(y_stride=2) + desired_y = y.copy() + desired_y[::2] = alpha*matrixmultiply(transpose(a), x)+beta*y[::2] + y = self.blas_func(alpha, a, x, beta, y, trans=1, incy=2) + assert_array_almost_equal(desired_y, y) + + def test_y_stride_assert(self): + # What is the use of this test? + alpha, beta, a, x, y = self.get_data(y_stride=2) + with pytest.raises(Exception, match='failed for 2nd keyword'): + y = self.blas_func(1, a, x, 1, y, trans=0, incy=3) + with pytest.raises(Exception, match='failed for 2nd keyword'): + y = self.blas_func(1, a, x, 1, y, trans=1, incy=3) + + +try: + class TestSgemv(BaseGemv): + blas_func = fblas.sgemv + dtype = float32 + + @pytest.mark.skipif(sys.platform != 'darwin', reason="MacOS specific test") + def test_sgemv_on_osx(self): + def aligned_array(shape, align, dtype, order='C'): + # Make array shape `shape` with aligned at `align` bytes + d = dtype() + # Make array of correct size with `align` extra bytes + N = np.prod(shape) + tmp = np.zeros(N * d.nbytes + align, dtype=np.uint8) + address = tmp.__array_interface__["data"][0] + # Find offset into array giving desired alignment + for offset in range(align): + if (address + offset) % align == 0: + break + tmp = tmp[offset:offset+N*d.nbytes].view(dtype=dtype) + return tmp.reshape(shape, order=order) + + def as_aligned(arr, align, dtype, order='C'): + # Copy `arr` into an aligned array with same shape + aligned = aligned_array(arr.shape, align, dtype, order) + aligned[:] = arr[:] + return aligned + + def assert_dot_close(A, X, desired): + assert_allclose(self.blas_func(1.0, A, X), desired, + rtol=1e-5, atol=1e-7) + + testdata = product((15, 32), (10000,), (200, 89), ('C', 'F')) + rng = np.random.default_rng(1234) + for align, m, n, a_order in testdata: + A_d = rng.random((m, n)) + X_d = rng.random(n) + desired = np.dot(A_d, X_d) + # Calculation with aligned single precision + A_f = as_aligned(A_d, align, np.float32, order=a_order) + X_f = as_aligned(X_d, align, np.float32, order=a_order) + assert_dot_close(A_f, X_f, desired) + +except AttributeError: + class TestSgemv: + pass + + +class TestDgemv(BaseGemv): + blas_func = fblas.dgemv + dtype = float64 + + +try: + class TestCgemv(BaseGemv): + blas_func = fblas.cgemv + dtype = complex64 +except AttributeError: + class TestCgemv: + pass + + +class TestZgemv(BaseGemv): + blas_func = fblas.zgemv + dtype = complex128 + + +""" +################################################## +### Test blas ?ger +### This will be a mess to test all cases. + +class BaseGer: + def get_data(self,x_stride=1,y_stride=1): + rng = np.random.default_rng(1234) + alpha = array(1., dtype = self.dtype) + a = rng.normal(0.,1.,(3,3)).astype(self.dtype) + x = arange(shape(a)[0]*x_stride,dtype=self.dtype) + y = arange(shape(a)[1]*y_stride,dtype=self.dtype) + return alpha,a,x,y + def test_simple(self): + alpha,a,x,y = self.get_data() + # transpose takes care of Fortran vs. C(and Python) memory layout + desired_a = alpha*transpose(x[:,newaxis]*y) + a + self.blas_func(x,y,a) + assert_array_almost_equal(desired_a,a) + def test_x_stride(self): + alpha,a,x,y = self.get_data(x_stride=2) + desired_a = alpha*transpose(x[::2,newaxis]*y) + a + self.blas_func(x,y,a,incx=2) + assert_array_almost_equal(desired_a,a) + def test_x_stride_assert(self): + alpha,a,x,y = self.get_data(x_stride=2) + with pytest.raises(ValueError, match='foo'): + self.blas_func(x,y,a,incx=3) + def test_y_stride(self): + alpha,a,x,y = self.get_data(y_stride=2) + desired_a = alpha*transpose(x[:,newaxis]*y[::2]) + a + self.blas_func(x,y,a,incy=2) + assert_array_almost_equal(desired_a,a) + + def test_y_stride_assert(self): + alpha,a,x,y = self.get_data(y_stride=2) + with pytest.raises(ValueError, match='foo'): + self.blas_func(a,x,y,incy=3) + +class TestSger(BaseGer): + blas_func = fblas.sger + dtype = float32 +class TestDger(BaseGer): + blas_func = fblas.dger + dtype = float64 +""" +################################################## +# Test blas ?gerc +# This will be a mess to test all cases. + +""" +class BaseGerComplex(BaseGer): + def get_data(self,x_stride=1,y_stride=1): + rng = np.random.default_rng(1234) + alpha = array(1+1j, dtype = self.dtype) + a = rng.normal(0.,1.,(3,3)).astype(self.dtype) + a = a + rng.normal(0.,1.,(3,3)) * array(1j, dtype = self.dtype) + x = rng.normal(0.,1.,shape(a)[0]*x_stride).astype(self.dtype) + x = x + x * array(1j, dtype = self.dtype) + y = rng.normal(0.,1.,shape(a)[1]*y_stride).astype(self.dtype) + y = y + y * array(1j, dtype = self.dtype) + return alpha,a,x,y + def test_simple(self): + alpha,a,x,y = self.get_data() + # transpose takes care of Fortran vs. C(and Python) memory layout + a = a * array(0.,dtype = self.dtype) + #desired_a = alpha*transpose(x[:,newaxis]*self.transform(y)) + a + desired_a = alpha*transpose(x[:,newaxis]*y) + a + #self.blas_func(x,y,a,alpha = alpha) + fblas.cgeru(x,y,a,alpha = alpha) + assert_array_almost_equal(desired_a,a) + + #def test_x_stride(self): + # alpha,a,x,y = self.get_data(x_stride=2) + # desired_a = alpha*transpose(x[::2,newaxis]*self.transform(y)) + a + # self.blas_func(x,y,a,incx=2) + # assert_array_almost_equal(desired_a,a) + #def test_y_stride(self): + # alpha,a,x,y = self.get_data(y_stride=2) + # desired_a = alpha*transpose(x[:,newaxis]*self.transform(y[::2])) + a + # self.blas_func(x,y,a,incy=2) + # assert_array_almost_equal(desired_a,a) + +class TestCgeru(BaseGerComplex): + blas_func = fblas.cgeru + dtype = complex64 + def transform(self,x): + return x +class TestZgeru(BaseGerComplex): + blas_func = fblas.zgeru + dtype = complex128 + def transform(self,x): + return x + +class TestCgerc(BaseGerComplex): + blas_func = fblas.cgerc + dtype = complex64 + def transform(self,x): + return conjugate(x) + +class TestZgerc(BaseGerComplex): + blas_func = fblas.zgerc + dtype = complex128 + def transform(self,x): + return conjugate(x) +""" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_interpolative.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_interpolative.py new file mode 100644 index 0000000000000000000000000000000000000000..bc250b475afb8087a09021a5bc7d803c137761e5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_interpolative.py @@ -0,0 +1,232 @@ +# ****************************************************************************** +# Copyright (C) 2013 Kenneth L. Ho +# 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. +# +# None of the names of the copyright holders 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. +# ****************************************************************************** + +import scipy.linalg.interpolative as pymatrixid +import numpy as np +from scipy.linalg import hilbert, svdvals, norm +from scipy.sparse.linalg import aslinearoperator +from scipy.linalg.interpolative import interp_decomp + +from numpy.testing import (assert_, assert_allclose, assert_equal, + assert_array_equal) +import pytest +from pytest import raises as assert_raises + + +@pytest.fixture() +def eps(): + yield 1e-12 + + +@pytest.fixture() +def rng(): + rng = np.random.default_rng(1718313768084012) + yield rng + + +@pytest.fixture(params=[np.float64, np.complex128]) +def A(request): + # construct Hilbert matrix + # set parameters + n = 300 + yield hilbert(n).astype(request.param) + + +@pytest.fixture() +def L(A): + yield aslinearoperator(A) + + +@pytest.fixture() +def rank(A, eps): + S = np.linalg.svd(A, compute_uv=False) + try: + rank = np.nonzero(S < eps)[0][0] + except IndexError: + rank = A.shape[0] + return rank + + +class TestInterpolativeDecomposition: + + @pytest.mark.parametrize( + "rand,lin_op", + [(False, False), (True, False), (True, True)]) + def test_real_id_fixed_precision(self, A, L, eps, rand, lin_op, rng): + # Test ID routines on a Hilbert matrix. + A_or_L = A if not lin_op else L + + k, idx, proj = pymatrixid.interp_decomp(A_or_L, eps, rand=rand, rng=rng) + B = pymatrixid.reconstruct_matrix_from_id(A[:, idx[:k]], idx, proj) + assert_allclose(A, B, rtol=eps, atol=1e-08) + + @pytest.mark.parametrize( + "rand,lin_op", + [(False, False), (True, False), (True, True)]) + def test_real_id_fixed_rank(self, A, L, eps, rank, rand, lin_op, rng): + k = rank + A_or_L = A if not lin_op else L + + idx, proj = pymatrixid.interp_decomp(A_or_L, k, rand=rand, rng=rng) + B = pymatrixid.reconstruct_matrix_from_id(A[:, idx[:k]], idx, proj) + assert_allclose(A, B, rtol=eps, atol=1e-08) + + @pytest.mark.parametrize("rand,lin_op", [(False, False)]) + def test_real_id_skel_and_interp_matrices( + self, A, L, eps, rank, rand, lin_op, rng): + k = rank + A_or_L = A if not lin_op else L + + idx, proj = pymatrixid.interp_decomp(A_or_L, k, rand=rand, rng=rng) + P = pymatrixid.reconstruct_interp_matrix(idx, proj) + B = pymatrixid.reconstruct_skel_matrix(A, k, idx) + assert_allclose(B, A[:, idx[:k]], rtol=eps, atol=1e-08) + assert_allclose(B @ P, A, rtol=eps, atol=1e-08) + + @pytest.mark.parametrize( + "rand,lin_op", + [(False, False), (True, False), (True, True)]) + def test_svd_fixed_precision(self, A, L, eps, rand, lin_op, rng): + A_or_L = A if not lin_op else L + + U, S, V = pymatrixid.svd(A_or_L, eps, rand=rand, rng=rng) + B = U * S @ V.T.conj() + assert_allclose(A, B, rtol=eps, atol=1e-08) + + @pytest.mark.parametrize( + "rand,lin_op", + [(False, False), (True, False), (True, True)]) + def test_svd_fixed_rank(self, A, L, eps, rank, rand, lin_op, rng): + k = rank + A_or_L = A if not lin_op else L + + U, S, V = pymatrixid.svd(A_or_L, k, rand=rand, rng=rng) + B = U * S @ V.T.conj() + assert_allclose(A, B, rtol=eps, atol=1e-08) + + def test_id_to_svd(self, A, eps, rank): + k = rank + + idx, proj = pymatrixid.interp_decomp(A, k, rand=False) + U, S, V = pymatrixid.id_to_svd(A[:, idx[:k]], idx, proj) + B = U * S @ V.T.conj() + assert_allclose(A, B, rtol=eps, atol=1e-08) + + def test_estimate_spectral_norm(self, A, rng): + s = svdvals(A) + norm_2_est = pymatrixid.estimate_spectral_norm(A, rng=rng) + assert_allclose(norm_2_est, s[0], rtol=1e-6, atol=1e-8) + + def test_estimate_spectral_norm_diff(self, A, rng): + B = A.copy() + B[:, 0] *= 1.2 + s = svdvals(A - B) + norm_2_est = pymatrixid.estimate_spectral_norm_diff(A, B, rng=rng) + assert_allclose(norm_2_est, s[0], rtol=1e-6, atol=1e-8) + + def test_rank_estimates_array(self, A, rng): + B = np.array([[1, 1, 0], [0, 0, 1], [0, 0, 1]], dtype=A.dtype) + + for M in [A, B]: + rank_tol = 1e-9 + rank_np = np.linalg.matrix_rank(M, norm(M, 2) * rank_tol) + rank_est = pymatrixid.estimate_rank(M, rank_tol, rng=rng) + assert_(rank_est >= rank_np) + assert_(rank_est <= rank_np + 10) + + def test_rank_estimates_lin_op(self, A, rng): + B = np.array([[1, 1, 0], [0, 0, 1], [0, 0, 1]], dtype=A.dtype) + + for M in [A, B]: + ML = aslinearoperator(M) + rank_tol = 1e-9 + rank_np = np.linalg.matrix_rank(M, norm(M, 2) * rank_tol) + rank_est = pymatrixid.estimate_rank(ML, rank_tol, rng=rng) + assert_(rank_est >= rank_np - 4) + assert_(rank_est <= rank_np + 4) + + def test_badcall(self): + A = hilbert(5).astype(np.float32) + with assert_raises(ValueError): + pymatrixid.interp_decomp(A, 1e-6, rand=False) + + def test_rank_too_large(self): + # svd(array, k) should not segfault + a = np.ones((4, 3)) + with assert_raises(ValueError): + pymatrixid.svd(a, 4) + + def test_full_rank(self): + eps = 1.0e-12 + rng = np.random.default_rng(1234) + # fixed precision + A = rng.random((16, 8)) + k, idx, proj = pymatrixid.interp_decomp(A, eps) + assert_equal(k, A.shape[1]) + + P = pymatrixid.reconstruct_interp_matrix(idx, proj) + B = pymatrixid.reconstruct_skel_matrix(A, k, idx) + assert_allclose(A, B @ P) + + # fixed rank + idx, proj = pymatrixid.interp_decomp(A, k) + + P = pymatrixid.reconstruct_interp_matrix(idx, proj) + B = pymatrixid.reconstruct_skel_matrix(A, k, idx) + assert_allclose(A, B @ P) + + @pytest.mark.parametrize("dtype", [np.float64, np.complex128]) + @pytest.mark.parametrize("rand", [True, False]) + @pytest.mark.parametrize("eps", [1, 0.1]) + def test_bug_9793(self, dtype, rand, eps): + A = np.array([[-1, -1, -1, 0, 0, 0], + [0, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 1]], + dtype=dtype, order="C") + B = A.copy() + interp_decomp(A.T, eps, rand=rand) + assert_array_equal(A, B) + + def test_svd_aslinearoperator_shape_check(self): + # See gh-issue #22451 + rng = np.random.default_rng(1744580941832515) + x = rng.uniform(size=[7, 5]) + xl = aslinearoperator(x) + u, s, v = pymatrixid.svd(xl, 3) + assert_equal(u.shape, (7, 3)) + assert_equal(s.shape, (3,)) + assert_equal(v.shape, (5, 3)) + + x = rng.uniform(size=[4, 9]) + xl = aslinearoperator(x) + u, s, v = pymatrixid.svd(xl, 2) + assert_equal(u.shape, (4, 2)) + assert_equal(s.shape, (2,)) + assert_equal(v.shape, (9, 2)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_lapack.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_lapack.py new file mode 100644 index 0000000000000000000000000000000000000000..0803809a86ee625ae3c3a2db3c3ae3089b9b412b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_lapack.py @@ -0,0 +1,3616 @@ +# +# Created by: Pearu Peterson, September 2002 +# + +from functools import reduce +import sysconfig + +from numpy.testing import (assert_equal, assert_array_almost_equal, assert_, + assert_allclose, assert_almost_equal, + assert_array_equal) +import pytest +from pytest import raises as assert_raises + +import numpy as np +from numpy import (eye, ones, zeros, zeros_like, triu, tril, tril_indices, + triu_indices) + +from scipy.linalg import (_flapack as flapack, lapack, inv, svd, cholesky, + solve, ldl, norm, block_diag, qr, eigh, qz) +from scipy.linalg._basic import _to_banded +from scipy.linalg.lapack import _compute_lwork +from scipy.stats import ortho_group, unitary_group + +import scipy.sparse as sps + +try: + from scipy.linalg import _clapack as clapack +except ImportError: + clapack = None +from scipy.linalg.lapack import get_lapack_funcs +from scipy.linalg.blas import get_blas_funcs + +from scipy.__config__ import CONFIG +blas_provider = blas_version = None +blas_provider = CONFIG['Build Dependencies']['blas']['name'] +blas_version = CONFIG['Build Dependencies']['blas']['version'] + +REAL_DTYPES = [np.float32, np.float64] +COMPLEX_DTYPES = [np.complex64, np.complex128] +DTYPES = REAL_DTYPES + COMPLEX_DTYPES + + +def generate_random_dtype_array(shape, dtype, rng): + # generates a random matrix of desired data type of shape + if dtype in COMPLEX_DTYPES: + return (rng.rand(*shape) + + rng.rand(*shape)*1.0j).astype(dtype) + return rng.rand(*shape).astype(dtype) + + +def test_lapack_documented(): + """Test that all entries are in the doc.""" + if lapack.__doc__ is None: # just in case there is a python -OO + pytest.skip('lapack.__doc__ is None') + names = set(lapack.__doc__.split()) + ignore_list = { + "absolute_import", + "clapack", + "division", + "find_best_lapack_type", + "flapack", + "print_function", + "HAS_ILP64", + "np", + } + missing = list() + for name in dir(lapack): + if (not name.startswith('_') and name not in ignore_list and + name not in names): + missing.append(name) + assert missing == [], 'Name(s) missing from lapack.__doc__ or ignore_list' + + +def test_ilp64_blas_lapack_both_or_none(): + from scipy.linalg.blas import HAS_ILP64 as blas_has_ilp64 + from scipy.linalg.lapack import HAS_ILP64 as lapack_has_ilp64 + assert blas_has_ilp64 == lapack_has_ilp64 + + +class TestFlapackSimple: + + def test_gebal(self): + a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + a1 = [[1, 0, 0, 3e-4], + [4, 0, 0, 2e-3], + [7, 1, 0, 0], + [0, 1, 0, 0]] + for p in 'sdzc': + f = getattr(flapack, p+'gebal', None) + if f is None: + continue + ba, lo, hi, pivscale, info = f(a) + assert_(not info, repr(info)) + assert_array_almost_equal(ba, a) + assert_equal((lo, hi), (0, len(a[0])-1)) + assert_array_almost_equal(pivscale, np.ones(len(a))) + + ba, lo, hi, pivscale, info = f(a1, permute=1, scale=1) + assert_(not info, repr(info)) + # print(a1) + # print(ba, lo, hi, pivscale) + + def test_gehrd(self): + a = [[-149, -50, -154], + [537, 180, 546], + [-27, -9, -25]] + for p in 'd': + f = getattr(flapack, p+'gehrd', None) + if f is None: + continue + ht, tau, info = f(a) + assert_(not info, repr(info)) + + def test_trsyl(self): + a = np.array([[1, 2], [0, 4]]) + b = np.array([[5, 6], [0, 8]]) + c = np.array([[9, 10], [11, 12]]) + trans = 'T' + + # Test single and double implementations, including most + # of the options + for dtype in 'fdFD': + a1, b1, c1 = a.astype(dtype), b.astype(dtype), c.astype(dtype) + trsyl, = get_lapack_funcs(('trsyl',), (a1,)) + if dtype.isupper(): # is complex dtype + a1[0] += 1j + trans = 'C' + + x, scale, info = trsyl(a1, b1, c1) + assert_array_almost_equal(np.dot(a1, x) + np.dot(x, b1), + scale * c1) + + x, scale, info = trsyl(a1, b1, c1, trana=trans, tranb=trans) + assert_array_almost_equal( + np.dot(a1.conjugate().T, x) + np.dot(x, b1.conjugate().T), + scale * c1, decimal=4) + + x, scale, info = trsyl(a1, b1, c1, isgn=-1) + assert_array_almost_equal(np.dot(a1, x) - np.dot(x, b1), + scale * c1, decimal=4) + + def test_lange(self): + a = np.array([ + [-149, -50, -154], + [537, 180, 546], + [-27, -9, -25]]) + + for dtype in 'fdFD': + for norm_str in 'Mm1OoIiFfEe': + a1 = a.astype(dtype) + if dtype.isupper(): + # is complex dtype + a1[0, 0] += 1j + + lange, = get_lapack_funcs(('lange',), (a1,)) + value = lange(norm_str, a1) + + if norm_str in 'FfEe': + if dtype in 'Ff': + decimal = 3 + else: + decimal = 7 + ref = np.sqrt(np.sum(np.square(np.abs(a1)))) + assert_almost_equal(value, ref, decimal) + else: + if norm_str in 'Mm': + ref = np.max(np.abs(a1)) + elif norm_str in '1Oo': + ref = np.max(np.sum(np.abs(a1), axis=0)) + elif norm_str in 'Ii': + ref = np.max(np.sum(np.abs(a1), axis=1)) + + assert_equal(value, ref) + + +class TestLapack: + + def test_flapack(self): + if hasattr(flapack, 'empty_module'): + # flapack module is empty + pass + + def test_clapack(self): + if hasattr(clapack, 'empty_module'): + # clapack module is empty + pass + + +class TestLeastSquaresSolvers: + + def test_gels(self): + rng = np.random.default_rng(1234) + # Test fat/tall matrix argument handling - gh-issue #8329 + for ind, dtype in enumerate(DTYPES): + m = 10 + n = 20 + nrhs = 1 + a1 = rng.random((m, n)).astype(dtype) + b1 = rng.random(n).astype(dtype) + gls, glslw = get_lapack_funcs(('gels', 'gels_lwork'), dtype=dtype) + + # Request of sizes + lwork = _compute_lwork(glslw, m, n, nrhs) + _, _, info = gls(a1, b1, lwork=lwork) + assert_(info >= 0) + _, _, info = gls(a1, b1, trans='TTCC'[ind], lwork=lwork) + assert_(info >= 0) + + for dtype in REAL_DTYPES: + a1 = np.array([[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]], dtype=dtype) + b1 = np.array([16.0, 17.0, 20.0], dtype=dtype) + gels, gels_lwork, geqrf = get_lapack_funcs( + ('gels', 'gels_lwork', 'geqrf'), (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + lwork = _compute_lwork(gels_lwork, m, n, nrhs) + + lqr, x, info = gels(a1, b1, lwork=lwork) + assert_allclose(x[:-1], np.array([-14.333333333333323, + 14.999999999999991], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + lqr_truth, _, _, _ = geqrf(a1) + assert_array_equal(lqr, lqr_truth) + + for dtype in COMPLEX_DTYPES: + a1 = np.array([[1.0+4.0j, 2.0], + [4.0+0.5j, 5.0-3.0j], + [7.0-2.0j, 8.0+0.7j]], dtype=dtype) + b1 = np.array([16.0, 17.0+2.0j, 20.0-4.0j], dtype=dtype) + gels, gels_lwork, geqrf = get_lapack_funcs( + ('gels', 'gels_lwork', 'geqrf'), (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + lwork = _compute_lwork(gels_lwork, m, n, nrhs) + + lqr, x, info = gels(a1, b1, lwork=lwork) + assert_allclose(x[:-1], + np.array([1.161753632288328-1.901075709391912j, + 1.735882340522193+1.521240901196909j], + dtype=dtype), rtol=25*np.finfo(dtype).eps) + lqr_truth, _, _, _ = geqrf(a1) + assert_array_equal(lqr, lqr_truth) + + def test_gelsd(self): + for dtype in REAL_DTYPES: + a1 = np.array([[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]], dtype=dtype) + b1 = np.array([16.0, 17.0, 20.0], dtype=dtype) + gelsd, gelsd_lwork = get_lapack_funcs(('gelsd', 'gelsd_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, iwork, info = gelsd_lwork(m, n, nrhs, -1) + lwork = int(np.real(work)) + iwork_size = iwork + + x, s, rank, info = gelsd(a1, b1, lwork, iwork_size, + -1, False, False) + assert_allclose(x[:-1], np.array([-14.333333333333323, + 14.999999999999991], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + assert_allclose(s, np.array([12.596017180511966, + 0.583396253199685], dtype=dtype), + rtol=25*np.finfo(dtype).eps) + + for dtype in COMPLEX_DTYPES: + a1 = np.array([[1.0+4.0j, 2.0], + [4.0+0.5j, 5.0-3.0j], + [7.0-2.0j, 8.0+0.7j]], dtype=dtype) + b1 = np.array([16.0, 17.0+2.0j, 20.0-4.0j], dtype=dtype) + gelsd, gelsd_lwork = get_lapack_funcs(('gelsd', 'gelsd_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, rwork, iwork, info = gelsd_lwork(m, n, nrhs, -1) + lwork = int(np.real(work)) + rwork_size = int(rwork) + iwork_size = iwork + + x, s, rank, info = gelsd(a1, b1, lwork, rwork_size, iwork_size, + -1, False, False) + assert_allclose(x[:-1], + np.array([1.161753632288328-1.901075709391912j, + 1.735882340522193+1.521240901196909j], + dtype=dtype), rtol=25*np.finfo(dtype).eps) + assert_allclose(s, + np.array([13.035514762572043, 4.337666985231382], + dtype=dtype), rtol=25*np.finfo(dtype).eps) + + def test_gelss(self): + + for dtype in REAL_DTYPES: + a1 = np.array([[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]], dtype=dtype) + b1 = np.array([16.0, 17.0, 20.0], dtype=dtype) + gelss, gelss_lwork = get_lapack_funcs(('gelss', 'gelss_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, info = gelss_lwork(m, n, nrhs, -1) + lwork = int(np.real(work)) + + v, x, s, rank, work, info = gelss(a1, b1, -1, lwork, False, False) + assert_allclose(x[:-1], np.array([-14.333333333333323, + 14.999999999999991], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + assert_allclose(s, np.array([12.596017180511966, + 0.583396253199685], dtype=dtype), + rtol=25*np.finfo(dtype).eps) + + for dtype in COMPLEX_DTYPES: + a1 = np.array([[1.0+4.0j, 2.0], + [4.0+0.5j, 5.0-3.0j], + [7.0-2.0j, 8.0+0.7j]], dtype=dtype) + b1 = np.array([16.0, 17.0+2.0j, 20.0-4.0j], dtype=dtype) + gelss, gelss_lwork = get_lapack_funcs(('gelss', 'gelss_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, info = gelss_lwork(m, n, nrhs, -1) + lwork = int(np.real(work)) + + v, x, s, rank, work, info = gelss(a1, b1, -1, lwork, False, False) + assert_allclose(x[:-1], + np.array([1.161753632288328-1.901075709391912j, + 1.735882340522193+1.521240901196909j], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + assert_allclose(s, np.array([13.035514762572043, + 4.337666985231382], dtype=dtype), + rtol=25*np.finfo(dtype).eps) + + def test_gelsy(self): + + for dtype in REAL_DTYPES: + a1 = np.array([[1.0, 2.0], + [4.0, 5.0], + [7.0, 8.0]], dtype=dtype) + b1 = np.array([16.0, 17.0, 20.0], dtype=dtype) + gelsy, gelsy_lwork = get_lapack_funcs(('gelsy', 'gelss_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, info = gelsy_lwork(m, n, nrhs, 10*np.finfo(dtype).eps) + lwork = int(np.real(work)) + + jptv = np.zeros((a1.shape[1], 1), dtype=np.int32) + v, x, j, rank, info = gelsy(a1, b1, jptv, np.finfo(dtype).eps, + lwork, False, False) + assert_allclose(x[:-1], np.array([-14.333333333333323, + 14.999999999999991], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + + for dtype in COMPLEX_DTYPES: + a1 = np.array([[1.0+4.0j, 2.0], + [4.0+0.5j, 5.0-3.0j], + [7.0-2.0j, 8.0+0.7j]], dtype=dtype) + b1 = np.array([16.0, 17.0+2.0j, 20.0-4.0j], dtype=dtype) + gelsy, gelsy_lwork = get_lapack_funcs(('gelsy', 'gelss_lwork'), + (a1, b1)) + + m, n = a1.shape + if len(b1.shape) == 2: + nrhs = b1.shape[1] + else: + nrhs = 1 + + # Request of sizes + work, info = gelsy_lwork(m, n, nrhs, 10*np.finfo(dtype).eps) + lwork = int(np.real(work)) + + jptv = np.zeros((a1.shape[1], 1), dtype=np.int32) + v, x, j, rank, info = gelsy(a1, b1, jptv, np.finfo(dtype).eps, + lwork, False, False) + assert_allclose(x[:-1], + np.array([1.161753632288328-1.901075709391912j, + 1.735882340522193+1.521240901196909j], + dtype=dtype), + rtol=25*np.finfo(dtype).eps) + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('shape', [(3, 4), (5, 2), (2**18, 2**18)]) +def test_geqrf_lwork(dtype, shape): + geqrf_lwork = get_lapack_funcs(('geqrf_lwork'), dtype=dtype) + m, n = shape + lwork, info = geqrf_lwork(m=m, n=n) + assert_equal(info, 0) + + +class TestRegression: + + def test_ticket_1645(self): + # Check that RQ routines have correct lwork + for dtype in DTYPES: + a = np.zeros((300, 2), dtype=dtype) + + gerqf, = get_lapack_funcs(['gerqf'], [a]) + assert_raises(Exception, gerqf, a, lwork=2) + rq, tau, work, info = gerqf(a) + + if dtype in REAL_DTYPES: + orgrq, = get_lapack_funcs(['orgrq'], [a]) + assert_raises(Exception, orgrq, rq[-2:], tau, lwork=1) + orgrq(rq[-2:], tau, lwork=2) + elif dtype in COMPLEX_DTYPES: + ungrq, = get_lapack_funcs(['ungrq'], [a]) + assert_raises(Exception, ungrq, rq[-2:], tau, lwork=1) + ungrq(rq[-2:], tau, lwork=2) + + +class TestDpotr: + # 'lower' argument of dportf/dpotri + @pytest.mark.parametrize("lower", [True, False]) + @pytest.mark.parametrize("clean", [True, False]) + def test_gh_2691(self, lower, clean): + rng = np.random.default_rng(42) + x = rng.normal(size=(3, 3)) + a = x.dot(x.T) + + dpotrf, dpotri = get_lapack_funcs(("potrf", "potri"), (a, )) + + c, _ = dpotrf(a, lower, clean=clean) + dpt = dpotri(c, lower)[0] + + if lower: + assert_allclose(np.tril(dpt), np.tril(inv(a))) + else: + assert_allclose(np.triu(dpt), np.triu(inv(a))) + + +class TestDlasd4: + def test_sing_val_update(self): + + sigmas = np.array([4., 3., 2., 0]) + m_vec = np.array([3.12, 5.7, -4.8, -2.2]) + + M = np.hstack((np.vstack((np.diag(sigmas[0:-1]), + np.zeros((1, len(m_vec) - 1)))), + m_vec[:, np.newaxis])) + SM = svd(M, full_matrices=False, compute_uv=False, overwrite_a=False, + check_finite=False) + + it_len = len(sigmas) + sgm = np.concatenate((sigmas[::-1], [sigmas[0] + it_len*norm(m_vec)])) + mvc = np.concatenate((m_vec[::-1], (0,))) + + lasd4 = get_lapack_funcs('lasd4', (sigmas,)) + + roots = [] + for i in range(0, it_len): + res = lasd4(i, sgm, mvc) + roots.append(res[1]) + + assert_( + (res[3] <= 0), + f"LAPACK root finding dlasd4 failed to find the singular value {i}" + ) + roots = np.array(roots)[::-1] + + assert_((not np.any(np.isnan(roots)), "There are NaN roots")) + assert_allclose(SM, roots, atol=100*np.finfo(np.float64).eps, + rtol=100*np.finfo(np.float64).eps) + + +class TestTbtrs: + + @pytest.mark.parametrize('dtype', DTYPES) + def test_nag_example_f07vef_f07vsf(self, dtype): + """Test real (f07vef) and complex (f07vsf) examples from NAG + + Examples available from: + * https://www.nag.com/numeric/fl/nagdoc_latest/html/f07/f07vef.html + * https://www.nag.com/numeric/fl/nagdoc_latest/html/f07/f07vsf.html + + """ + if dtype in REAL_DTYPES: + ab = np.array([[-4.16, 4.78, 6.32, 0.16], + [-2.25, 5.86, -4.82, 0]], + dtype=dtype) + b = np.array([[-16.64, -4.16], + [-13.78, -16.59], + [13.10, -4.94], + [-14.14, -9.96]], + dtype=dtype) + x_out = np.array([[4, 1], + [-1, -3], + [3, 2], + [2, -2]], + dtype=dtype) + elif dtype in COMPLEX_DTYPES: + ab = np.array([[-1.94+4.43j, 4.12-4.27j, 0.43-2.66j, 0.44+0.1j], + [-3.39+3.44j, -1.84+5.52j, 1.74 - 0.04j, 0], + [1.62+3.68j, -2.77-1.93j, 0, 0]], + dtype=dtype) + b = np.array([[-8.86 - 3.88j, -24.09 - 5.27j], + [-15.57 - 23.41j, -57.97 + 8.14j], + [-7.63 + 22.78j, 19.09 - 29.51j], + [-14.74 - 2.40j, 19.17 + 21.33j]], + dtype=dtype) + x_out = np.array([[2j, 1 + 5j], + [1 - 3j, -7 - 2j], + [-4.001887 - 4.988417j, 3.026830 + 4.003182j], + [1.996158 - 1.045105j, -6.103357 - 8.986653j]], + dtype=dtype) + else: + raise ValueError(f"Datatype {dtype} not understood.") + + tbtrs = get_lapack_funcs(('tbtrs'), dtype=dtype) + x, info = tbtrs(ab=ab, b=b, uplo='L') + assert_equal(info, 0) + assert_allclose(x, x_out, rtol=0, atol=1e-5) + + @pytest.mark.parametrize('dtype,trans', + [(dtype, trans) + for dtype in DTYPES for trans in ['N', 'T', 'C'] + if not (trans == 'C' and dtype in REAL_DTYPES)]) + @pytest.mark.parametrize('uplo', ['U', 'L']) + @pytest.mark.parametrize('diag', ['N', 'U']) + def test_random_matrices(self, dtype, trans, uplo, diag): + rng = np.random.RandomState(1724) + + # n, nrhs, kd are used to specify A and b. + # A is of shape n x n with kd super/sub-diagonals + # b is of shape n x nrhs matrix + n, nrhs, kd = 4, 3, 2 + tbtrs = get_lapack_funcs('tbtrs', dtype=dtype) + + is_upper = (uplo == 'U') + ku = kd * is_upper + kl = kd - ku + + # Construct the diagonal and kd super/sub diagonals of A with + # the corresponding offsets. + band_offsets = range(ku, -kl - 1, -1) + band_widths = [n - abs(x) for x in band_offsets] + bands = [generate_random_dtype_array((width,), dtype, rng) + for width in band_widths] + + if diag == 'U': # A must be unit triangular + bands[ku] = np.ones(n, dtype=dtype) + + # Construct the diagonal banded matrix A from the bands and offsets. + a = sps.diags(bands, band_offsets, format='dia') + + # Convert A into banded storage form + ab = np.zeros((kd + 1, n), dtype) + for row, k in enumerate(band_offsets): + ab[row, max(k, 0):min(n+k, n)] = a.diagonal(k) + + # The RHS values. + b = generate_random_dtype_array((n, nrhs), dtype, rng) + + x, info = tbtrs(ab=ab, b=b, uplo=uplo, trans=trans, diag=diag) + assert_equal(info, 0) + + if trans == 'N': + assert_allclose(a @ x, b, rtol=5e-5) + elif trans == 'T': + assert_allclose(a.T @ x, b, rtol=5e-5) + elif trans == 'C': + assert_allclose(a.T.conjugate() @ x, b, rtol=5e-5) + else: + raise ValueError('Invalid trans argument') + + @pytest.mark.parametrize('uplo,trans,diag', + [['U', 'N', 'Invalid'], + ['U', 'Invalid', 'N'], + ['Invalid', 'N', 'N']]) + def test_invalid_argument_raises_exception(self, uplo, trans, diag): + """Test if invalid values of uplo, trans and diag raise exceptions""" + # Argument checks occur independently of used datatype. + # This mean we must not parameterize all available datatypes. + tbtrs = get_lapack_funcs('tbtrs', dtype=np.float64) + rng = np.random.default_rng(1234) + ab = rng.random((4, 2)) + b = rng.random((2, 4)) + assert_raises(Exception, tbtrs, ab, b, uplo, trans, diag) + + def test_zero_element_in_diagonal(self): + """Test if a matrix with a zero diagonal element is singular + + If the i-th diagonal of A is zero, ?tbtrs should return `i` in `info` + indicating the provided matrix is singular. + + Note that ?tbtrs requires the matrix A to be stored in banded form. + In this form the diagonal corresponds to the last row.""" + ab = np.ones((3, 4), dtype=float) + b = np.ones(4, dtype=float) + tbtrs = get_lapack_funcs('tbtrs', dtype=float) + + ab[-1, 3] = 0 + _, info = tbtrs(ab=ab, b=b, uplo='U') + assert_equal(info, 4) + + @pytest.mark.parametrize('ldab,n,ldb,nrhs', [ + (5, 5, 0, 5), + (5, 5, 3, 5) + ]) + def test_invalid_matrix_shapes(self, ldab, n, ldb, nrhs): + """Test ?tbtrs fails correctly if shapes are invalid.""" + ab = np.ones((ldab, n), dtype=float) + b = np.ones((ldb, nrhs), dtype=float) + tbtrs = get_lapack_funcs('tbtrs', dtype=float) + assert_raises(Exception, tbtrs, ab, b) + + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('norm', ['I', '1', 'O']) +@pytest.mark.parametrize('uplo', ['U', 'L']) +@pytest.mark.parametrize('diag', ['N', 'U']) +@pytest.mark.parametrize('n', [3, 10]) +def test_trcon(dtype, norm, uplo, diag, n): + # Simple way to get deterministic (unlike `hash`) seed based on arguments + seed = list(f"{dtype}{norm}{uplo}{diag}{n}".encode()) + rng = np.random.default_rng(seed) + + A = rng.random(size=(n, n)) + rng.random(size=(n, n))*1j + # make the condition numbers more interesting + offset = rng.permuted(np.logspace(0, rng.integers(0, 10), n)) + A += offset + A = A.real if np.issubdtype(dtype, np.floating) else A + A = np.triu(A) if uplo == 'U' else np.tril(A) + if diag == 'U': + A /= np.diag(A)[:, np.newaxis] + A = A.astype(dtype) + + trcon = get_lapack_funcs('trcon', (A,)) + res, _ = trcon(A, norm=norm, uplo=uplo, diag=diag) + + if norm == 'I': + norm_A = np.linalg.norm(A, ord=np.inf) + norm_inv_A = np.linalg.norm(np.linalg.inv(A), ord=np.inf) + ref = 1 / (norm_A * norm_inv_A) + else: + anorm = np.linalg.norm(A, ord=1) + gecon, getrf = get_lapack_funcs(('gecon', 'getrf'), (A,)) + lu, ipvt, info = getrf(A) + ref, _ = gecon(lu, anorm, norm=norm) + + # This is an estimate of reciprocal condition number; we just need order of + # magnitude. In testing, we observed that much smaller rtol is OK in almost + # all cases... but sometimes it isn't. + rtol = 1 # np.finfo(dtype).eps**0.75 + assert_allclose(res, ref, rtol=rtol) + + +def test_lartg(): + for dtype in 'fdFD': + lartg = get_lapack_funcs('lartg', dtype=dtype) + + f = np.array(3, dtype) + g = np.array(4, dtype) + + if np.iscomplexobj(g): + g *= 1j + + cs, sn, r = lartg(f, g) + + assert_allclose(cs, 3.0/5.0) + assert_allclose(r, 5.0) + + if np.iscomplexobj(g): + assert_allclose(sn, -4.0j/5.0) + assert_(isinstance(r, complex)) + assert_(isinstance(cs, float)) + else: + assert_allclose(sn, 4.0/5.0) + + +def test_rot(): + # srot, drot from blas and crot and zrot from lapack. + + for dtype in 'fdFD': + c = 0.6 + s = 0.8 + + u = np.full(4, 3, dtype) + v = np.full(4, 4, dtype) + atol = 10**-(np.finfo(dtype).precision-1) + + if dtype in 'fd': + rot = get_blas_funcs('rot', dtype=dtype) + f = 4 + else: + rot = get_lapack_funcs('rot', dtype=dtype) + s *= -1j + v *= 1j + f = 4j + + assert_allclose(rot(u, v, c, s), [[5, 5, 5, 5], + [0, 0, 0, 0]], atol=atol) + assert_allclose(rot(u, v, c, s, n=2), [[5, 5, 3, 3], + [0, 0, f, f]], atol=atol) + assert_allclose(rot(u, v, c, s, offx=2, offy=2), + [[3, 3, 5, 5], [f, f, 0, 0]], atol=atol) + assert_allclose(rot(u, v, c, s, incx=2, offy=2, n=2), + [[5, 3, 5, 3], [f, f, 0, 0]], atol=atol) + assert_allclose(rot(u, v, c, s, offx=2, incy=2, n=2), + [[3, 3, 5, 5], [0, f, 0, f]], atol=atol) + assert_allclose(rot(u, v, c, s, offx=2, incx=2, offy=2, incy=2, n=1), + [[3, 3, 5, 3], [f, f, 0, f]], atol=atol) + assert_allclose(rot(u, v, c, s, incx=-2, incy=-2, n=2), + [[5, 3, 5, 3], [0, f, 0, f]], atol=atol) + + a, b = rot(u, v, c, s, overwrite_x=1, overwrite_y=1) + assert_(a is u) + assert_(b is v) + assert_allclose(a, [5, 5, 5, 5], atol=atol) + assert_allclose(b, [0, 0, 0, 0], atol=atol) + + +def test_larfg_larf(): + rng = np.random.default_rng(1234) + a0 = rng.random((4, 4)) + a0 = a0.T.dot(a0) + + a0j = rng.random((4, 4)) + 1j*rng.random((4, 4)) + a0j = a0j.T.conj().dot(a0j) + + # our test here will be to do one step of reducing a hermetian matrix to + # tridiagonal form using householder transforms. + + for dtype in 'fdFD': + larfg, larf = get_lapack_funcs(['larfg', 'larf'], dtype=dtype) + + if dtype in 'FD': + a = a0j.copy() + else: + a = a0.copy() + + # generate a householder transform to clear a[2:,0] + alpha, x, tau = larfg(a.shape[0]-1, a[1, 0], a[2:, 0]) + + # create expected output + expected = np.zeros_like(a[:, 0]) + expected[0] = a[0, 0] + expected[1] = alpha + + # assemble householder vector + v = np.zeros_like(a[1:, 0]) + v[0] = 1.0 + v[1:] = x + + # apply transform from the left + a[1:, :] = larf(v, tau.conjugate(), a[1:, :], np.zeros(a.shape[1])) + + # apply transform from the right + a[:, 1:] = larf(v, tau, a[:, 1:], np.zeros(a.shape[0]), side='R') + + assert_allclose(a[:, 0], expected, atol=1e-5) + assert_allclose(a[0, :], expected, atol=1e-5) + + +def test_sgesdd_lwork_bug_workaround(): + # Test that SGESDD lwork is sufficiently large for LAPACK. + # + # This checks that _compute_lwork() correctly works around a bug in + # LAPACK versions older than 3.10.1. + + sgesdd_lwork = get_lapack_funcs('gesdd_lwork', dtype=np.float32, + ilp64='preferred') + n = 9537 + lwork = _compute_lwork(sgesdd_lwork, n, n, + compute_uv=True, full_matrices=True) + # If we called the Fortran function SGESDD directly with IWORK=-1, the + # LAPACK bug would result in lwork being 272929856, which was too small. + # (The result was returned in a single precision float, which does not + # have sufficient precision to represent the exact integer value that it + # computed internally.) The work-around implemented in _compute_lwork() + # will convert that to 272929888. If we are using LAPACK 3.10.1 or later + # (such as in OpenBLAS 0.3.21 or later), the work-around will return + # 272929920, because it does not know which version of LAPACK is being + # used, so it always applies the correction to whatever it is given. We + # will accept either 272929888 or 272929920. + # Note that the acceptable values are a LAPACK implementation detail. + # If a future version of LAPACK changes how SGESDD works, and therefore + # changes the required LWORK size, the acceptable values might have to + # be updated. + assert lwork == 272929888 or lwork == 272929920 + + +class TestSytrd: + @pytest.mark.parametrize('dtype', REAL_DTYPES) + def test_sytrd_with_zero_dim_array(self, dtype): + # Assert that a 0x0 matrix raises an error + A = np.zeros((0, 0), dtype=dtype) + sytrd = get_lapack_funcs('sytrd', (A,)) + assert_raises(ValueError, sytrd, A) + + @pytest.mark.parametrize('dtype', REAL_DTYPES) + @pytest.mark.parametrize('n', (1, 3)) + def test_sytrd(self, dtype, n): + A = np.zeros((n, n), dtype=dtype) + + sytrd, sytrd_lwork = \ + get_lapack_funcs(('sytrd', 'sytrd_lwork'), (A,)) + + # some upper triangular array + A[np.triu_indices_from(A)] = \ + np.arange(1, n*(n+1)//2+1, dtype=dtype) + + # query lwork + lwork, info = sytrd_lwork(n) + assert_equal(info, 0) + + # check lower=1 behavior (shouldn't do much since the matrix is + # upper triangular) + data, d, e, tau, info = sytrd(A, lower=1, lwork=lwork) + assert_equal(info, 0) + + assert_allclose(data, A, atol=5*np.finfo(dtype).eps, rtol=1.0) + assert_allclose(d, np.diag(A)) + assert_allclose(e, 0.0) + assert_allclose(tau, 0.0) + + # and now for the proper test (lower=0 is the default) + data, d, e, tau, info = sytrd(A, lwork=lwork) + assert_equal(info, 0) + + # assert Q^T*A*Q = tridiag(e, d, e) + + # build tridiagonal matrix + T = np.zeros_like(A, dtype=dtype) + k = np.arange(A.shape[0]) + T[k, k] = d + k2 = np.arange(A.shape[0]-1) + T[k2+1, k2] = e + T[k2, k2+1] = e + + # build Q + Q = np.eye(n, n, dtype=dtype) + for i in range(n-1): + v = np.zeros(n, dtype=dtype) + v[:i] = data[:i, i+1] + v[i] = 1.0 + H = np.eye(n, n, dtype=dtype) - tau[i] * np.outer(v, v) + Q = np.dot(H, Q) + + # Make matrix fully symmetric + i_lower = np.tril_indices(n, -1) + A[i_lower] = A.T[i_lower] + + QTAQ = np.dot(Q.T, np.dot(A, Q)) + + # disable rtol here since some values in QTAQ and T are very close + # to 0. + assert_allclose(QTAQ, T, atol=5*np.finfo(dtype).eps, rtol=1.0) + + +class TestHetrd: + @pytest.mark.parametrize('complex_dtype', COMPLEX_DTYPES) + def test_hetrd_with_zero_dim_array(self, complex_dtype): + # Assert that a 0x0 matrix raises an error + A = np.zeros((0, 0), dtype=complex_dtype) + hetrd = get_lapack_funcs('hetrd', (A,)) + assert_raises(ValueError, hetrd, A) + + @pytest.mark.parametrize('real_dtype,complex_dtype', + zip(REAL_DTYPES, COMPLEX_DTYPES)) + @pytest.mark.parametrize('n', (1, 3)) + def test_hetrd(self, n, real_dtype, complex_dtype): + A = np.zeros((n, n), dtype=complex_dtype) + hetrd, hetrd_lwork = \ + get_lapack_funcs(('hetrd', 'hetrd_lwork'), (A,)) + + # some upper triangular array + A[np.triu_indices_from(A)] = ( + np.arange(1, n*(n+1)//2+1, dtype=real_dtype) + + 1j * np.arange(1, n*(n+1)//2+1, dtype=real_dtype) + ) + np.fill_diagonal(A, np.real(np.diag(A))) + + # test query lwork + for x in [0, 1]: + _, info = hetrd_lwork(n, lower=x) + assert_equal(info, 0) + # lwork returns complex which segfaults hetrd call (gh-10388) + # use the safe and recommended option + lwork = _compute_lwork(hetrd_lwork, n) + + # check lower=1 behavior (shouldn't do much since the matrix is + # upper triangular) + data, d, e, tau, info = hetrd(A, lower=1, lwork=lwork) + assert_equal(info, 0) + + assert_allclose(data, A, atol=5*np.finfo(real_dtype).eps, rtol=1.0) + + assert_allclose(d, np.real(np.diag(A))) + assert_allclose(e, 0.0) + assert_allclose(tau, 0.0) + + # and now for the proper test (lower=0 is the default) + data, d, e, tau, info = hetrd(A, lwork=lwork) + assert_equal(info, 0) + + # assert Q^T*A*Q = tridiag(e, d, e) + + # build tridiagonal matrix + T = np.zeros_like(A, dtype=real_dtype) + k = np.arange(A.shape[0], dtype=int) + T[k, k] = d + k2 = np.arange(A.shape[0]-1, dtype=int) + T[k2+1, k2] = e + T[k2, k2+1] = e + + # build Q + Q = np.eye(n, n, dtype=complex_dtype) + for i in range(n-1): + v = np.zeros(n, dtype=complex_dtype) + v[:i] = data[:i, i+1] + v[i] = 1.0 + H = np.eye(n, n, dtype=complex_dtype) \ + - tau[i] * np.outer(v, np.conj(v)) + Q = np.dot(H, Q) + + # Make matrix fully Hermitian + i_lower = np.tril_indices(n, -1) + A[i_lower] = np.conj(A.T[i_lower]) + + QHAQ = np.dot(np.conj(Q.T), np.dot(A, Q)) + + # disable rtol here since some values in QTAQ and T are very close + # to 0. + assert_allclose( + QHAQ, T, atol=10*np.finfo(real_dtype).eps, rtol=1.0 + ) + + +def test_gglse(): + # Example data taken from NAG manual + for ind, dtype in enumerate(DTYPES): + # DTYPES = gglse + func, func_lwork = get_lapack_funcs(('gglse', 'gglse_lwork'), + dtype=dtype) + lwork = _compute_lwork(func_lwork, m=6, n=4, p=2) + # For gglse + if ind < 2: + a = np.array([[-0.57, -1.28, -0.39, 0.25], + [-1.93, 1.08, -0.31, -2.14], + [2.30, 0.24, 0.40, -0.35], + [-1.93, 0.64, -0.66, 0.08], + [0.15, 0.30, 0.15, -2.13], + [-0.02, 1.03, -1.43, 0.50]], dtype=dtype) + c = np.array([-1.50, -2.14, 1.23, -0.54, -1.68, 0.82], dtype=dtype) + d = np.array([0., 0.], dtype=dtype) + # For gglse + else: + a = np.array([[0.96-0.81j, -0.03+0.96j, -0.91+2.06j, -0.05+0.41j], + [-0.98+1.98j, -1.20+0.19j, -0.66+0.42j, -0.81+0.56j], + [0.62-0.46j, 1.01+0.02j, 0.63-0.17j, -1.11+0.60j], + [0.37+0.38j, 0.19-0.54j, -0.98-0.36j, 0.22-0.20j], + [0.83+0.51j, 0.20+0.01j, -0.17-0.46j, 1.47+1.59j], + [1.08-0.28j, 0.20-0.12j, -0.07+1.23j, 0.26+0.26j]]) + c = np.array([[-2.54+0.09j], + [1.65-2.26j], + [-2.11-3.96j], + [1.82+3.30j], + [-6.41+3.77j], + [2.07+0.66j]]) + d = np.zeros(2, dtype=dtype) + + b = np.array([[1., 0., -1., 0.], [0., 1., 0., -1.]], dtype=dtype) + + _, _, _, result, _ = func(a, b, c, d, lwork=lwork) + if ind < 2: + expected = np.array([0.48904455, + 0.99754786, + 0.48904455, + 0.99754786]) + else: + expected = np.array([1.08742917-1.96205783j, + -0.74093902+3.72973919j, + 1.08742917-1.96205759j, + -0.74093896+3.72973895j]) + assert_array_almost_equal(result, expected, decimal=4) + + +def test_sycon_hecon(): + rng = np.random.default_rng(1234) + for ind, dtype in enumerate(DTYPES+COMPLEX_DTYPES): + # DTYPES + COMPLEX DTYPES = sycon + hecon + n = 10 + # For sycon + if ind < 4: + func_lwork = get_lapack_funcs('sytrf_lwork', dtype=dtype) + funcon, functrf = get_lapack_funcs(('sycon', 'sytrf'), dtype=dtype) + A = (rng.random((n, n))).astype(dtype) + # For hecon + else: + func_lwork = get_lapack_funcs('hetrf_lwork', dtype=dtype) + funcon, functrf = get_lapack_funcs(('hecon', 'hetrf'), dtype=dtype) + A = (rng.random((n, n)) + rng.random((n, n))*1j).astype(dtype) + + # Since sycon only refers to upper/lower part, conj() is safe here. + A = (A + A.conj().T)/2 + 2*np.eye(n, dtype=dtype) + + anorm = norm(A, 1) + lwork = _compute_lwork(func_lwork, n) + ldu, ipiv, _ = functrf(A, lwork=lwork, lower=1) + rcond, _ = funcon(a=ldu, ipiv=ipiv, anorm=anorm, lower=1) + # The error is at most 1-fold + assert_(abs(1/rcond - np.linalg.cond(A, p=1))*rcond < 1) + + +def test_sygst(): + rng = np.random.default_rng(1234) + for ind, dtype in enumerate(REAL_DTYPES): + # DTYPES = sygst + n = 10 + + potrf, sygst, syevd, sygvd = get_lapack_funcs(('potrf', 'sygst', + 'syevd', 'sygvd'), + dtype=dtype) + + A = rng.random((n, n)).astype(dtype) + A = (A + A.T)/2 + # B must be positive definite + B = rng.random((n, n)).astype(dtype) + B = (B + B.T)/2 + 2 * np.eye(n, dtype=dtype) + + # Perform eig (sygvd) + eig_gvd, _, info = sygvd(A, B) + assert_(info == 0) + + # Convert to std problem potrf + b, info = potrf(B) + assert_(info == 0) + a, info = sygst(A, b) + assert_(info == 0) + + eig, _, info = syevd(a) + assert_(info == 0) + assert_allclose(eig, eig_gvd, rtol=1.2e-4) + + +def test_hegst(): + rng = np.random.default_rng(1234) + for ind, dtype in enumerate(COMPLEX_DTYPES): + # DTYPES = hegst + n = 10 + + potrf, hegst, heevd, hegvd = get_lapack_funcs(('potrf', 'hegst', + 'heevd', 'hegvd'), + dtype=dtype) + + A = rng.random((n, n)).astype(dtype) + 1j * rng.random((n, n)).astype(dtype) + A = (A + A.conj().T)/2 + # B must be positive definite + B = rng.random((n, n)).astype(dtype) + 1j * rng.random((n, n)).astype(dtype) + B = (B + B.conj().T)/2 + 2 * np.eye(n, dtype=dtype) + + # Perform eig (hegvd) + eig_gvd, _, info = hegvd(A, B) + assert_(info == 0) + + # Convert to std problem potrf + b, info = potrf(B) + assert_(info == 0) + a, info = hegst(A, b) + assert_(info == 0) + + eig, _, info = heevd(a) + assert_(info == 0) + assert_allclose(eig, eig_gvd, rtol=1e-4) + + +def test_tzrzf(): + """ + This test performs an RZ decomposition in which an m x n upper trapezoidal + array M (m <= n) is factorized as M = [R 0] * Z where R is upper triangular + and Z is unitary. + """ + rng = np.random.RandomState(1234) + m, n = 10, 15 + for ind, dtype in enumerate(DTYPES): + tzrzf, tzrzf_lw = get_lapack_funcs(('tzrzf', 'tzrzf_lwork'), + dtype=dtype) + lwork = _compute_lwork(tzrzf_lw, m, n) + + if ind < 2: + A = triu(rng.rand(m, n).astype(dtype)) + else: + A = triu((rng.rand(m, n) + rng.rand(m, n)*1j).astype(dtype)) + + # assert wrong shape arg, f2py returns generic error + assert_raises(Exception, tzrzf, A.T) + rz, tau, info = tzrzf(A, lwork=lwork) + # Check success + assert_(info == 0) + + # Get Z manually for comparison + R = np.hstack((rz[:, :m], np.zeros((m, n-m), dtype=dtype))) + V = np.hstack((np.eye(m, dtype=dtype), rz[:, m:])) + Id = np.eye(n, dtype=dtype) + ref = [Id-tau[x]*V[[x], :].T.dot(V[[x], :].conj()) for x in range(m)] + Z = reduce(np.dot, ref) + assert_allclose(R.dot(Z) - A, zeros_like(A, dtype=dtype), + atol=10*np.spacing(dtype(1.0).real), rtol=0.) + + +def test_tfsm(): + """ + Test for solving a linear system with the coefficient matrix is a + triangular array stored in Full Packed (RFP) format. + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A = triu(rng.rand(n, n) + rng.rand(n, n)*1j + eye(n)).astype(dtype) + trans = 'C' + else: + A = triu(rng.rand(n, n) + eye(n)).astype(dtype) + trans = 'T' + + trttf, tfttr, tfsm = get_lapack_funcs(('trttf', 'tfttr', 'tfsm'), + dtype=dtype) + + Afp, _ = trttf(A) + B = rng.rand(n, 2).astype(dtype) + soln = tfsm(-1, Afp, B) + assert_array_almost_equal(soln, solve(-A, B), + decimal=4 if ind % 2 == 0 else 6) + + soln = tfsm(-1, Afp, B, trans=trans) + assert_array_almost_equal(soln, solve(-A.conj().T, B), + decimal=4 if ind % 2 == 0 else 6) + + # Make A, unit diagonal + A[np.arange(n), np.arange(n)] = dtype(1.) + soln = tfsm(-1, Afp, B, trans=trans, diag='U') + assert_array_almost_equal(soln, solve(-A.conj().T, B), + decimal=4 if ind % 2 == 0 else 6) + + # Change side + B2 = rng.rand(3, n).astype(dtype) + soln = tfsm(-1, Afp, B2, trans=trans, diag='U', side='R') + assert_array_almost_equal(soln, solve(-A, B2.T).conj().T, + decimal=4 if ind % 2 == 0 else 6) + + +def test_ormrz_unmrz(): + """ + This test performs a matrix multiplication with an arbitrary m x n matrix C + and a unitary matrix Q without explicitly forming the array. The array data + is encoded in the rectangular part of A which is obtained from ?TZRZF. Q + size is inferred by m, n, side keywords. + """ + rng = np.random.RandomState(1234) + qm, qn, cn = 10, 15, 15 + for ind, dtype in enumerate(DTYPES): + tzrzf, tzrzf_lw = get_lapack_funcs(('tzrzf', 'tzrzf_lwork'), + dtype=dtype) + lwork_rz = _compute_lwork(tzrzf_lw, qm, qn) + + if ind < 2: + A = triu(rng.random((qm, qn)).astype(dtype)) + C = rng.random((cn, cn)).astype(dtype) + orun_mrz, orun_mrz_lw = get_lapack_funcs(('ormrz', 'ormrz_lwork'), + dtype=dtype) + else: + A = triu((rng.random((qm, qn)) + rng.random((qm, qn))*1j).astype(dtype)) + C = (rng.random((cn, cn)) + rng.random((cn, cn))*1j).astype(dtype) + orun_mrz, orun_mrz_lw = get_lapack_funcs(('unmrz', 'unmrz_lwork'), + dtype=dtype) + + lwork_mrz = _compute_lwork(orun_mrz_lw, cn, cn) + rz, tau, info = tzrzf(A, lwork=lwork_rz) + + # Get Q manually for comparison + V = np.hstack((np.eye(qm, dtype=dtype), rz[:, qm:])) + Id = np.eye(qn, dtype=dtype) + ref = [Id-tau[x]*V[[x], :].T.dot(V[[x], :].conj()) for x in range(qm)] + Q = reduce(np.dot, ref) + + # Now that we have Q, we can test whether lapack results agree with + # each case of CQ, CQ^H, QC, and QC^H + trans = 'T' if ind < 2 else 'C' + tol = 10*np.spacing(dtype(1.0).real) + + cq, info = orun_mrz(rz, tau, C, lwork=lwork_mrz) + assert_(info == 0) + assert_allclose(cq - Q.dot(C), zeros_like(C), atol=tol, rtol=0.) + + cq, info = orun_mrz(rz, tau, C, trans=trans, lwork=lwork_mrz) + assert_(info == 0) + assert_allclose(cq - Q.conj().T.dot(C), zeros_like(C), atol=tol, + rtol=0.) + + cq, info = orun_mrz(rz, tau, C, side='R', lwork=lwork_mrz) + assert_(info == 0) + assert_allclose(cq - C.dot(Q), zeros_like(C), atol=tol, rtol=0.) + + cq, info = orun_mrz(rz, tau, C, side='R', trans=trans, lwork=lwork_mrz) + assert_(info == 0) + assert_allclose(cq - C.dot(Q.conj().T), zeros_like(C), atol=tol, + rtol=0.) + + +def test_tfttr_trttf(): + """ + Test conversion routines between the Rectangular Full Packed (RFP) format + and Standard Triangular Array (TR) + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A_full = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + transr = 'C' + else: + A_full = (rng.rand(n, n)).astype(dtype) + transr = 'T' + + trttf, tfttr = get_lapack_funcs(('trttf', 'tfttr'), dtype=dtype) + A_tf_U, info = trttf(A_full) + assert_(info == 0) + A_tf_L, info = trttf(A_full, uplo='L') + assert_(info == 0) + A_tf_U_T, info = trttf(A_full, transr=transr, uplo='U') + assert_(info == 0) + A_tf_L_T, info = trttf(A_full, transr=transr, uplo='L') + assert_(info == 0) + + # Create the RFP array manually (n is even!) + A_tf_U_m = zeros((n+1, n//2), dtype=dtype) + A_tf_U_m[:-1, :] = triu(A_full)[:, n//2:] + A_tf_U_m[n//2+1:, :] += triu(A_full)[:n//2, :n//2].conj().T + + A_tf_L_m = zeros((n+1, n//2), dtype=dtype) + A_tf_L_m[1:, :] = tril(A_full)[:, :n//2] + A_tf_L_m[:n//2, :] += tril(A_full)[n//2:, n//2:].conj().T + + assert_array_almost_equal(A_tf_U, A_tf_U_m.reshape(-1, order='F')) + assert_array_almost_equal(A_tf_U_T, + A_tf_U_m.conj().T.reshape(-1, order='F')) + + assert_array_almost_equal(A_tf_L, A_tf_L_m.reshape(-1, order='F')) + assert_array_almost_equal(A_tf_L_T, + A_tf_L_m.conj().T.reshape(-1, order='F')) + + # Get the original array from RFP + A_tr_U, info = tfttr(n, A_tf_U) + assert_(info == 0) + A_tr_L, info = tfttr(n, A_tf_L, uplo='L') + assert_(info == 0) + A_tr_U_T, info = tfttr(n, A_tf_U_T, transr=transr, uplo='U') + assert_(info == 0) + A_tr_L_T, info = tfttr(n, A_tf_L_T, transr=transr, uplo='L') + assert_(info == 0) + + assert_array_almost_equal(A_tr_U, triu(A_full)) + assert_array_almost_equal(A_tr_U_T, triu(A_full)) + assert_array_almost_equal(A_tr_L, tril(A_full)) + assert_array_almost_equal(A_tr_L_T, tril(A_full)) + + +def test_tpttr_trttp(): + """ + Test conversion routines between the Rectangular Full Packed (RFP) format + and Standard Triangular Array (TR) + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A_full = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + else: + A_full = (rng.rand(n, n)).astype(dtype) + + trttp, tpttr = get_lapack_funcs(('trttp', 'tpttr'), dtype=dtype) + A_tp_U, info = trttp(A_full) + assert_(info == 0) + A_tp_L, info = trttp(A_full, uplo='L') + assert_(info == 0) + + # Create the TP array manually + inds = tril_indices(n) + A_tp_U_m = zeros(n*(n+1)//2, dtype=dtype) + A_tp_U_m[:] = (triu(A_full).T)[inds] + + inds = triu_indices(n) + A_tp_L_m = zeros(n*(n+1)//2, dtype=dtype) + A_tp_L_m[:] = (tril(A_full).T)[inds] + + assert_array_almost_equal(A_tp_U, A_tp_U_m) + assert_array_almost_equal(A_tp_L, A_tp_L_m) + + # Get the original array from TP + A_tr_U, info = tpttr(n, A_tp_U) + assert_(info == 0) + A_tr_L, info = tpttr(n, A_tp_L, uplo='L') + assert_(info == 0) + + assert_array_almost_equal(A_tr_U, triu(A_full)) + assert_array_almost_equal(A_tr_L, tril(A_full)) + + +def test_pftrf(): + """ + Test Cholesky factorization of a positive definite Rectangular Full + Packed (RFP) format array + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + A = A + A.conj().T + n*eye(n) + else: + A = (rng.rand(n, n)).astype(dtype) + A = A + A.T + n*eye(n) + + pftrf, trttf, tfttr = get_lapack_funcs(('pftrf', 'trttf', 'tfttr'), + dtype=dtype) + + # Get the original array from TP + Afp, info = trttf(A) + Achol_rfp, info = pftrf(n, Afp) + assert_(info == 0) + A_chol_r, _ = tfttr(n, Achol_rfp) + Achol = cholesky(A) + assert_array_almost_equal(A_chol_r, Achol) + + +def test_pftri(): + """ + Test Cholesky factorization of a positive definite Rectangular Full + Packed (RFP) format array to find its inverse + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + A = A + A.conj().T + n*eye(n) + else: + A = (rng.rand(n, n)).astype(dtype) + A = A + A.T + n*eye(n) + + pftri, pftrf, trttf, tfttr = get_lapack_funcs(('pftri', + 'pftrf', + 'trttf', + 'tfttr'), + dtype=dtype) + + # Get the original array from TP + Afp, info = trttf(A) + A_chol_rfp, info = pftrf(n, Afp) + A_inv_rfp, info = pftri(n, A_chol_rfp) + assert_(info == 0) + A_inv_r, _ = tfttr(n, A_inv_rfp) + Ainv = inv(A) + assert_array_almost_equal(A_inv_r, triu(Ainv), + decimal=4 if ind % 2 == 0 else 6) + + +def test_pftrs(): + """ + Test Cholesky factorization of a positive definite Rectangular Full + Packed (RFP) format array and solve a linear system + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + A = A + A.conj().T + n*eye(n) + else: + A = (rng.rand(n, n)).astype(dtype) + A = A + A.T + n*eye(n) + + B = ones((n, 3), dtype=dtype) + Bf1 = ones((n+2, 3), dtype=dtype) + Bf2 = ones((n-2, 3), dtype=dtype) + pftrs, pftrf, trttf, tfttr = get_lapack_funcs(('pftrs', + 'pftrf', + 'trttf', + 'tfttr'), + dtype=dtype) + + # Get the original array from TP + Afp, info = trttf(A) + A_chol_rfp, info = pftrf(n, Afp) + # larger B arrays shouldn't segfault + soln, info = pftrs(n, A_chol_rfp, Bf1) + assert_(info == 0) + assert_raises(Exception, pftrs, n, A_chol_rfp, Bf2) + soln, info = pftrs(n, A_chol_rfp, B) + assert_(info == 0) + assert_array_almost_equal(solve(A, B), soln, + decimal=4 if ind % 2 == 0 else 6) + + +def test_sfrk_hfrk(): + """ + Test for performing a symmetric rank-k operation for matrix in RFP format. + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + A = A + A.conj().T + n*eye(n) + else: + A = (rng.rand(n, n)).astype(dtype) + A = A + A.T + n*eye(n) + + prefix = 's'if ind < 2 else 'h' + trttf, tfttr, shfrk = get_lapack_funcs(('trttf', 'tfttr', f'{prefix}frk'), + dtype=dtype) + + Afp, _ = trttf(A) + C = rng.rand(n, 2).astype(dtype) + Afp_out = shfrk(n, 2, -1, C, 2, Afp) + A_out, _ = tfttr(n, Afp_out) + assert_array_almost_equal(A_out, triu(-C.dot(C.conj().T) + 2*A), + decimal=4 if ind % 2 == 0 else 6) + + +def test_syconv(): + """ + Test for going back and forth between the returned format of he/sytrf to + L and D factors/permutations. + """ + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 10 + + if ind > 1: + A = (rng.randint(-30, 30, (n, n)) + + rng.randint(-30, 30, (n, n))*1j).astype(dtype) + + A = A + A.conj().T + else: + A = rng.randint(-30, 30, (n, n)).astype(dtype) + A = A + A.T + n*eye(n) + + tol = 100*np.spacing(dtype(1.0).real) + syconv, trf, trf_lwork = get_lapack_funcs(('syconv', 'sytrf', + 'sytrf_lwork'), dtype=dtype) + lw = _compute_lwork(trf_lwork, n, lower=1) + L, D, perm = ldl(A, lower=1, hermitian=False) + lw = _compute_lwork(trf_lwork, n, lower=1) + ldu, ipiv, info = trf(A, lower=1, lwork=lw) + a, e, info = syconv(ldu, ipiv, lower=1) + assert_allclose(tril(a, -1,), tril(L[perm, :], -1), atol=tol, rtol=0.) + + # Test also upper + U, D, perm = ldl(A, lower=0, hermitian=False) + ldu, ipiv, info = trf(A, lower=0) + a, e, info = syconv(ldu, ipiv, lower=0) + assert_allclose(triu(a, 1), triu(U[perm, :], 1), atol=tol, rtol=0.) + + +class TestBlockedQR: + """ + Tests for the blocked QR factorization, namely through geqrt, gemqrt, tpqrt + and tpmqr. + """ + + def test_geqrt_gemqrt(self): + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + else: + A = (rng.rand(n, n)).astype(dtype) + + tol = 100*np.spacing(dtype(1.0).real) + geqrt, gemqrt = get_lapack_funcs(('geqrt', 'gemqrt'), dtype=dtype) + + a, t, info = geqrt(n, A) + assert info == 0 + + # Extract elementary reflectors from lower triangle, adding the + # main diagonal of ones. + v = np.tril(a, -1) + np.eye(n, dtype=dtype) + # Generate the block Householder transform I - VTV^H + Q = np.eye(n, dtype=dtype) - v @ t @ v.T.conj() + R = np.triu(a) + + # Test columns of Q are orthogonal + assert_allclose(Q.T.conj() @ Q, np.eye(n, dtype=dtype), atol=tol, + rtol=0.) + assert_allclose(Q @ R, A, atol=tol, rtol=0.) + + if ind > 1: + C = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + transpose = 'C' + else: + C = (rng.rand(n, n)).astype(dtype) + transpose = 'T' + + for side in ('L', 'R'): + for trans in ('N', transpose): + c, info = gemqrt(a, t, C, side=side, trans=trans) + assert info == 0 + + if trans == transpose: + q = Q.T.conj() + else: + q = Q + + if side == 'L': + qC = q @ C + else: + qC = C @ q + + assert_allclose(c, qC, atol=tol, rtol=0.) + + # Test default arguments + if (side, trans) == ('L', 'N'): + c_default, info = gemqrt(a, t, C) + assert info == 0 + assert_equal(c_default, c) + + # Test invalid side/trans + assert_raises(Exception, gemqrt, a, t, C, side='A') + assert_raises(Exception, gemqrt, a, t, C, trans='A') + + def test_tpqrt_tpmqrt(self): + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + n = 20 + + if ind > 1: + A = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + B = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + else: + A = (rng.rand(n, n)).astype(dtype) + B = (rng.rand(n, n)).astype(dtype) + + tol = 100*np.spacing(dtype(1.0).real) + tpqrt, tpmqrt = get_lapack_funcs(('tpqrt', 'tpmqrt'), dtype=dtype) + + # Test for the range of pentagonal B, from square to upper + # triangular + for l in (0, n // 2, n): + a, b, t, info = tpqrt(l, n, A, B) + assert info == 0 + + # Check that lower triangular part of A has not been modified + assert_equal(np.tril(a, -1), np.tril(A, -1)) + # Check that elements not part of the pentagonal portion of B + # have not been modified. + assert_equal(np.tril(b, l - n - 1), np.tril(B, l - n - 1)) + + # Extract pentagonal portion of B + B_pent, b_pent = np.triu(B, l - n), np.triu(b, l - n) + + # Generate elementary reflectors + v = np.concatenate((np.eye(n, dtype=dtype), b_pent)) + # Generate the block Householder transform I - VTV^H + Q = np.eye(2 * n, dtype=dtype) - v @ t @ v.T.conj() + R = np.concatenate((np.triu(a), np.zeros_like(a))) + + # Test columns of Q are orthogonal + assert_allclose(Q.T.conj() @ Q, np.eye(2 * n, dtype=dtype), + atol=tol, rtol=0.) + assert_allclose(Q @ R, np.concatenate((np.triu(A), B_pent)), + atol=tol, rtol=0.) + + if ind > 1: + C = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + D = (rng.rand(n, n) + rng.rand(n, n)*1j).astype(dtype) + transpose = 'C' + else: + C = (rng.rand(n, n)).astype(dtype) + D = (rng.rand(n, n)).astype(dtype) + transpose = 'T' + + for side in ('L', 'R'): + for trans in ('N', transpose): + c, d, info = tpmqrt(l, b, t, C, D, side=side, + trans=trans) + assert info == 0 + + if trans == transpose: + q = Q.T.conj() + else: + q = Q + + if side == 'L': + cd = np.concatenate((c, d), axis=0) + CD = np.concatenate((C, D), axis=0) + qCD = q @ CD + else: + cd = np.concatenate((c, d), axis=1) + CD = np.concatenate((C, D), axis=1) + qCD = CD @ q + + assert_allclose(cd, qCD, atol=tol, rtol=0.) + + if (side, trans) == ('L', 'N'): + c_default, d_default, info = tpmqrt(l, b, t, C, D) + assert info == 0 + assert_equal(c_default, c) + assert_equal(d_default, d) + + # Test invalid side/trans + assert_raises(Exception, tpmqrt, l, b, t, C, D, side='A') + assert_raises(Exception, tpmqrt, l, b, t, C, D, trans='A') + + +def test_pstrf(): + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + # DTYPES = pstrf + n = 10 + r = 2 + pstrf = get_lapack_funcs('pstrf', dtype=dtype) + + # Create positive semidefinite A + if ind > 1: + A = rng.rand(n, n-r).astype(dtype) + 1j * rng.rand(n, n-r).astype(dtype) + A = A @ A.conj().T + else: + A = rng.rand(n, n-r).astype(dtype) + A = A @ A.T + + c, piv, r_c, info = pstrf(A) + U = triu(c) + U[r_c - n:, r_c - n:] = 0. + + assert_equal(info, 1) + # python-dbg 3.5.2 runs cause trouble with the following assertion. + # assert_equal(r_c, n - r) + single_atol = 1000 * np.finfo(np.float32).eps + double_atol = 1000 * np.finfo(np.float64).eps + atol = single_atol if ind in [0, 2] else double_atol + assert_allclose(A[piv-1][:, piv-1], U.conj().T @ U, rtol=0., atol=atol) + + c, piv, r_c, info = pstrf(A, lower=1) + L = tril(c) + L[r_c - n:, r_c - n:] = 0. + + assert_equal(info, 1) + # assert_equal(r_c, n - r) + single_atol = 1000 * np.finfo(np.float32).eps + double_atol = 1000 * np.finfo(np.float64).eps + atol = single_atol if ind in [0, 2] else double_atol + assert_allclose(A[piv-1][:, piv-1], L @ L.conj().T, rtol=0., atol=atol) + + +def test_pstf2(): + rng = np.random.RandomState(1234) + for ind, dtype in enumerate(DTYPES): + # DTYPES = pstf2 + n = 10 + r = 2 + pstf2 = get_lapack_funcs('pstf2', dtype=dtype) + + # Create positive semidefinite A + if ind > 1: + A = rng.rand(n, n-r).astype(dtype) + 1j * rng.rand(n, n-r).astype(dtype) + A = A @ A.conj().T + else: + A = rng.rand(n, n-r).astype(dtype) + A = A @ A.T + + c, piv, r_c, info = pstf2(A) + U = triu(c) + U[r_c - n:, r_c - n:] = 0. + + assert_equal(info, 1) + # python-dbg 3.5.2 runs cause trouble with the commented assertions. + # assert_equal(r_c, n - r) + single_atol = 1000 * np.finfo(np.float32).eps + double_atol = 1000 * np.finfo(np.float64).eps + atol = single_atol if ind in [0, 2] else double_atol + assert_allclose(A[piv-1][:, piv-1], U.conj().T @ U, rtol=0., atol=atol) + + c, piv, r_c, info = pstf2(A, lower=1) + L = tril(c) + L[r_c - n:, r_c - n:] = 0. + + assert_equal(info, 1) + # assert_equal(r_c, n - r) + single_atol = 1000 * np.finfo(np.float32).eps + double_atol = 1000 * np.finfo(np.float64).eps + atol = single_atol if ind in [0, 2] else double_atol + assert_allclose(A[piv-1][:, piv-1], L @ L.conj().T, rtol=0., atol=atol) + + +def test_geequ(): + desired_real = np.array([[0.6250, 1.0000, 0.0393, -0.4269], + [1.0000, -0.5619, -1.0000, -1.0000], + [0.5874, -1.0000, -0.0596, -0.5341], + [-1.0000, -0.5946, -0.0294, 0.9957]]) + + desired_cplx = np.array([[-0.2816+0.5359*1j, + 0.0812+0.9188*1j, + -0.7439-0.2561*1j], + [-0.3562-0.2954*1j, + 0.9566-0.0434*1j, + -0.0174+0.1555*1j], + [0.8607+0.1393*1j, + -0.2759+0.7241*1j, + -0.1642-0.1365*1j]]) + + for ind, dtype in enumerate(DTYPES): + if ind < 2: + # Use examples from the NAG documentation + A = np.array([[1.80e+10, 2.88e+10, 2.05e+00, -8.90e+09], + [5.25e+00, -2.95e+00, -9.50e-09, -3.80e+00], + [1.58e+00, -2.69e+00, -2.90e-10, -1.04e+00], + [-1.11e+00, -6.60e-01, -5.90e-11, 8.00e-01]]) + A = A.astype(dtype) + else: + A = np.array([[-1.34e+00, 0.28e+10, -6.39e+00], + [-1.70e+00, 3.31e+10, -0.15e+00], + [2.41e-10, -0.56e+00, -0.83e-10]], dtype=dtype) + A += np.array([[2.55e+00, 3.17e+10, -2.20e+00], + [-1.41e+00, -0.15e+10, 1.34e+00], + [0.39e-10, 1.47e+00, -0.69e-10]])*1j + + A = A.astype(dtype) + + geequ = get_lapack_funcs('geequ', dtype=dtype) + r, c, rowcnd, colcnd, amax, info = geequ(A) + + if ind < 2: + assert_allclose(desired_real.astype(dtype), r[:, None]*A*c, + rtol=0, atol=1e-4) + else: + assert_allclose(desired_cplx.astype(dtype), r[:, None]*A*c, + rtol=0, atol=1e-4) + + +def test_syequb(): + desired_log2s = np.array([0, 0, 0, 0, 0, 0, -1, -1, -2, -3]) + + for ind, dtype in enumerate(DTYPES): + A = np.eye(10, dtype=dtype) + alpha = dtype(1. if ind < 2 else 1.j) + d = np.array([alpha * 2.**x for x in range(-5, 5)], dtype=dtype) + A += np.rot90(np.diag(d)) + + syequb = get_lapack_funcs('syequb', dtype=dtype) + s, scond, amax, info = syequb(A) + + assert_equal(np.log2(s).astype(int), desired_log2s) + + +@pytest.mark.skipif(True, + reason="Failing on some OpenBLAS version, see gh-12276") +def test_heequb(): + # zheequb has a bug for versions =< LAPACK 3.9.0 + # See Reference-LAPACK gh-61 and gh-408 + # Hence the zheequb test is customized accordingly to avoid + # work scaling. + A = np.diag([2]*5 + [1002]*5) + np.diag(np.ones(9), k=1)*1j + s, scond, amax, info = lapack.zheequb(A) + assert_equal(info, 0) + assert_allclose(np.log2(s), [0., -1.]*2 + [0.] + [-4]*5) + + A = np.diag(2**np.abs(np.arange(-5, 6)) + 0j) + A[5, 5] = 1024 + A[5, 0] = 16j + s, scond, amax, info = lapack.cheequb(A.astype(np.complex64), lower=1) + assert_equal(info, 0) + assert_allclose(np.log2(s), [-2, -1, -1, 0, 0, -5, 0, -1, -1, -2, -2]) + + +def test_getc2_gesc2(): + rng = np.random.RandomState(42) + n = 10 + desired_real = rng.rand(n) + desired_cplx = rng.rand(n) + rng.rand(n)*1j + + for ind, dtype in enumerate(DTYPES): + if ind < 2: + A = rng.rand(n, n) + A = A.astype(dtype) + b = A @ desired_real + b = b.astype(dtype) + else: + A = rng.rand(n, n) + rng.rand(n, n)*1j + A = A.astype(dtype) + b = A @ desired_cplx + b = b.astype(dtype) + + getc2 = get_lapack_funcs('getc2', dtype=dtype) + gesc2 = get_lapack_funcs('gesc2', dtype=dtype) + lu, ipiv, jpiv, info = getc2(A, overwrite_a=0) + x, scale = gesc2(lu, b, ipiv, jpiv, overwrite_rhs=0) + + if ind < 2: + assert_array_almost_equal(desired_real.astype(dtype), + x/scale, decimal=4) + else: + assert_array_almost_equal(desired_cplx.astype(dtype), + x/scale, decimal=4) + + +@pytest.mark.parametrize('size', [(6, 5), (5, 5)]) +@pytest.mark.parametrize('dtype', REAL_DTYPES) +@pytest.mark.parametrize('joba', range(6)) # 'C', 'E', 'F', 'G', 'A', 'R' +@pytest.mark.parametrize('jobu', range(4)) # 'U', 'F', 'W', 'N' +@pytest.mark.parametrize('jobv', range(4)) # 'V', 'J', 'W', 'N' +@pytest.mark.parametrize('jobr', [0, 1]) +@pytest.mark.parametrize('jobp', [0, 1]) +def test_gejsv_general(size, dtype, joba, jobu, jobv, jobr, jobp, jobt=0): + """Test the lapack routine ?gejsv. + + This function tests that a singular value decomposition can be performed + on the random M-by-N matrix A. The test performs the SVD using ?gejsv + then performs the following checks: + + * ?gejsv exist successfully (info == 0) + * The returned singular values are correct + * `A` can be reconstructed from `u`, `SIGMA`, `v` + * Ensure that u.T @ u is the identity matrix + * Ensure that v.T @ v is the identity matrix + * The reported matrix rank + * The reported number of singular values + * If denormalized floats are required + + Notes + ----- + joba specifies several choices effecting the calculation's accuracy + Although all arguments are tested, the tests only check that the correct + solution is returned - NOT that the prescribed actions are performed + internally. + + jobt is, as of v3.9.0, still experimental and removed to cut down number of + test cases. However keyword itself is tested externally. + """ + rng = np.random.RandomState(42) + + # Define some constants for later use: + m, n = size + atol = 100 * np.finfo(dtype).eps + A = generate_random_dtype_array(size, dtype, rng) + gejsv = get_lapack_funcs('gejsv', dtype=dtype) + + # Set up checks for invalid job? combinations + # if an invalid combination occurs we set the appropriate + # exit status. + lsvec = jobu < 2 # Calculate left singular vectors + rsvec = jobv < 2 # Calculate right singular vectors + l2tran = (jobt == 1) and (m == n) + is_complex = np.iscomplexobj(A) + + invalid_real_jobv = (jobv == 1) and (not lsvec) and (not is_complex) + invalid_cplx_jobu = (jobu == 2) and not (rsvec and l2tran) and is_complex + invalid_cplx_jobv = (jobv == 2) and not (lsvec and l2tran) and is_complex + + # Set the exit status to the expected value. + # Here we only check for invalid combinations, not individual + # parameters. + if invalid_cplx_jobu: + exit_status = -2 + elif invalid_real_jobv or invalid_cplx_jobv: + exit_status = -3 + else: + exit_status = 0 + + if (jobu > 1) and (jobv == 1): + assert_raises(Exception, gejsv, A, joba, jobu, jobv, jobr, jobt, jobp) + else: + sva, u, v, work, iwork, info = gejsv(A, + joba=joba, + jobu=jobu, + jobv=jobv, + jobr=jobr, + jobt=jobt, + jobp=jobp) + + # Check that ?gejsv exited successfully/as expected + assert_equal(info, exit_status) + + # If exit_status is non-zero the combination of jobs is invalid. + # We test this above but no calculations are performed. + if not exit_status: + + # Check the returned singular values + sigma = (work[0] / work[1]) * sva[:n] + assert_allclose(sigma, svd(A, compute_uv=False), atol=atol) + + if jobu == 1: + # If JOBU = 'F', then u contains the M-by-M matrix of + # the left singular vectors, including an ONB of the orthogonal + # complement of the Range(A) + # However, to recalculate A we are concerned about the + # first n singular values and so can ignore the latter. + # TODO: Add a test for ONB? + u = u[:, :n] + + if lsvec and rsvec: + assert_allclose(u @ np.diag(sigma) @ v.conj().T, A, atol=atol) + if lsvec: + assert_allclose(u.conj().T @ u, np.identity(n), atol=atol) + if rsvec: + assert_allclose(v.conj().T @ v, np.identity(n), atol=atol) + + assert_equal(iwork[0], np.linalg.matrix_rank(A)) + assert_equal(iwork[1], np.count_nonzero(sigma)) + # iwork[2] is non-zero if requested accuracy is not warranted for + # the data. This should never occur for these tests. + assert_equal(iwork[2], 0) + + +@pytest.mark.parametrize('dtype', REAL_DTYPES) +def test_gejsv_edge_arguments(dtype): + """Test edge arguments return expected status""" + gejsv = get_lapack_funcs('gejsv', dtype=dtype) + + # scalar A + sva, u, v, work, iwork, info = gejsv(1.) + assert_equal(info, 0) + assert_equal(u.shape, (1, 1)) + assert_equal(v.shape, (1, 1)) + assert_equal(sva, np.array([1.], dtype=dtype)) + + # 1d A + A = np.ones((1,), dtype=dtype) + sva, u, v, work, iwork, info = gejsv(A) + assert_equal(info, 0) + assert_equal(u.shape, (1, 1)) + assert_equal(v.shape, (1, 1)) + assert_equal(sva, np.array([1.], dtype=dtype)) + + # 2d empty A + A = np.ones((1, 0), dtype=dtype) + sva, u, v, work, iwork, info = gejsv(A) + assert_equal(info, 0) + assert_equal(u.shape, (1, 0)) + assert_equal(v.shape, (1, 0)) + assert_equal(sva, np.array([], dtype=dtype)) + + # make sure "overwrite_a" is respected - user reported in gh-13191 + A = np.sin(np.arange(100).reshape(10, 10)).astype(dtype) + A = np.asfortranarray(A + A.T) # make it symmetric and column major + Ac = A.copy('A') + _ = gejsv(A) + assert_allclose(A, Ac) + + +@pytest.mark.parametrize(('kwargs'), + ({'joba': 9}, + {'jobu': 9}, + {'jobv': 9}, + {'jobr': 9}, + {'jobt': 9}, + {'jobp': 9}) + ) +def test_gejsv_invalid_job_arguments(kwargs): + """Test invalid job arguments raise an Exception""" + A = np.ones((2, 2), dtype=float) + gejsv = get_lapack_funcs('gejsv', dtype=float) + assert_raises(Exception, gejsv, A, **kwargs) + + +@pytest.mark.parametrize("A,sva_expect,u_expect,v_expect", + [(np.array([[2.27, -1.54, 1.15, -1.94], + [0.28, -1.67, 0.94, -0.78], + [-0.48, -3.09, 0.99, -0.21], + [1.07, 1.22, 0.79, 0.63], + [-2.35, 2.93, -1.45, 2.30], + [0.62, -7.39, 1.03, -2.57]]), + np.array([9.9966, 3.6831, 1.3569, 0.5000]), + np.array([[0.2774, -0.6003, -0.1277, 0.1323], + [0.2020, -0.0301, 0.2805, 0.7034], + [0.2918, 0.3348, 0.6453, 0.1906], + [-0.0938, -0.3699, 0.6781, -0.5399], + [-0.4213, 0.5266, 0.0413, -0.0575], + [0.7816, 0.3353, -0.1645, -0.3957]]), + np.array([[0.1921, -0.8030, 0.0041, -0.5642], + [-0.8794, -0.3926, -0.0752, 0.2587], + [0.2140, -0.2980, 0.7827, 0.5027], + [-0.3795, 0.3351, 0.6178, -0.6017]]))]) +def test_gejsv_NAG(A, sva_expect, u_expect, v_expect): + """ + This test implements the example found in the NAG manual, f08khf. + An example was not found for the complex case. + """ + # NAG manual provides accuracy up to 4 decimals + atol = 1e-4 + gejsv = get_lapack_funcs('gejsv', dtype=A.dtype) + + sva, u, v, work, iwork, info = gejsv(A) + + assert_allclose(sva_expect, sva, atol=atol) + assert_allclose(u_expect, u, atol=atol) + assert_allclose(v_expect, v, atol=atol) + + +@pytest.mark.parametrize("dtype", DTYPES) +def test_gttrf_gttrs(dtype): + # The test uses ?gttrf and ?gttrs to solve a random system for each dtype, + # tests that the output of ?gttrf define LU matrices, that input + # parameters are unmodified, transposal options function correctly, that + # incompatible matrix shapes raise an error, and singular matrices return + # non zero info. + + rng = np.random.RandomState(42) + n = 10 + atol = 100 * np.finfo(dtype).eps + + # create the matrix in accordance with the data type + du = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + d = generate_random_dtype_array((n,), dtype=dtype, rng=rng) + dl = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + + diag_cpy = [dl.copy(), d.copy(), du.copy()] + + A = np.diag(d) + np.diag(dl, -1) + np.diag(du, 1) + x = rng.random(n) + b = A @ x + + gttrf, gttrs = get_lapack_funcs(('gttrf', 'gttrs'), dtype=dtype) + + _dl, _d, _du, du2, ipiv, info = gttrf(dl, d, du) + # test to assure that the inputs of ?gttrf are unmodified + assert_array_equal(dl, diag_cpy[0]) + assert_array_equal(d, diag_cpy[1]) + assert_array_equal(du, diag_cpy[2]) + + # generate L and U factors from ?gttrf return values + # L/U are lower/upper triangular by construction (initially and at end) + U = np.diag(_d, 0) + np.diag(_du, 1) + np.diag(du2, 2) + L = np.eye(n, dtype=dtype) + + for i, m in enumerate(_dl): + # L is given in a factored form. + # See + # www.hpcavf.uclan.ac.uk/softwaredoc/sgi_scsl_html/sgi_html/ch03.html + piv = ipiv[i] - 1 + # right multiply by permutation matrix + L[:, [i, piv]] = L[:, [piv, i]] + # right multiply by Li, rank-one modification of identity + L[:, i] += L[:, i+1]*m + + # one last permutation + i, piv = -1, ipiv[-1] - 1 + # right multiply by final permutation matrix + L[:, [i, piv]] = L[:, [piv, i]] + + # check that the outputs of ?gttrf define an LU decomposition of A + assert_allclose(A, L @ U, atol=atol) + + b_cpy = b.copy() + x_gttrs, info = gttrs(_dl, _d, _du, du2, ipiv, b) + # test that the inputs of ?gttrs are unmodified + assert_array_equal(b, b_cpy) + # test that the result of ?gttrs matches the expected input + assert_allclose(x, x_gttrs, atol=atol) + + # test that ?gttrf and ?gttrs work with transposal options + if dtype in REAL_DTYPES: + trans = "T" + b_trans = A.T @ x + else: + trans = "C" + b_trans = A.conj().T @ x + + x_gttrs, info = gttrs(_dl, _d, _du, du2, ipiv, b_trans, trans=trans) + assert_allclose(x, x_gttrs, atol=atol) + + # test that ValueError is raised with incompatible matrix shapes + with assert_raises(ValueError): + gttrf(dl[:-1], d, du) + with assert_raises(ValueError): + gttrf(dl, d[:-1], du) + with assert_raises(ValueError): + gttrf(dl, d, du[:-1]) + + # test that matrix of size n=2 raises exception + with assert_raises(ValueError): + gttrf(dl[0], d[:1], du[0]) + + # test that singular (row of all zeroes) matrix fails via info + du[0] = 0 + d[0] = 0 + __dl, __d, __du, _du2, _ipiv, _info = gttrf(dl, d, du) + np.testing.assert_(__d[info - 1] == 0, (f"?gttrf: _d[info-1] is {__d[info - 1]}," + " not the illegal value :0.")) + + +@pytest.mark.parametrize("du, d, dl, du_exp, d_exp, du2_exp, ipiv_exp, b, x", + [(np.array([2.1, -1.0, 1.9, 8.0]), + np.array([3.0, 2.3, -5.0, -.9, 7.1]), + np.array([3.4, 3.6, 7.0, -6.0]), + np.array([2.3, -5, -.9, 7.1]), + np.array([3.4, 3.6, 7, -6, -1.015373]), + np.array([-1, 1.9, 8]), + np.array([2, 3, 4, 5, 5]), + np.array([[2.7, 6.6], + [-0.5, 10.8], + [2.6, -3.2], + [0.6, -11.2], + [2.7, 19.1] + ]), + np.array([[-4, 5], + [7, -4], + [3, -3], + [-4, -2], + [-3, 1]])), + ( + np.array([2 - 1j, 2 + 1j, -1 + 1j, 1 - 1j]), + np.array([-1.3 + 1.3j, -1.3 + 1.3j, + -1.3 + 3.3j, - .3 + 4.3j, + -3.3 + 1.3j]), + np.array([1 - 2j, 1 + 1j, 2 - 3j, 1 + 1j]), + # du exp + np.array([-1.3 + 1.3j, -1.3 + 3.3j, + -0.3 + 4.3j, -3.3 + 1.3j]), + np.array([1 - 2j, 1 + 1j, 2 - 3j, 1 + 1j, + -1.3399 + 0.2875j]), + np.array([2 + 1j, -1 + 1j, 1 - 1j]), + np.array([2, 3, 4, 5, 5]), + np.array([[2.4 - 5j, 2.7 + 6.9j], + [3.4 + 18.2j, - 6.9 - 5.3j], + [-14.7 + 9.7j, - 6 - .6j], + [31.9 - 7.7j, -3.9 + 9.3j], + [-1 + 1.6j, -3 + 12.2j]]), + np.array([[1 + 1j, 2 - 1j], + [3 - 1j, 1 + 2j], + [4 + 5j, -1 + 1j], + [-1 - 2j, 2 + 1j], + [1 - 1j, 2 - 2j]]) + )]) +def test_gttrf_gttrs_NAG_f07cdf_f07cef_f07crf_f07csf(du, d, dl, du_exp, d_exp, + du2_exp, ipiv_exp, b, x): + # test to assure that wrapper is consistent with NAG Library Manual Mark 26 + # example problems: f07cdf and f07cef (real) + # examples: f07crf and f07csf (complex) + # (Links may expire, so search for "NAG Library Manual Mark 26" online) + + gttrf, gttrs = get_lapack_funcs(('gttrf', "gttrs"), (du[0], du[0])) + + _dl, _d, _du, du2, ipiv, info = gttrf(dl, d, du) + assert_allclose(du2, du2_exp) + assert_allclose(_du, du_exp) + assert_allclose(_d, d_exp, atol=1e-4) # NAG examples provide 4 decimals. + assert_allclose(ipiv, ipiv_exp) + + x_gttrs, info = gttrs(_dl, _d, _du, du2, ipiv, b) + + assert_allclose(x_gttrs, x) + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('norm', ['1', 'I', 'O']) +@pytest.mark.parametrize('n', [3, 10]) +def test_gtcon(dtype, norm, n): + rng = np.random.default_rng(23498324) + + d = rng.random(n) + rng.random(n)*1j + dl = rng.random(n - 1) + rng.random(n - 1)*1j + du = rng.random(n - 1) + rng.random(n - 1)*1j + A = np.diag(d) + np.diag(dl, -1) + np.diag(du, 1) + if np.issubdtype(dtype, np.floating): + A, d, dl, du = A.real, d.real, dl.real, du.real + A, d, dl, du = A.astype(dtype), d.astype(dtype), dl.astype(dtype), du.astype(dtype) + + anorm = np.linalg.norm(A, ord=np.inf if norm == 'I' else 1) + + gttrf, gtcon = get_lapack_funcs(('gttrf', 'gtcon'), (A,)) + dl, d, du, du2, ipiv, info = gttrf(dl, d, du) + res, _ = gtcon(dl, d, du, du2, ipiv, anorm, norm=norm) + + gecon, getrf = get_lapack_funcs(('gecon', 'getrf'), (A,)) + lu, ipvt, info = getrf(A) + ref, _ = gecon(lu, anorm, norm=norm) + + rtol = np.finfo(dtype).eps**0.75 + assert_allclose(res, ref, rtol=rtol) + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('shape', [(3, 7), (7, 3), (2**18, 2**18)]) +def test_geqrfp_lwork(dtype, shape): + geqrfp_lwork = get_lapack_funcs(('geqrfp_lwork'), dtype=dtype) + m, n = shape + lwork, info = geqrfp_lwork(m=m, n=n) + assert_equal(info, 0) + + +@pytest.mark.parametrize("ddtype,dtype", + zip(REAL_DTYPES + REAL_DTYPES, DTYPES)) +def test_pttrf_pttrs(ddtype, dtype): + rng = np.random.RandomState(42) + # set test tolerance appropriate for dtype + atol = 100*np.finfo(dtype).eps + # n is the length diagonal of A + n = 10 + # create diagonals according to size and dtype + + # diagonal d should always be real. + # add 4 to d so it will be dominant for all dtypes + d = generate_random_dtype_array((n,), ddtype, rng) + 4 + # diagonal e may be real or complex. + e = generate_random_dtype_array((n-1,), dtype, rng) + + # assemble diagonals together into matrix + A = np.diag(d) + np.diag(e, -1) + np.diag(np.conj(e), 1) + # store a copy of diagonals to later verify + diag_cpy = [d.copy(), e.copy()] + + pttrf = get_lapack_funcs('pttrf', dtype=dtype) + + _d, _e, info = pttrf(d, e) + # test to assure that the inputs of ?pttrf are unmodified + assert_array_equal(d, diag_cpy[0]) + assert_array_equal(e, diag_cpy[1]) + assert_equal(info, 0, err_msg=f"pttrf: info = {info}, should be 0") + + # test that the factors from pttrf can be recombined to make A + L = np.diag(_e, -1) + np.diag(np.ones(n)) + D = np.diag(_d) + + assert_allclose(A, L@D@L.conjugate().T, atol=atol) + + # generate random solution x + x = generate_random_dtype_array((n,), dtype, rng) + # determine accompanying b to get soln x + b = A@x + + # determine _x from pttrs + pttrs = get_lapack_funcs('pttrs', dtype=dtype) + _x, info = pttrs(_d, _e.conj(), b) + assert_equal(info, 0, err_msg=f"pttrs: info = {info}, should be 0") + + # test that _x from pttrs matches the expected x + assert_allclose(x, _x, atol=atol) + + +@pytest.mark.parametrize("ddtype,dtype", + zip(REAL_DTYPES + REAL_DTYPES, DTYPES)) +def test_pttrf_pttrs_errors_incompatible_shape(ddtype, dtype): + n = 10 + rng = np.random.RandomState(1234) + pttrf = get_lapack_funcs('pttrf', dtype=dtype) + d = generate_random_dtype_array((n,), ddtype, rng) + 2 + e = generate_random_dtype_array((n-1,), dtype, rng) + # test that ValueError is raised with incompatible matrix shapes + assert_raises(ValueError, pttrf, d[:-1], e) + assert_raises(ValueError, pttrf, d, e[:-1]) + + +@pytest.mark.parametrize("ddtype,dtype", + zip(REAL_DTYPES + REAL_DTYPES, DTYPES)) +def test_pttrf_pttrs_errors_singular_nonSPD(ddtype, dtype): + n = 10 + rng = np.random.RandomState(42) + pttrf = get_lapack_funcs('pttrf', dtype=dtype) + d = generate_random_dtype_array((n,), ddtype, rng) + 2 + e = generate_random_dtype_array((n-1,), dtype, rng) + # test that singular (row of all zeroes) matrix fails via info + d[0] = 0 + e[0] = 0 + _d, _e, info = pttrf(d, e) + assert_equal(_d[info - 1], 0, + f"?pttrf: _d[info-1] is {_d[info - 1]}, not the illegal value :0.") + + # test with non-spd matrix + d = generate_random_dtype_array((n,), ddtype, rng) + _d, _e, info = pttrf(d, e) + assert_(info != 0, "?pttrf should fail with non-spd matrix, but didn't") + + +@pytest.mark.parametrize(("d, e, d_expect, e_expect, b, x_expect"), [ + (np.array([4, 10, 29, 25, 5]), + np.array([-2, -6, 15, 8]), + np.array([4, 9, 25, 16, 1]), + np.array([-.5, -.6667, .6, .5]), + np.array([[6, 10], [9, 4], [2, 9], [14, 65], + [7, 23]]), + np.array([[2.5, 2], [2, -1], [1, -3], [-1, 6], + [3, -5]]) + ), ( + np.array([16, 41, 46, 21]), + np.array([16 + 16j, 18 - 9j, 1 - 4j]), + np.array([16, 9, 1, 4]), + np.array([1+1j, 2-1j, 1-4j]), + np.array([[64+16j, -16-32j], [93+62j, 61-66j], + [78-80j, 71-74j], [14-27j, 35+15j]]), + np.array([[2+1j, -3-2j], [1+1j, 1+1j], [1-2j, 1-2j], + [1-1j, 2+1j]]) + )]) +def test_pttrf_pttrs_NAG(d, e, d_expect, e_expect, b, x_expect): + # test to assure that wrapper is consistent with NAG Manual Mark 26 + # example problems: f07jdf and f07jef (real) + # examples: f07jrf and f07csf (complex) + # NAG examples provide 4 decimals. + # (Links expire, so please search for "NAG Library Manual Mark 26" online) + + atol = 1e-4 + pttrf = get_lapack_funcs('pttrf', dtype=e[0]) + _d, _e, info = pttrf(d, e) + assert_allclose(_d, d_expect, atol=atol) + assert_allclose(_e, e_expect, atol=atol) + + pttrs = get_lapack_funcs('pttrs', dtype=e[0]) + _x, info = pttrs(_d, _e.conj(), b) + assert_allclose(_x, x_expect, atol=atol) + + # also test option `lower` + if e.dtype in COMPLEX_DTYPES: + _x, info = pttrs(_d, _e, b, lower=1) + assert_allclose(_x, x_expect, atol=atol) + + +def pteqr_get_d_e_A_z(dtype, realtype, n, compute_z): + # used by ?pteqr tests to build parameters + # returns tuple of (d, e, A, z) + rng = np.random.RandomState(42) + if compute_z == 1: + # build Hermitian A from Q**T * tri * Q = A by creating Q and tri + A_eig = generate_random_dtype_array((n, n), dtype, rng) + A_eig = A_eig + np.diag(np.zeros(n) + 4*n) + A_eig = (A_eig + A_eig.conj().T) / 2 + # obtain right eigenvectors (orthogonal) + vr = eigh(A_eig)[1] + # create tridiagonal matrix + d = generate_random_dtype_array((n,), realtype, rng) + 4 + e = generate_random_dtype_array((n-1,), realtype, rng) + tri = np.diag(d) + np.diag(e, 1) + np.diag(e, -1) + # Build A using these factors that sytrd would: (Q**T * tri * Q = A) + A = vr @ tri @ vr.conj().T + # vr is orthogonal + z = vr + + else: + # d and e are always real per lapack docs. + d = generate_random_dtype_array((n,), realtype, rng) + e = generate_random_dtype_array((n-1,), realtype, rng) + + # make SPD + d = d + 4 + A = np.diag(d) + np.diag(e, 1) + np.diag(e, -1) + z = np.diag(d) + np.diag(e, -1) + np.diag(e, 1) + return (d, e, A, z) + + +@pytest.mark.parametrize("dtype,realtype", + zip(DTYPES, REAL_DTYPES + REAL_DTYPES)) +@pytest.mark.parametrize("compute_z", range(3)) +def test_pteqr(dtype, realtype, compute_z): + ''' + Tests the ?pteqr lapack routine for all dtypes and compute_z parameters. + It generates random SPD matrix diagonals d and e, and then confirms + correct eigenvalues with scipy.linalg.eig. With applicable compute_z=2 it + tests that z can reform A. + ''' + atol = 1000*np.finfo(dtype).eps + pteqr = get_lapack_funcs(('pteqr'), dtype=dtype) + + n = 10 + + d, e, A, z = pteqr_get_d_e_A_z(dtype, realtype, n, compute_z) + + d_pteqr, e_pteqr, z_pteqr, info = pteqr(d=d, e=e, z=z, compute_z=compute_z) + assert_equal(info, 0, f"info = {info}, should be 0.") + + # compare the routine's eigenvalues with scipy.linalg.eig's. + assert_allclose(np.sort(eigh(A)[0]), np.sort(d_pteqr), atol=atol) + + if compute_z: + # verify z_pteqr as orthogonal + assert_allclose(z_pteqr @ np.conj(z_pteqr).T, np.identity(n), + atol=atol) + # verify that z_pteqr recombines to A + assert_allclose(z_pteqr @ np.diag(d_pteqr) @ np.conj(z_pteqr).T, + A, atol=atol) + + +@pytest.mark.parametrize("dtype,realtype", + zip(DTYPES, REAL_DTYPES + REAL_DTYPES)) +@pytest.mark.parametrize("compute_z", range(3)) +def test_pteqr_error_non_spd(dtype, realtype, compute_z): + pteqr = get_lapack_funcs(('pteqr'), dtype=dtype) + + n = 10 + d, e, A, z = pteqr_get_d_e_A_z(dtype, realtype, n, compute_z) + + # test with non-spd matrix + d_pteqr, e_pteqr, z_pteqr, info = pteqr(d - 4, e, z=z, compute_z=compute_z) + assert info > 0 + + +@pytest.mark.parametrize("dtype,realtype", + zip(DTYPES, REAL_DTYPES + REAL_DTYPES)) +@pytest.mark.parametrize("compute_z", range(3)) +def test_pteqr_raise_error_wrong_shape(dtype, realtype, compute_z): + pteqr = get_lapack_funcs(('pteqr'), dtype=dtype) + n = 10 + d, e, A, z = pteqr_get_d_e_A_z(dtype, realtype, n, compute_z) + # test with incorrect/incompatible array sizes + assert_raises(ValueError, pteqr, d[:-1], e, z=z, compute_z=compute_z) + assert_raises(ValueError, pteqr, d, e[:-1], z=z, compute_z=compute_z) + if compute_z: + assert_raises(ValueError, pteqr, d, e, z=z[:-1], compute_z=compute_z) + + +@pytest.mark.parametrize("dtype,realtype", + zip(DTYPES, REAL_DTYPES + REAL_DTYPES)) +@pytest.mark.parametrize("compute_z", range(3)) +def test_pteqr_error_singular(dtype, realtype, compute_z): + pteqr = get_lapack_funcs(('pteqr'), dtype=dtype) + n = 10 + d, e, A, z = pteqr_get_d_e_A_z(dtype, realtype, n, compute_z) + # test with singular matrix + d[0] = 0 + e[0] = 0 + d_pteqr, e_pteqr, z_pteqr, info = pteqr(d, e, z=z, compute_z=compute_z) + assert info > 0 + + +@pytest.mark.parametrize("compute_z,d,e,d_expect,z_expect", + [(2, # "I" + np.array([4.16, 5.25, 1.09, .62]), + np.array([3.17, -.97, .55]), + np.array([8.0023, 1.9926, 1.0014, 0.1237]), + np.array([[0.6326, 0.6245, -0.4191, 0.1847], + [0.7668, -0.4270, 0.4176, -0.2352], + [-0.1082, 0.6071, 0.4594, -0.6393], + [-0.0081, 0.2432, 0.6625, 0.7084]])), + ]) +def test_pteqr_NAG_f08jgf(compute_z, d, e, d_expect, z_expect): + ''' + Implements real (f08jgf) example from NAG Manual Mark 26. + Tests for correct outputs. + ''' + # the NAG manual has 4 decimals accuracy + atol = 1e-4 + pteqr = get_lapack_funcs(('pteqr'), dtype=d.dtype) + + z = np.diag(d) + np.diag(e, 1) + np.diag(e, -1) + _d, _e, _z, info = pteqr(d=d, e=e, z=z, compute_z=compute_z) + assert_allclose(_d, d_expect, atol=atol) + assert_allclose(np.abs(_z), np.abs(z_expect), atol=atol) + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('matrix_size', [(3, 4), (7, 6), (6, 6)]) +def test_geqrfp(dtype, matrix_size): + # Tests for all dytpes, tall, wide, and square matrices. + # Using the routine with random matrix A, Q and R are obtained and then + # tested such that R is upper triangular and non-negative on the diagonal, + # and Q is an orthogonal matrix. Verifies that A=Q@R. It also + # tests against a matrix that for which the linalg.qr method returns + # negative diagonals, and for error messaging. + + # set test tolerance appropriate for dtype + rng = np.random.RandomState(42) + rtol = 250*np.finfo(dtype).eps + atol = 100*np.finfo(dtype).eps + # get appropriate ?geqrfp for dtype + geqrfp = get_lapack_funcs(('geqrfp'), dtype=dtype) + gqr = get_lapack_funcs(("orgqr"), dtype=dtype) + + m, n = matrix_size + + # create random matrix of dimensions m x n + A = generate_random_dtype_array((m, n), dtype=dtype, rng=rng) + # create qr matrix using geqrfp + qr_A, tau, info = geqrfp(A) + + # obtain r from the upper triangular area + r = np.triu(qr_A) + + # obtain q from the orgqr lapack routine + # based on linalg.qr's extraction strategy of q with orgqr + + if m > n: + # this adds an extra column to the end of qr_A + # let qqr be an empty m x m matrix + qqr = np.zeros((m, m), dtype=dtype) + # set first n columns of qqr to qr_A + qqr[:, :n] = qr_A + # determine q from this qqr + # note that m is a sufficient for lwork based on LAPACK documentation + q = gqr(qqr, tau=tau, lwork=m)[0] + else: + q = gqr(qr_A[:, :m], tau=tau, lwork=m)[0] + + # test that q and r still make A + assert_allclose(q@r, A, rtol=rtol) + # ensure that q is orthogonal (that q @ transposed q is the identity) + assert_allclose(np.eye(q.shape[0]), q@(q.conj().T), rtol=rtol, + atol=atol) + # ensure r is upper tri by comparing original r to r as upper triangular + assert_allclose(r, np.triu(r), rtol=rtol) + # make sure diagonals of r are positive for this random solution + assert_(np.all(np.diag(r) > np.zeros(len(np.diag(r))))) + # ensure that info is zero for this success + assert_(info == 0) + + # test that this routine gives r diagonals that are positive for a + # matrix that returns negatives in the diagonal with scipy.linalg.rq + A_negative = generate_random_dtype_array((n, m), dtype=dtype, rng=rng) * -1 + r_rq_neg, q_rq_neg = qr(A_negative) + rq_A_neg, tau_neg, info_neg = geqrfp(A_negative) + # assert that any of the entries on the diagonal from linalg.qr + # are negative and that all of geqrfp are positive. + assert_(np.any(np.diag(r_rq_neg) < 0) and + np.all(np.diag(r) > 0)) + + +def test_geqrfp_errors_with_empty_array(): + # check that empty array raises good error message + A_empty = np.array([]) + geqrfp = get_lapack_funcs('geqrfp', dtype=A_empty.dtype) + assert_raises(Exception, geqrfp, A_empty) + + +@pytest.mark.parametrize("driver", ['ev', 'evd', 'evr', 'evx']) +@pytest.mark.parametrize("pfx", ['sy', 'he']) +def test_standard_eigh_lworks(pfx, driver): + n = 1200 # Some sufficiently big arbitrary number + dtype = REAL_DTYPES if pfx == 'sy' else COMPLEX_DTYPES + sc_dlw = get_lapack_funcs(pfx+driver+'_lwork', dtype=dtype[0]) + dz_dlw = get_lapack_funcs(pfx+driver+'_lwork', dtype=dtype[1]) + try: + _compute_lwork(sc_dlw, n, lower=1) + _compute_lwork(dz_dlw, n, lower=1) + except Exception as e: + pytest.fail(f"{pfx+driver}_lwork raised unexpected exception: {e}") + + +@pytest.mark.parametrize("driver", ['gv', 'gvx']) +@pytest.mark.parametrize("pfx", ['sy', 'he']) +def test_generalized_eigh_lworks(pfx, driver): + n = 1200 # Some sufficiently big arbitrary number + dtype = REAL_DTYPES if pfx == 'sy' else COMPLEX_DTYPES + sc_dlw = get_lapack_funcs(pfx+driver+'_lwork', dtype=dtype[0]) + dz_dlw = get_lapack_funcs(pfx+driver+'_lwork', dtype=dtype[1]) + # Shouldn't raise any exceptions + try: + _compute_lwork(sc_dlw, n, uplo="L") + _compute_lwork(dz_dlw, n, uplo="L") + except Exception as e: + pytest.fail(f"{pfx+driver}_lwork raised unexpected exception: {e}") + + +@pytest.mark.parametrize("dtype_", DTYPES) +@pytest.mark.parametrize("m", [1, 10, 100, 1000]) +def test_orcsd_uncsd_lwork(dtype_, m): + rng = np.random.default_rng(1234) + p = rng.integers(0, m) + q = m - p + pfx = 'or' if dtype_ in REAL_DTYPES else 'un' + dlw = pfx + 'csd_lwork' + lw = get_lapack_funcs(dlw, dtype=dtype_) + lwval = _compute_lwork(lw, m, p, q) + lwval = lwval if pfx == 'un' else (lwval,) + assert all([x > 0 for x in lwval]) + + +@pytest.mark.parametrize("dtype_", DTYPES) +def test_orcsd_uncsd(dtype_): + m, p, q = 250, 80, 170 + + pfx = 'or' if dtype_ in REAL_DTYPES else 'un' + X = ortho_group.rvs(m) if pfx == 'or' else unitary_group.rvs(m) + + drv, dlw = get_lapack_funcs((pfx + 'csd', pfx + 'csd_lwork'), dtype=dtype_) + lwval = _compute_lwork(dlw, m, p, q) + lwvals = {'lwork': lwval} if pfx == 'or' else dict(zip(['lwork', + 'lrwork'], lwval)) + + cs11, cs12, cs21, cs22, theta, u1, u2, v1t, v2t, info =\ + drv(X[:p, :q], X[:p, q:], X[p:, :q], X[p:, q:], **lwvals) + + assert info == 0 + + U = block_diag(u1, u2) + VH = block_diag(v1t, v2t) + r = min(min(p, q), min(m-p, m-q)) + n11 = min(p, q) - r + n12 = min(p, m-q) - r + n21 = min(m-p, q) - r + n22 = min(m-p, m-q) - r + + S = np.zeros((m, m), dtype=dtype_) + one = dtype_(1.) + for i in range(n11): + S[i, i] = one + for i in range(n22): + S[p+i, q+i] = one + for i in range(n12): + S[i+n11+r, i+n11+r+n21+n22+r] = -one + for i in range(n21): + S[p+n22+r+i, n11+r+i] = one + + for i in range(r): + S[i+n11, i+n11] = np.cos(theta[i]) + S[p+n22+i, i+r+n21+n22] = np.cos(theta[i]) + + S[i+n11, i+n11+n21+n22+r] = -np.sin(theta[i]) + S[p+n22+i, i+n11] = np.sin(theta[i]) + + Xc = U @ S @ VH + assert_allclose(X, Xc, rtol=0., atol=1e4*np.finfo(dtype_).eps) + + +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("trans_bool", [False, True]) +@pytest.mark.parametrize("fact", ["F", "N"]) +def test_gtsvx(dtype, trans_bool, fact): + """ + These tests uses ?gtsvx to solve a random Ax=b system for each dtype. + It tests that the outputs define an LU matrix, that inputs are unmodified, + transposal options, incompatible shapes, singular matrices, and + singular factorizations. It parametrizes DTYPES and the 'fact' value along + with the fact related inputs. + """ + rng = np.random.RandomState(42) + # set test tolerance appropriate for dtype + atol = 100 * np.finfo(dtype).eps + # obtain routine + gtsvx, gttrf = get_lapack_funcs(('gtsvx', 'gttrf'), dtype=dtype) + # Generate random tridiagonal matrix A + n = 10 + dl = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + d = generate_random_dtype_array((n,), dtype=dtype, rng=rng) + du = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + A = np.diag(dl, -1) + np.diag(d) + np.diag(du, 1) + # generate random solution x + x = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + # create b from x for equation Ax=b + trans = ("T" if dtype in REAL_DTYPES else "C") if trans_bool else "N" + b = (A.conj().T if trans_bool else A) @ x + + # store a copy of the inputs to check they haven't been modified later + inputs_cpy = [dl.copy(), d.copy(), du.copy(), b.copy()] + + # set these to None if fact = 'N', or the output of gttrf is fact = 'F' + dlf_, df_, duf_, du2f_, ipiv_, info_ = \ + gttrf(dl, d, du) if fact == 'F' else [None]*6 + + gtsvx_out = gtsvx(dl, d, du, b, fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + dlf, df, duf, du2f, ipiv, x_soln, rcond, ferr, berr, info = gtsvx_out + assert_(info == 0, f"?gtsvx info = {info}, should be zero") + + # assure that inputs are unmodified + assert_array_equal(dl, inputs_cpy[0]) + assert_array_equal(d, inputs_cpy[1]) + assert_array_equal(du, inputs_cpy[2]) + assert_array_equal(b, inputs_cpy[3]) + + # test that x_soln matches the expected x + assert_allclose(x, x_soln, atol=atol) + + # assert that the outputs are of correct type or shape + # rcond should be a scalar + assert_(hasattr(rcond, "__len__") is not True, + f"rcond should be scalar but is {rcond}") + # ferr should be length of # of cols in x + assert_(ferr.shape[0] == b.shape[1], (f"ferr.shape is {ferr.shape[0]} but should" + f" be {b.shape[1]}")) + # berr should be length of # of cols in x + assert_(berr.shape[0] == b.shape[1], (f"berr.shape is {berr.shape[0]} but should" + f" be {b.shape[1]}")) + + +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("trans_bool", [0, 1]) +@pytest.mark.parametrize("fact", ["F", "N"]) +def test_gtsvx_error_singular(dtype, trans_bool, fact): + rng = np.random.RandomState(42) + # obtain routine + gtsvx, gttrf = get_lapack_funcs(('gtsvx', 'gttrf'), dtype=dtype) + # Generate random tridiagonal matrix A + n = 10 + dl = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + d = generate_random_dtype_array((n,), dtype=dtype, rng=rng) + du = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + A = np.diag(dl, -1) + np.diag(d) + np.diag(du, 1) + # generate random solution x + x = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + # create b from x for equation Ax=b + trans = "T" if dtype in REAL_DTYPES else "C" + b = (A.conj().T if trans_bool else A) @ x + + # set these to None if fact = 'N', or the output of gttrf is fact = 'F' + dlf_, df_, duf_, du2f_, ipiv_, info_ = \ + gttrf(dl, d, du) if fact == 'F' else [None]*6 + + gtsvx_out = gtsvx(dl, d, du, b, fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + dlf, df, duf, du2f, ipiv, x_soln, rcond, ferr, berr, info = gtsvx_out + # test with singular matrix + # no need to test inputs with fact "F" since ?gttrf already does. + if fact == "N": + # Construct a singular example manually + d[-1] = 0 + dl[-1] = 0 + # solve using routine + gtsvx_out = gtsvx(dl, d, du, b) + dlf, df, duf, du2f, ipiv, x_soln, rcond, ferr, berr, info = gtsvx_out + # test for the singular matrix. + assert info > 0, "info should be > 0 for singular matrix" + + elif fact == 'F': + # assuming that a singular factorization is input + df_[-1] = 0 + duf_[-1] = 0 + du2f_[-1] = 0 + + gtsvx_out = gtsvx(dl, d, du, b, fact=fact, dlf=dlf_, df=df_, duf=duf_, + du2=du2f_, ipiv=ipiv_) + dlf, df, duf, du2f, ipiv, x_soln, rcond, ferr, berr, info = gtsvx_out + # info should not be zero and should provide index of illegal value + assert info > 0, "info should be > 0 for singular matrix" + + +@pytest.mark.parametrize("dtype", DTYPES*2) +@pytest.mark.parametrize("trans_bool", [False, True]) +@pytest.mark.parametrize("fact", ["F", "N"]) +def test_gtsvx_error_incompatible_size(dtype, trans_bool, fact): + rng = np.random.RandomState(42) + # obtain routine + gtsvx, gttrf = get_lapack_funcs(('gtsvx', 'gttrf'), dtype=dtype) + # Generate random tridiagonal matrix A + n = 10 + dl = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + d = generate_random_dtype_array((n,), dtype=dtype, rng=rng) + du = generate_random_dtype_array((n-1,), dtype=dtype, rng=rng) + A = np.diag(dl, -1) + np.diag(d) + np.diag(du, 1) + # generate random solution x + x = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + # create b from x for equation Ax=b + trans = "T" if dtype in REAL_DTYPES else "C" + b = (A.conj().T if trans_bool else A) @ x + + # set these to None if fact = 'N', or the output of gttrf is fact = 'F' + dlf_, df_, duf_, du2f_, ipiv_, info_ = \ + gttrf(dl, d, du) if fact == 'F' else [None]*6 + + if fact == "N": + assert_raises(ValueError, gtsvx, dl[:-1], d, du, b, + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + assert_raises(ValueError, gtsvx, dl, d[:-1], du, b, + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + assert_raises(ValueError, gtsvx, dl, d, du[:-1], b, + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + assert_raises(Exception, gtsvx, dl, d, du, b[:-1], + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + else: + assert_raises(ValueError, gtsvx, dl, d, du, b, + fact=fact, trans=trans, dlf=dlf_[:-1], df=df_, + duf=duf_, du2=du2f_, ipiv=ipiv_) + assert_raises(ValueError, gtsvx, dl, d, du, b, + fact=fact, trans=trans, dlf=dlf_, df=df_[:-1], + duf=duf_, du2=du2f_, ipiv=ipiv_) + assert_raises(ValueError, gtsvx, dl, d, du, b, + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_[:-1], du2=du2f_, ipiv=ipiv_) + assert_raises(ValueError, gtsvx, dl, d, du, b, + fact=fact, trans=trans, dlf=dlf_, df=df_, + duf=duf_, du2=du2f_[:-1], ipiv=ipiv_) + + +@pytest.mark.parametrize("du,d,dl,b,x", + [(np.array([2.1, -1.0, 1.9, 8.0]), + np.array([3.0, 2.3, -5.0, -0.9, 7.1]), + np.array([3.4, 3.6, 7.0, -6.0]), + np.array([[2.7, 6.6], [-.5, 10.8], [2.6, -3.2], + [.6, -11.2], [2.7, 19.1]]), + np.array([[-4, 5], [7, -4], [3, -3], [-4, -2], + [-3, 1]])), + (np.array([2 - 1j, 2 + 1j, -1 + 1j, 1 - 1j]), + np.array([-1.3 + 1.3j, -1.3 + 1.3j, -1.3 + 3.3j, + -.3 + 4.3j, -3.3 + 1.3j]), + np.array([1 - 2j, 1 + 1j, 2 - 3j, 1 + 1j]), + np.array([[2.4 - 5j, 2.7 + 6.9j], + [3.4 + 18.2j, -6.9 - 5.3j], + [-14.7 + 9.7j, -6 - .6j], + [31.9 - 7.7j, -3.9 + 9.3j], + [-1 + 1.6j, -3 + 12.2j]]), + np.array([[1 + 1j, 2 - 1j], [3 - 1j, 1 + 2j], + [4 + 5j, -1 + 1j], [-1 - 2j, 2 + 1j], + [1 - 1j, 2 - 2j]]))]) +def test_gtsvx_NAG(du, d, dl, b, x): + # Test to ensure wrapper is consistent with NAG Manual Mark 26 + # example problems: real (f07cbf) and complex (f07cpf) + gtsvx = get_lapack_funcs('gtsvx', dtype=d.dtype) + + gtsvx_out = gtsvx(dl, d, du, b) + dlf, df, duf, du2f, ipiv, x_soln, rcond, ferr, berr, info = gtsvx_out + + assert_array_almost_equal(x, x_soln) + + +@pytest.mark.parametrize("dtype,realtype", zip(DTYPES, REAL_DTYPES + + REAL_DTYPES)) +@pytest.mark.parametrize("fact,df_de_lambda", + [("F", + lambda d, e: get_lapack_funcs('pttrf', + dtype=e.dtype)(d, e)), + ("N", lambda d, e: (None, None, None))]) +def test_ptsvx(dtype, realtype, fact, df_de_lambda): + ''' + This tests the ?ptsvx lapack routine wrapper to solve a random system + Ax = b for all dtypes and input variations. Tests for: unmodified + input parameters, fact options, incompatible matrix shapes raise an error, + and singular matrices return info of illegal value. + ''' + rng = np.random.RandomState(42) + # set test tolerance appropriate for dtype + atol = 100 * np.finfo(dtype).eps + ptsvx = get_lapack_funcs('ptsvx', dtype=dtype) + n = 5 + # create diagonals according to size and dtype + d = generate_random_dtype_array((n,), realtype, rng) + 4 + e = generate_random_dtype_array((n-1,), dtype, rng) + A = np.diag(d) + np.diag(e, -1) + np.diag(np.conj(e), 1) + x_soln = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + b = A @ x_soln + + # use lambda to determine what df, ef are + df, ef, info = df_de_lambda(d, e) + + # create copy to later test that they are unmodified + diag_cpy = [d.copy(), e.copy(), b.copy()] + + # solve using routine + df, ef, x, rcond, ferr, berr, info = ptsvx(d, e, b, fact=fact, + df=df, ef=ef) + # d, e, and b should be unmodified + assert_array_equal(d, diag_cpy[0]) + assert_array_equal(e, diag_cpy[1]) + assert_array_equal(b, diag_cpy[2]) + assert_(info == 0, f"info should be 0 but is {info}.") + assert_array_almost_equal(x_soln, x) + + # test that the factors from ptsvx can be recombined to make A + L = np.diag(ef, -1) + np.diag(np.ones(n)) + D = np.diag(df) + assert_allclose(A, L@D@(np.conj(L).T), atol=atol) + + # assert that the outputs are of correct type or shape + # rcond should be a scalar + assert not hasattr(rcond, "__len__"), \ + f"rcond should be scalar but is {rcond}" + # ferr should be length of # of cols in x + assert_(ferr.shape == (2,), (f"ferr.shape is {ferr.shape} but should be " + "({x_soln.shape[1]},)")) + # berr should be length of # of cols in x + assert_(berr.shape == (2,), (f"berr.shape is {berr.shape} but should be " + "({x_soln.shape[1]},)")) + + +@pytest.mark.parametrize("dtype,realtype", zip(DTYPES, REAL_DTYPES + + REAL_DTYPES)) +@pytest.mark.parametrize("fact,df_de_lambda", + [("F", + lambda d, e: get_lapack_funcs('pttrf', + dtype=e.dtype)(d, e)), + ("N", lambda d, e: (None, None, None))]) +def test_ptsvx_error_raise_errors(dtype, realtype, fact, df_de_lambda): + rng = np.random.RandomState(42) + ptsvx = get_lapack_funcs('ptsvx', dtype=dtype) + n = 5 + # create diagonals according to size and dtype + d = generate_random_dtype_array((n,), realtype, rng) + 4 + e = generate_random_dtype_array((n-1,), dtype, rng) + A = np.diag(d) + np.diag(e, -1) + np.diag(np.conj(e), 1) + x_soln = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + b = A @ x_soln + + # use lambda to determine what df, ef are + df, ef, info = df_de_lambda(d, e) + + # test with malformatted array sizes + assert_raises(ValueError, ptsvx, d[:-1], e, b, fact=fact, df=df, ef=ef) + assert_raises(ValueError, ptsvx, d, e[:-1], b, fact=fact, df=df, ef=ef) + assert_raises(Exception, ptsvx, d, e, b[:-1], fact=fact, df=df, ef=ef) + + +@pytest.mark.parametrize("dtype,realtype", zip(DTYPES, REAL_DTYPES + + REAL_DTYPES)) +@pytest.mark.parametrize("fact,df_de_lambda", + [("F", + lambda d, e: get_lapack_funcs('pttrf', + dtype=e.dtype)(d, e)), + ("N", lambda d, e: (None, None, None))]) +def test_ptsvx_non_SPD_singular(dtype, realtype, fact, df_de_lambda): + rng = np.random.RandomState(42) + ptsvx = get_lapack_funcs('ptsvx', dtype=dtype) + n = 5 + # create diagonals according to size and dtype + d = generate_random_dtype_array((n,), realtype, rng) + 4 + e = generate_random_dtype_array((n-1,), dtype, rng) + A = np.diag(d) + np.diag(e, -1) + np.diag(np.conj(e), 1) + x_soln = generate_random_dtype_array((n, 2), dtype=dtype, rng=rng) + b = A @ x_soln + + # use lambda to determine what df, ef are + df, ef, info = df_de_lambda(d, e) + + if fact == "N": + d[3] = 0 + # obtain new df, ef + df, ef, info = df_de_lambda(d, e) + # solve using routine + df, ef, x, rcond, ferr, berr, info = ptsvx(d, e, b) + # test for the singular matrix. + assert info > 0 and info <= n + + # non SPD matrix + d = generate_random_dtype_array((n,), realtype, rng) + df, ef, x, rcond, ferr, berr, info = ptsvx(d, e, b) + assert info > 0 and info <= n + else: + # assuming that someone is using a singular factorization + df, ef, info = df_de_lambda(d, e) + df[0] = 0 + ef[0] = 0 + df, ef, x, rcond, ferr, berr, info = ptsvx(d, e, b, fact=fact, + df=df, ef=ef) + assert info > 0 + + +@pytest.mark.parametrize('d,e,b,x', + [(np.array([4, 10, 29, 25, 5]), + np.array([-2, -6, 15, 8]), + np.array([[6, 10], [9, 4], [2, 9], [14, 65], + [7, 23]]), + np.array([[2.5, 2], [2, -1], [1, -3], + [-1, 6], [3, -5]])), + (np.array([16, 41, 46, 21]), + np.array([16 + 16j, 18 - 9j, 1 - 4j]), + np.array([[64 + 16j, -16 - 32j], + [93 + 62j, 61 - 66j], + [78 - 80j, 71 - 74j], + [14 - 27j, 35 + 15j]]), + np.array([[2 + 1j, -3 - 2j], + [1 + 1j, 1 + 1j], + [1 - 2j, 1 - 2j], + [1 - 1j, 2 + 1j]]))]) +def test_ptsvx_NAG(d, e, b, x): + # test to assure that wrapper is consistent with NAG Manual Mark 26 + # example problems: f07jbf, f07jpf + # (Links expire, so please search for "NAG Library Manual Mark 26" online) + + # obtain routine with correct type based on e.dtype + ptsvx = get_lapack_funcs('ptsvx', dtype=e.dtype) + # solve using routine + df, ef, x_ptsvx, rcond, ferr, berr, info = ptsvx(d, e, b) + # determine ptsvx's solution and x are the same. + assert_array_almost_equal(x, x_ptsvx) + + +@pytest.mark.parametrize('lower', [False, True]) +@pytest.mark.parametrize('dtype', DTYPES) +def test_pptrs_pptri_pptrf_ppsv_ppcon(dtype, lower): + rng = np.random.RandomState(1234) + atol = np.finfo(dtype).eps*100 + # Manual conversion to/from packed format is feasible here. + n, nrhs = 10, 4 + a = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + b = generate_random_dtype_array([n, nrhs], dtype=dtype, rng=rng) + + a = a.conj().T + a + np.eye(n, dtype=dtype) * dtype(5.) + if lower: + inds = ([x for y in range(n) for x in range(y, n)], + [y for y in range(n) for x in range(y, n)]) + else: + inds = ([x for y in range(1, n+1) for x in range(y)], + [y-1 for y in range(1, n+1) for x in range(y)]) + ap = a[inds] + ppsv, pptrf, pptrs, pptri, ppcon = get_lapack_funcs( + ('ppsv', 'pptrf', 'pptrs', 'pptri', 'ppcon'), + dtype=dtype, + ilp64="preferred") + + ul, info = pptrf(n, ap, lower=lower) + assert_equal(info, 0) + aul = cholesky(a, lower=lower)[inds] + assert_allclose(ul, aul, rtol=0, atol=atol) + + uli, info = pptri(n, ul, lower=lower) + assert_equal(info, 0) + auli = inv(a)[inds] + assert_allclose(uli, auli, rtol=0, atol=atol) + + x, info = pptrs(n, ul, b, lower=lower) + assert_equal(info, 0) + bx = solve(a, b) + assert_allclose(x, bx, rtol=0, atol=atol) + + xv, info = ppsv(n, ap, b, lower=lower) + assert_equal(info, 0) + assert_allclose(xv, bx, rtol=0, atol=atol) + + anorm = np.linalg.norm(a, 1) + rcond, info = ppcon(n, ap, anorm=anorm, lower=lower) + assert_equal(info, 0) + assert_(abs(1/rcond - np.linalg.cond(a, p=1))*rcond < 1) + + +@pytest.mark.parametrize('dtype', DTYPES) +def test_gees_trexc(dtype): + rng = np.random.RandomState(1234) + atol = np.finfo(dtype).eps*100 + + n = 10 + a = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + + gees, trexc = get_lapack_funcs(('gees', 'trexc'), dtype=dtype) + + result = gees(lambda x: None, a, overwrite_a=False) + assert_equal(result[-1], 0) + + t = result[0] + z = result[-3] + + d2 = t[6, 6] + + if dtype in COMPLEX_DTYPES: + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(z @ t @ z.conj().T, a, rtol=0, atol=atol) + + result = trexc(t, z, 7, 1) + assert_equal(result[-1], 0) + + t = result[0] + z = result[-2] + + if dtype in COMPLEX_DTYPES: + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(z @ t @ z.conj().T, a, rtol=0, atol=atol) + + assert_allclose(t[0, 0], d2, rtol=0, atol=atol) + + +@pytest.mark.parametrize( + "t, expect, ifst, ilst", + [(np.array([[0.80, -0.11, 0.01, 0.03], + [0.00, -0.10, 0.25, 0.35], + [0.00, -0.65, -0.10, 0.20], + [0.00, 0.00, 0.00, -0.10]]), + np.array([[-0.1000, -0.6463, 0.0874, 0.2010], + [0.2514, -0.1000, 0.0927, 0.3505], + [0.0000, 0.0000, 0.8000, -0.0117], + [0.0000, 0.0000, 0.0000, -0.1000]]), + 2, 1), + (np.array([[-6.00 - 7.00j, 0.36 - 0.36j, -0.19 + 0.48j, 0.88 - 0.25j], + [0.00 + 0.00j, -5.00 + 2.00j, -0.03 - 0.72j, -0.23 + 0.13j], + [0.00 + 0.00j, 0.00 + 0.00j, 8.00 - 1.00j, 0.94 + 0.53j], + [0.00 + 0.00j, 0.00 + 0.00j, 0.00 + 0.00j, 3.00 - 4.00j]]), + np.array([[-5.0000 + 2.0000j, -0.1574 + 0.7143j, + 0.1781 - 0.1913j, 0.3950 + 0.3861j], + [0.0000 + 0.0000j, 8.0000 - 1.0000j, + 1.0742 + 0.1447j, 0.2515 - 0.3397j], + [0.0000 + 0.0000j, 0.0000 + 0.0000j, + 3.0000 - 4.0000j, 0.2264 + 0.8962j], + [0.0000 + 0.0000j, 0.0000 + 0.0000j, + 0.0000 + 0.0000j, -6.0000 - 7.0000j]]), + 1, 4)]) +def test_trexc_NAG(t, ifst, ilst, expect): + """ + This test implements the example found in the NAG manual, + f08qfc, f08qtc, f08qgc, f08quc. + """ + # NAG manual provides accuracy up to 4 decimals + atol = 1e-4 + trexc = get_lapack_funcs('trexc', dtype=t.dtype) + + result = trexc(t, t, ifst, ilst, wantq=0) + assert_equal(result[-1], 0) + + t = result[0] + assert_allclose(expect, t, atol=atol) + + +@pytest.mark.parametrize('dtype', DTYPES) +def test_gges_tgexc(dtype): + rng = np.random.RandomState(1234) + atol = np.finfo(dtype).eps*100 + + n = 10 + a = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + b = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + + gges, tgexc = get_lapack_funcs(('gges', 'tgexc'), dtype=dtype) + + result = gges(lambda x: None, a, b, overwrite_a=False, overwrite_b=False) + assert_equal(result[-1], 0) + + s = result[0] + t = result[1] + q = result[-4] + z = result[-3] + + d1 = s[0, 0] / t[0, 0] + d2 = s[6, 6] / t[6, 6] + + if dtype in COMPLEX_DTYPES: + assert_allclose(s, np.triu(s), rtol=0, atol=atol) + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(q @ s @ z.conj().T, a, rtol=0, atol=atol) + assert_allclose(q @ t @ z.conj().T, b, rtol=0, atol=atol) + + result = tgexc(s, t, q, z, 7, 1) + assert_equal(result[-1], 0) + + s = result[0] + t = result[1] + q = result[2] + z = result[3] + + if dtype in COMPLEX_DTYPES: + assert_allclose(s, np.triu(s), rtol=0, atol=atol) + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(q @ s @ z.conj().T, a, rtol=0, atol=atol) + assert_allclose(q @ t @ z.conj().T, b, rtol=0, atol=atol) + + assert_allclose(s[0, 0] / t[0, 0], d2, rtol=0, atol=atol) + assert_allclose(s[1, 1] / t[1, 1], d1, rtol=0, atol=atol) + + +@pytest.mark.parametrize('dtype', DTYPES) +def test_gees_trsen(dtype): + rng = np.random.RandomState(1234) + atol = np.finfo(dtype).eps*100 + + n = 10 + a = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + + gees, trsen, trsen_lwork = get_lapack_funcs( + ('gees', 'trsen', 'trsen_lwork'), dtype=dtype) + + result = gees(lambda x: None, a, overwrite_a=False) + assert_equal(result[-1], 0) + + t = result[0] + z = result[-3] + + d2 = t[6, 6] + + if dtype in COMPLEX_DTYPES: + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(z @ t @ z.conj().T, a, rtol=0, atol=atol) + + select = np.zeros(n) + select[6] = 1 + + lwork = _compute_lwork(trsen_lwork, select, t) + + if dtype in COMPLEX_DTYPES: + result = trsen(select, t, z, lwork=lwork) + else: + result = trsen(select, t, z, lwork=lwork, liwork=lwork[1]) + assert_equal(result[-1], 0) + + t = result[0] + z = result[1] + + if dtype in COMPLEX_DTYPES: + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(z @ t @ z.conj().T, a, rtol=0, atol=atol) + + assert_allclose(t[0, 0], d2, rtol=0, atol=atol) + + +@pytest.mark.parametrize( + "t, q, expect, select, expect_s, expect_sep", + [(np.array([[0.7995, -0.1144, 0.0060, 0.0336], + [0.0000, -0.0994, 0.2478, 0.3474], + [0.0000, -0.6483, -0.0994, 0.2026], + [0.0000, 0.0000, 0.0000, -0.1007]]), + np.array([[0.6551, 0.1037, 0.3450, 0.6641], + [0.5236, -0.5807, -0.6141, -0.1068], + [-0.5362, -0.3073, -0.2935, 0.7293], + [0.0956, 0.7467, -0.6463, 0.1249]]), + np.array([[0.3500, 0.4500, -0.1400, -0.1700], + [0.0900, 0.0700, -0.5399, 0.3500], + [-0.4400, -0.3300, -0.0300, 0.1700], + [0.2500, -0.3200, -0.1300, 0.1100]]), + np.array([1, 0, 0, 1]), + 1.75e+00, 3.22e+00), + (np.array([[-6.0004 - 6.9999j, 0.3637 - 0.3656j, + -0.1880 + 0.4787j, 0.8785 - 0.2539j], + [0.0000 + 0.0000j, -5.0000 + 2.0060j, + -0.0307 - 0.7217j, -0.2290 + 0.1313j], + [0.0000 + 0.0000j, 0.0000 + 0.0000j, + 7.9982 - 0.9964j, 0.9357 + 0.5359j], + [0.0000 + 0.0000j, 0.0000 + 0.0000j, + 0.0000 + 0.0000j, 3.0023 - 3.9998j]]), + np.array([[-0.8347 - 0.1364j, -0.0628 + 0.3806j, + 0.2765 - 0.0846j, 0.0633 - 0.2199j], + [0.0664 - 0.2968j, 0.2365 + 0.5240j, + -0.5877 - 0.4208j, 0.0835 + 0.2183j], + [-0.0362 - 0.3215j, 0.3143 - 0.5473j, + 0.0576 - 0.5736j, 0.0057 - 0.4058j], + [0.0086 + 0.2958j, -0.3416 - 0.0757j, + -0.1900 - 0.1600j, 0.8327 - 0.1868j]]), + np.array([[-3.9702 - 5.0406j, -4.1108 + 3.7002j, + -0.3403 + 1.0098j, 1.2899 - 0.8590j], + [0.3397 - 1.5006j, 1.5201 - 0.4301j, + 1.8797 - 5.3804j, 3.3606 + 0.6498j], + [3.3101 - 3.8506j, 2.4996 + 3.4504j, + 0.8802 - 1.0802j, 0.6401 - 1.4800j], + [-1.0999 + 0.8199j, 1.8103 - 1.5905j, + 3.2502 + 1.3297j, 1.5701 - 3.4397j]]), + np.array([1, 0, 0, 1]), + 1.02e+00, 1.82e-01)]) +def test_trsen_NAG(t, q, select, expect, expect_s, expect_sep): + """ + This test implements the example found in the NAG manual, + f08qgc, f08quc. + """ + # NAG manual provides accuracy up to 4 and 2 decimals + atol = 1e-4 + atol2 = 1e-2 + trsen, trsen_lwork = get_lapack_funcs( + ('trsen', 'trsen_lwork'), dtype=t.dtype) + + lwork = _compute_lwork(trsen_lwork, select, t) + + if t.dtype in COMPLEX_DTYPES: + result = trsen(select, t, q, lwork=lwork) + else: + result = trsen(select, t, q, lwork=lwork, liwork=lwork[1]) + assert_equal(result[-1], 0) + + t = result[0] + q = result[1] + if t.dtype in COMPLEX_DTYPES: + s = result[4] + sep = result[5] + else: + s = result[5] + sep = result[6] + + assert_allclose(expect, q @ t @ q.conj().T, atol=atol) + assert_allclose(expect_s, 1 / s, atol=atol2) + assert_allclose(expect_sep, 1 / sep, atol=atol2) + + +@pytest.mark.parametrize('dtype', DTYPES) +def test_gges_tgsen(dtype): + rng = np.random.RandomState(1234) + atol = np.finfo(dtype).eps*100 + + n = 10 + a = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + b = generate_random_dtype_array([n, n], dtype=dtype, rng=rng) + + gges, tgsen, tgsen_lwork = get_lapack_funcs( + ('gges', 'tgsen', 'tgsen_lwork'), dtype=dtype) + + result = gges(lambda x: None, a, b, overwrite_a=False, overwrite_b=False) + assert_equal(result[-1], 0) + + s = result[0] + t = result[1] + q = result[-4] + z = result[-3] + + d1 = s[0, 0] / t[0, 0] + d2 = s[6, 6] / t[6, 6] + + if dtype in COMPLEX_DTYPES: + assert_allclose(s, np.triu(s), rtol=0, atol=atol) + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(q @ s @ z.conj().T, a, rtol=0, atol=atol) + assert_allclose(q @ t @ z.conj().T, b, rtol=0, atol=atol) + + select = np.zeros(n) + select[6] = 1 + + lwork = _compute_lwork(tgsen_lwork, select, s, t) + + # off-by-one error in LAPACK, see gh-issue #13397 + lwork = (lwork[0]+1, lwork[1]) + + result = tgsen(select, s, t, q, z, lwork=lwork) + assert_equal(result[-1], 0) + + s = result[0] + t = result[1] + q = result[-7] + z = result[-6] + + if dtype in COMPLEX_DTYPES: + assert_allclose(s, np.triu(s), rtol=0, atol=atol) + assert_allclose(t, np.triu(t), rtol=0, atol=atol) + + assert_allclose(q @ s @ z.conj().T, a, rtol=0, atol=atol) + assert_allclose(q @ t @ z.conj().T, b, rtol=0, atol=atol) + + assert_allclose(s[0, 0] / t[0, 0], d2, rtol=0, atol=atol) + assert_allclose(s[1, 1] / t[1, 1], d1, rtol=0, atol=atol) + + +@pytest.mark.parametrize( + "a, b, c, d, e, f, rans, lans", + [(np.array([[4.0, 1.0, 1.0, 2.0], + [0.0, 3.0, 4.0, 1.0], + [0.0, 1.0, 3.0, 1.0], + [0.0, 0.0, 0.0, 6.0]]), + np.array([[1.0, 1.0, 1.0, 1.0], + [0.0, 3.0, 4.0, 1.0], + [0.0, 1.0, 3.0, 1.0], + [0.0, 0.0, 0.0, 4.0]]), + np.array([[-4.0, 7.0, 1.0, 12.0], + [-9.0, 2.0, -2.0, -2.0], + [-4.0, 2.0, -2.0, 8.0], + [-7.0, 7.0, -6.0, 19.0]]), + np.array([[2.0, 1.0, 1.0, 3.0], + [0.0, 1.0, 2.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 2.0]]), + np.array([[1.0, 1.0, 1.0, 2.0], + [0.0, 1.0, 4.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 1.0]]), + np.array([[-7.0, 5.0, 0.0, 7.0], + [-5.0, 1.0, -8.0, 0.0], + [-1.0, 2.0, -3.0, 5.0], + [-3.0, 2.0, 0.0, 5.0]]), + np.array([[1.0, 1.0, 1.0, 1.0], + [-1.0, 2.0, -1.0, -1.0], + [-1.0, 1.0, 3.0, 1.0], + [-1.0, 1.0, -1.0, 4.0]]), + np.array([[4.0, -1.0, 1.0, -1.0], + [1.0, 3.0, -1.0, 1.0], + [-1.0, 1.0, 2.0, -1.0], + [1.0, -1.0, 1.0, 1.0]]))]) +@pytest.mark.parametrize('dtype', REAL_DTYPES) +def test_tgsyl_NAG(a, b, c, d, e, f, rans, lans, dtype): + atol = 1e-4 + + tgsyl = get_lapack_funcs(('tgsyl'), dtype=dtype) + rout, lout, scale, dif, info = tgsyl(a, b, c, d, e, f) + + assert_equal(info, 0) + assert_allclose(scale, 1.0, rtol=0, atol=np.finfo(dtype).eps*100, + err_msg="SCALE must be 1.0") + assert_allclose(dif, 0.0, rtol=0, atol=np.finfo(dtype).eps*100, + err_msg="DIF must be nearly 0") + assert_allclose(rout, rans, atol=atol, + err_msg="Solution for R is incorrect") + assert_allclose(lout, lans, atol=atol, + err_msg="Solution for L is incorrect") + + +@pytest.mark.parametrize('dtype', REAL_DTYPES) +@pytest.mark.parametrize('trans', ('N', 'T')) +@pytest.mark.parametrize('ijob', [0, 1, 2, 3, 4]) +def test_tgsyl(dtype, trans, ijob): + + atol = 1e-3 if dtype == np.float32 else 1e-10 + rng = np.random.default_rng(1685779866898198) + m, n = 10, 15 + + a, d, *_ = qz(rng.uniform(-10, 10, [m, m]).astype(dtype), + rng.uniform(-10, 10, [m, m]).astype(dtype), + output='real') + + b, e, *_ = qz(rng.uniform(-10, 10, [n, n]).astype(dtype), + rng.uniform(-10, 10, [n, n]).astype(dtype), + output='real') + + c = rng.uniform(-2, 2, [m, n]).astype(dtype) + f = rng.uniform(-2, 2, [m, n]).astype(dtype) + + tgsyl = get_lapack_funcs(('tgsyl'), dtype=dtype) + rout, lout, scale, dif, info = tgsyl(a, b, c, d, e, f, + trans=trans, ijob=ijob) + + assert info == 0, "INFO is non-zero" + assert scale >= 0.0, "SCALE must be non-negative" + if ijob == 0: + assert_allclose(dif, 0.0, rtol=0, atol=np.finfo(dtype).eps*100, + err_msg="DIF must be 0 for ijob =0") + else: + assert dif >= 0.0, "DIF must be non-negative" + + # Only DIF is calculated for ijob = 3/4 + if ijob <= 2: + if trans == 'N': + lhs1 = a @ rout - lout @ b + rhs1 = scale*c + lhs2 = d @ rout - lout @ e + rhs2 = scale*f + elif trans == 'T': + lhs1 = np.transpose(a) @ rout + np.transpose(d) @ lout + rhs1 = scale*c + lhs2 = rout @ np.transpose(b) + lout @ np.transpose(e) + rhs2 = -1.0*scale*f + + assert_allclose(lhs1, rhs1, atol=atol, rtol=0., + err_msg='lhs1 and rhs1 do not match') + assert_allclose(lhs2, rhs2, atol=atol, rtol=0., + err_msg='lhs2 and rhs2 do not match') + + +@pytest.mark.parametrize('mtype', ['sy', 'he']) # matrix type +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('lower', (0, 1)) +def test_sy_hetrs(mtype, dtype, lower): + if mtype == 'he' and dtype in REAL_DTYPES: + pytest.skip("hetrs not for real dtypes.") + rng = np.random.default_rng(1723059677121834) + n, nrhs = 20, 5 + if dtype in COMPLEX_DTYPES: + A = (rng.uniform(size=(n, n)) + rng.uniform(size=(n, n))*1j).astype(dtype) + else: + A = rng.uniform(size=(n, n)).astype(dtype) + + A = A + A.T if mtype == 'sy' else A + A.conj().T + b = rng.uniform(size=(n, nrhs)).astype(dtype) + names = f'{mtype}trf', f'{mtype}trf_lwork', f'{mtype}trs' + trf, trf_lwork, trs = get_lapack_funcs(names, dtype=dtype) + lwork = trf_lwork(n, lower=lower) + ldu, ipiv, info = trf(A, lwork=lwork, lower=lower) + assert info == 0 + x, info = trs(a=ldu, ipiv=ipiv, b=b, lower=lower) + assert info == 0 + eps = np.finfo(dtype).eps + assert_allclose(A@x, b, atol=100*n*eps) + + +@pytest.mark.parametrize('mtype', ['sy', 'he']) # matrix type +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('lower', (0, 1)) +def test_sy_he_tri(dtype, lower, mtype): + if mtype == 'he' and dtype in REAL_DTYPES: + pytest.skip("hetri not for real dtypes.") + if sysconfig.get_platform() == 'win-arm64' and dtype in COMPLEX_DTYPES: + pytest.skip("Test segfaulting on win-arm64 in CI, see gh-23133") + + rng = np.random.default_rng(1723059677121834) + n = 20 + A = rng.random((n, n)) + rng.random((n, n))*1j + if np.issubdtype(dtype, np.floating): + A = A.real + A = A.astype(dtype) + A = A + A.T if mtype == 'sy' else A + A.conj().T + names = f'{mtype}trf', f'{mtype}tri' + trf, tri = get_lapack_funcs(names, dtype=dtype) + ldu, ipiv, info = trf(A, lower=lower) + assert info == 0 + A_inv, info = tri(a=ldu, ipiv=ipiv, lower=lower) + assert info == 0 + eps = np.finfo(dtype).eps + ref = np.linalg.inv(A) + if lower: + assert_allclose(np.tril(A_inv), np.tril(ref), atol=100*n*eps) + else: + assert_allclose(np.triu(A_inv), np.triu(ref), atol=100*n*eps) + + +@pytest.mark.parametrize('norm', list('Mm1OoIiFfEe')) +@pytest.mark.parametrize('uplo, m, n', [('U', 5, 10), ('U', 10, 10), + ('L', 10, 5), ('L', 10, 10)]) +@pytest.mark.parametrize('diag', ['N', 'U']) +@pytest.mark.parametrize('dtype', DTYPES) +def test_lantr(norm, uplo, m, n, diag, dtype): + rng = np.random.default_rng(98426598246982456) + A = rng.random(size=(m, n)).astype(dtype) + lantr, lange = get_lapack_funcs(('lantr', 'lange'), (A,)) + res = lantr(norm, A, uplo=uplo, diag=diag) + + # now modify the matrix according to assumptions made by `lantr` + A = np.triu(A) if uplo == 'U' else np.tril(A) + if diag == 'U': + i = np.arange(min(m, n)) + A[i, i] = 1 + ref = lange(norm, A) + + assert_allclose(res, ref, rtol=2e-6) + + +@pytest.mark.parametrize('dtype', DTYPES) +@pytest.mark.parametrize('norm', ['1', 'I', 'O']) +def test_gbcon(dtype, norm): + rng = np.random.default_rng(17273783424) + + # A is of shape n x n with ku/kl super/sub-diagonals + n, ku, kl = 10, 2, 2 + A = rng.random((n, n)) + rng.random((n, n))*1j + # make the condition numbers more interesting + offset = rng.permuted(np.logspace(0, rng.integers(0, 10), n)) + A += offset + if np.issubdtype(dtype, np.floating): + A = A.real + A = A.astype(dtype) + A[np.triu_indices(n, ku + 1)] = 0 + A[np.tril_indices(n, -kl - 1)] = 0 + + # construct banded form + tmp = _to_banded(kl, ku, A) + # add rows required by ?gbtrf + LDAB = 2*kl + ku + 1 + ab = np.zeros((LDAB, n), dtype=dtype) + ab[kl:, :] = tmp + + anorm = np.linalg.norm(A, ord=np.inf if norm == 'I' else 1) + gbcon, gbtrf = get_lapack_funcs(("gbcon", "gbtrf"), (ab,)) + lu_band, ipiv, _ = gbtrf(ab, kl, ku) + res = gbcon(norm=norm, kl=kl, ku=ku, ab=lu_band, ipiv=ipiv, + anorm=anorm)[0] + + gecon, getrf = get_lapack_funcs(('gecon', 'getrf'), (A,)) + lu = getrf(A)[0] + ref = gecon(lu, anorm, norm=norm)[0] + # This is an estimate of reciprocal condition number; we just need order of + # magnitude. + assert_allclose(res, ref, rtol=1) + + +@pytest.mark.parametrize('norm', list('Mm1OoIiFfEe')) +@pytest.mark.parametrize('dtype', DTYPES) +def test_langb(dtype, norm): + rng = np.random.default_rng(17273783424) + + # A is of shape n x n with ku/kl super/sub-diagonals + n, ku, kl = 10, 2, 2 + A = rng.random((n, n)) + rng.random((n, n))*1j + if np.issubdtype(dtype, np.floating): + A = A.real + A = A.astype(dtype) + A[np.triu_indices(n, ku + 1)] = 0 + A[np.tril_indices(n, -kl - 1)] = 0 + ab = _to_banded(kl, ku, A) + + langb, lange = get_lapack_funcs(('langb', 'lange'), (A,)) + ref = lange(norm, A) + res = langb(norm, kl, ku, ab) + assert_allclose(res, ref, rtol=2e-6) + + +@pytest.mark.parametrize('dtype', REAL_DTYPES) +@pytest.mark.parametrize('compute_v', (0, 1)) +def test_stevd(dtype, compute_v): + rng = np.random.default_rng(266474747488348746) + n = 10 + d = rng.random(n, dtype=dtype) + e = rng.random(n - 1, dtype=dtype) + A = np.diag(e, -1) + np.diag(d) + np.diag(e, 1) + ref = np.linalg.eigvalsh(A) + + stevd = get_lapack_funcs('stevd') + U, V, info = stevd(d, e, compute_v=compute_v) + assert info == 0 + assert_allclose(np.sort(U), np.sort(ref)) + if compute_v: + eps = np.finfo(dtype).eps + assert_allclose(V @ np.diag(U) @ V.T, A, atol=eps**0.8) + diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matfuncs.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..de1ccb75819b623ff3ff364a4736ea86c5df930d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matfuncs.py @@ -0,0 +1,1121 @@ +# +# Created by: Pearu Peterson, March 2002 +# +""" Test functions for linalg.matfuncs module + +""" +import functools +import pytest +import warnings + +import numpy as np +from numpy import array, identity, sqrt +from numpy.testing import (assert_array_almost_equal, assert_allclose, assert_, + assert_array_less, assert_array_equal) + +import scipy.linalg +from scipy.linalg import (funm, signm, logm, sqrtm, fractional_matrix_power, + expm, expm_frechet, expm_cond, norm, khatri_rao, + cosm, sinm, tanm, coshm, sinhm, tanhm) + +from scipy.linalg import _matfuncs_inv_ssq +from scipy.linalg._matfuncs import pick_pade_structure +from scipy.linalg._matfuncs_inv_ssq import LogmExactlySingularWarning +import scipy.linalg._expm_frechet +from scipy.linalg import LinAlgWarning +from scipy.optimize import minimize + + +def _get_al_mohy_higham_2012_experiment_1(): + """ + Return the test matrix from Experiment (1) of [1]_. + + References + ---------- + .. [1] Awad H. Al-Mohy and Nicholas J. Higham (2012) + "Improved Inverse Scaling and Squaring Algorithms + for the Matrix Logarithm." + SIAM Journal on Scientific Computing, 34 (4). C152-C169. + ISSN 1095-7197 + + """ + A = np.array([ + [3.2346e-1, 3e4, 3e4, 3e4], + [0, 3.0089e-1, 3e4, 3e4], + [0, 0, 3.2210e-1, 3e4], + [0, 0, 0, 3.0744e-1]], dtype=float) + return A + + +class TestSignM: + + def test_nils(self): + a = array([[29.2, -24.2, 69.5, 49.8, 7.], + [-9.2, 5.2, -18., -16.8, -2.], + [-10., 6., -20., -18., -2.], + [-9.6, 9.6, -25.5, -15.4, -2.], + [9.8, -4.8, 18., 18.2, 2.]]) + cr = array([[11.94933333,-2.24533333,15.31733333,21.65333333,-2.24533333], + [-3.84266667,0.49866667,-4.59066667,-7.18666667,0.49866667], + [-4.08,0.56,-4.92,-7.6,0.56], + [-4.03466667,1.04266667,-5.59866667,-7.02666667,1.04266667], + [4.15733333,-0.50133333,4.90933333,7.81333333,-0.50133333]]) + r = signm(a) + assert_array_almost_equal(r,cr) + + def test_defective1(self): + a = array([[0.0,1,0,0],[1,0,1,0],[0,0,0,1],[0,0,1,0]]) + signm(a) + #XXX: what would be the correct result? + + def test_defective2(self): + a = array(( + [29.2,-24.2,69.5,49.8,7.0], + [-9.2,5.2,-18.0,-16.8,-2.0], + [-10.0,6.0,-20.0,-18.0,-2.0], + [-9.6,9.6,-25.5,-15.4,-2.0], + [9.8,-4.8,18.0,18.2,2.0])) + signm(a) + #XXX: what would be the correct result? + + def test_defective3(self): + a = array([[-2., 25., 0., 0., 0., 0., 0.], + [0., -3., 10., 3., 3., 3., 0.], + [0., 0., 2., 15., 3., 3., 0.], + [0., 0., 0., 0., 15., 3., 0.], + [0., 0., 0., 0., 3., 10., 0.], + [0., 0., 0., 0., 0., -2., 25.], + [0., 0., 0., 0., 0., 0., -3.]]) + signm(a) + #XXX: what would be the correct result? + + +class TestLogM: + @pytest.mark.filterwarnings("ignore:.*inaccurate.*:RuntimeWarning") + def test_nils(self): + a = array([[-2., 25., 0., 0., 0., 0., 0.], + [0., -3., 10., 3., 3., 3., 0.], + [0., 0., 2., 15., 3., 3., 0.], + [0., 0., 0., 0., 15., 3., 0.], + [0., 0., 0., 0., 3., 10., 0.], + [0., 0., 0., 0., 0., -2., 25.], + [0., 0., 0., 0., 0., 0., -3.]]) + m = (identity(7)*3.1+0j)-a + logm(m) + #XXX: what would be the correct result? + + @pytest.mark.filterwarnings("ignore:.*inaccurate.*:RuntimeWarning") + def test_al_mohy_higham_2012_experiment_1_logm(self): + # The logm completes the round trip successfully. + # Note that the expm leg of the round trip is badly conditioned. + A = _get_al_mohy_higham_2012_experiment_1() + A_logm = logm(A) + A_round_trip = expm(A_logm) + assert_allclose(A_round_trip, A, rtol=5e-5, atol=1e-14) + + def test_al_mohy_higham_2012_experiment_1_funm_log(self): + # The raw funm with np.log does not complete the round trip. + # Note that the expm leg of the round trip is badly conditioned. + A = _get_al_mohy_higham_2012_experiment_1() + A_funm_log = funm(A, np.log) + A_round_trip = expm(A_funm_log) + assert_(not np.allclose(A_round_trip, A, rtol=1e-5, atol=1e-14)) + + def test_round_trip_random_float(self): + rng = np.random.default_rng(1738098768840254) + for n in range(1, 6): + M_unscaled = rng.uniform(size=(n, n)) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + + # Eigenvalues are related to the branch cut. + W = np.linalg.eigvals(M) + err_msg = f'M:{M} eivals:{W}' + + # Check sqrtm round trip because it is used within logm. + M_sqrtm = sqrtm(M) + M_sqrtm_round_trip = M_sqrtm @ M_sqrtm + assert_allclose(M_sqrtm_round_trip, M) + + # Check logm round trip. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + + M_logm = logm(M) + M_logm_round_trip = expm(M_logm) + assert_allclose(M_logm_round_trip, M, err_msg=err_msg) + + def test_round_trip_random_complex(self): + rng = np.random.default_rng(1738098768840254) + for n in range(1, 6): + M_unscaled = (rng.standard_normal((n, n)) + + 1j*rng.standard_normal((n, n))) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + M_logm = logm(M) + M_round_trip = expm(M_logm) + assert_allclose(M_round_trip, M) + + def test_logm_type_preservation_and_conversion(self): + # The logm matrix function should preserve the type of a matrix + # whose eigenvalues are positive with zero imaginary part. + # Test this preservation for variously structured matrices. + complex_dtype_chars = ('F', 'D', 'G') + for matrix_as_list in ( + [[1, 0], [0, 1]], + [[1, 0], [1, 1]], + [[2, 1], [1, 1]], + [[2, 3], [1, 2]]): + + # check that the spectrum has the expected properties + W = scipy.linalg.eigvals(matrix_as_list) + assert_(not any(w.imag or w.real < 0 for w in W)) + + # check float type preservation + A = np.array(matrix_as_list, dtype=float) + A_logm = logm(A) + assert_(A_logm.dtype.char not in complex_dtype_chars) + + # check complex type preservation + A = np.array(matrix_as_list, dtype=complex) + A_logm = logm(A) + assert_(A_logm.dtype.char in complex_dtype_chars) + + # check float->complex type conversion for the matrix negation + A = -np.array(matrix_as_list, dtype=float) + A_logm = logm(A) + assert_(A_logm.dtype.char in complex_dtype_chars) + + def test_complex_spectrum_real_logm(self): + # This matrix has complex eigenvalues and real logm. + # Its output dtype depends on its input dtype. + M = [[1, 1, 2], [2, 1, 1], [1, 2, 1]] + for dt in float, complex: + X = np.array(M, dtype=dt) + w = scipy.linalg.eigvals(X) + assert_(1e-2 < np.absolute(w.imag).sum()) + Y = logm(X) + assert_(np.issubdtype(Y.dtype, np.inexact)) + assert_allclose(expm(Y), X) + + def test_real_mixed_sign_spectrum(self): + # These matrices have real eigenvalues with mixed signs. + # The output logm dtype is complex, regardless of input dtype. + for M in ( + [[1, 0], [0, -1]], + [[0, 1], [1, 0]]): + for dt in float, complex: + A = np.array(M, dtype=dt) + A_logm, info = logm(A) + assert_(np.issubdtype(A_logm.dtype, np.complexfloating)) + + def test_exactly_singular(self): + A = np.array([[0, 0], [1j, 1j]]) + B = np.asarray([[1, 1], [0, 0]]) + for M in A, A.T, B, B.T: + with pytest.warns(_matfuncs_inv_ssq.LogmExactlySingularWarning): + L = logm(M) + E = expm(L) + assert_allclose(E, M, atol=1e-14) + + def test_nearly_singular(self): + M = np.array([[1e-100]]) + with pytest.warns(_matfuncs_inv_ssq.LogmNearlySingularWarning): + L = logm(M) + E = expm(L) + assert_allclose(E, M, atol=1e-14) + + def test_opposite_sign_complex_eigenvalues(self): + # See gh-6113 + E = [[0, 1], [-1, 0]] + L = [[0, np.pi*0.5], [-np.pi*0.5, 0]] + assert_allclose(expm(L), E, atol=1e-14) + assert_allclose(logm(E), L, atol=1e-14) + E = [[1j, 4], [0, -1j]] + L = [[1j*np.pi*0.5, 2*np.pi], [0, -1j*np.pi*0.5]] + assert_allclose(expm(L), E, atol=1e-14) + assert_allclose(logm(E), L, atol=1e-14) + E = [[1j, 0], [0, -1j]] + L = [[1j*np.pi*0.5, 0], [0, -1j*np.pi*0.5]] + assert_allclose(expm(L), E, atol=1e-14) + assert_allclose(logm(E), L, atol=1e-14) + + def test_readonly(self): + n = 5 + a = np.ones((n, n)) + np.identity(n) + a.flags.writeable = False + logm(a) + + @pytest.mark.xfail(reason="ValueError: attempt to get argmax of an empty sequence") + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + log_a = logm(a) + a0 = np.eye(2, dtype=dt) + log_a0 = logm(a0) + + assert log_a.shape == (0, 0) + assert log_a.dtype == log_a0.dtype + + @pytest.mark.parametrize('dtype', [int, float, np.float32, complex, np.complex64]) + def test_no_ZeroDivisionError(self, dtype): + # gh-17136 reported inconsistent behavior in `logm` depending on input dtype: + # sometimes it raised an error, and sometimes it printed a warning message. + # check that this is resolved and that the warning is emitted properly. + with (pytest.warns(RuntimeWarning, match="logm result may be inaccurate"), + pytest.warns(LogmExactlySingularWarning)): + logm(np.zeros((2, 2), dtype=dtype)) + + +class TestSqrtM: + + def test_round_trip_random_float(self): + rng = np.random.default_rng(1738151906092735) + for n in range(1, 6): + M_unscaled = rng.standard_normal((n, n)) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + M_sqrtm = sqrtm(M) + M_sqrtm_round_trip = M_sqrtm.dot(M_sqrtm) + assert_allclose(M_sqrtm_round_trip, M) + + def test_round_trip_random_complex(self): + rng = np.random.default_rng(1738151906092736) + for n in range(1, 6): + M_unscaled = (rng.standard_normal((n, n)) + + 1j * rng.standard_normal((n, n))) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + M_sqrtm = sqrtm(M) + M_sqrtm_round_trip = M_sqrtm.dot(M_sqrtm) + assert_allclose(M_sqrtm_round_trip, M) + + def test_bad(self): + # See https://web.archive.org/web/20051220232650/http://www.maths.man.ac.uk/~nareports/narep336.ps.gz + e = 2**-5 + se = sqrt(e) + a = array([[1.0,0,0,1], + [0,e,0,0], + [0,0,e,0], + [0,0,0,1]]) + sa = array([[1,0,0,0.5], + [0,se,0,0], + [0,0,se,0], + [0,0,0,1]]) + assert_array_almost_equal(sa @ sa, a) + # Check default sqrtm. + esa = sqrtm(a) + assert_array_almost_equal(esa @ esa, a) + + def test_sqrtm_type_preservation_and_conversion(self): + # The sqrtm matrix function should preserve the type of a matrix + # whose eigenvalues are nonnegative with zero imaginary part. + # Test this preservation for variously structured matrices. + complex_dtype_chars = ('F', 'D', 'G') + for matrix_as_list in ( + [[1, 0], [0, 1]], + [[1, 0], [1, 1]], + [[2, 1], [1, 1]], + [[2, 3], [1, 2]], + [[1, 1], [1, 1]]): + + # check that the spectrum has the expected properties + W = scipy.linalg.eigvals(matrix_as_list) + assert_(not any(w.imag or w.real < 0 for w in W)) + + # Last test matrix is singular so suppress the warning + with warnings.catch_warnings(): + warnings.simplefilter("ignore", LinAlgWarning) + + # check float type preservation + A = np.array(matrix_as_list, dtype=float) + A_sqrtm = sqrtm(A) + assert_(A_sqrtm.dtype.char not in complex_dtype_chars) + + # check complex type preservation + A = np.array(matrix_as_list, dtype=complex) + A_sqrtm = sqrtm(A) + assert_(A_sqrtm.dtype.char in complex_dtype_chars) + + # check float->complex type conversion for the matrix negation + A = -np.array(matrix_as_list, dtype=float) + A_sqrtm = sqrtm(A) + assert_(A_sqrtm.dtype.char in complex_dtype_chars) + + def test_sqrtm_type_conversion_mixed_sign_or_complex_spectrum(self): + complex_dtype_chars = ('F', 'D', 'G') + for matrix_as_list in ( + [[1, 0], [0, -1]], + [[0, 1], [1, 0]], + [[0, 1, 0], [0, 0, -1], [1, 0, 0]]): + + # check that the spectrum has the expected properties + W = scipy.linalg.eigvals(matrix_as_list) + assert_(any(w.imag or w.real < 0 for w in W)) + + # check complex->complex + A = np.array(matrix_as_list, dtype=complex) + A_sqrtm = sqrtm(A) + assert_(A_sqrtm.dtype.char in complex_dtype_chars) + + # check float->complex + A = np.array(matrix_as_list, dtype=float) + A_sqrtm = sqrtm(A) + assert_(A_sqrtm.dtype.char in complex_dtype_chars) + + def test_al_mohy_higham_2012_experiment_1(self): + # Matrix square root of a tricky upper triangular matrix. + A = _get_al_mohy_higham_2012_experiment_1() + A_sqrtm = sqrtm(A) + A_round_trip = A_sqrtm @ A_sqrtm + assert_allclose(A_round_trip, A, rtol=1e-5) + assert_allclose(np.tril(A_round_trip), np.tril(A)) + + def test_strict_upper_triangular(self): + # This matrix has no square root but upper triangular hence upper + # triangle will be filled with junk values. + for dt in int, float: + A = np.array([ + [0, 3, 0, 0], + [0, 0, 3, 0], + [0, 0, 0, 3], + [0, 0, 0, 0]], dtype=dt) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", LinAlgWarning) + + A_sqrtm = sqrtm(A) + assert_allclose(np.tril(A_sqrtm), np.zeros((4, 4))) + assert np.isnan(A_sqrtm).any() + assert np.isinf(A_sqrtm).any() + + # Future edit: This squareroot is not possible to find algorithmically + # with the current methods. Now sqrtm docstring has another example of + # such matrix whose squareroot is not a polynomial in it. Hence no need + # to test it here. + """ + def test_weird_matrix(self): + # The square root of matrix B exists. + for dt in int, float: + A = np.array([ + [0, 0, 1], + [0, 0, 0], + [0, 1, 0]], dtype=dt) + B = np.array([ + [0, 1, 0], + [0, 0, 0], + [0, 0, 0]], dtype=dt) + assert_array_equal(B, A @ A) + + # But scipy sqrtm is not clever enough to find it. + B_sqrtm, info = sqrtm(B, disp=False) + assert_(np.isnan(B_sqrtm).all()) + """ + + def test_opposite_sign_complex_eigenvalues(self): + M = [[2j, 4], [0, -2j]] + R = [[1+1j, 2], [0, 1-1j]] + assert_allclose(np.dot(R, R), M, atol=1e-14) + assert_allclose(sqrtm(M), R, atol=1e-14) + + def test_gh4866(self): + M = np.array([[1, 0, 0, 1], + [0, 0, 0, 0], + [0, 0, 0, 0], + [1, 0, 0, 1]]) + R = np.array([[sqrt(0.5), 0, 0, sqrt(0.5)], + [0, 0, 0, 0], + [0, 0, 0, 0], + [sqrt(0.5), 0, 0, sqrt(0.5)]]) + assert_allclose(R @ R, M, atol=1e-14) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", LinAlgWarning) + + assert_allclose(sqrtm(M), R, atol=1e-14) + + def test_gh5336(self): + M = np.diag([2, 1, 0]) + R = np.diag([sqrt(2), 1, 0]) + assert_allclose(R @ R, M, atol=1e-14) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=LinAlgWarning) + assert_allclose(sqrtm(M), R, atol=1e-14) + + def test_gh7839(self): + M = np.zeros((2, 2)) + R = np.zeros((2, 2)) + # Catch and silence LinAlgWarning + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=LinAlgWarning) + + assert_allclose(sqrtm(M), R, atol=1e-14) + + def test_gh17918(self): + M = np.empty((19, 19)) + M.fill(0.94) + np.fill_diagonal(M, 1) + assert np.isrealobj(sqrtm(M)) + + def test_gh23278(self): + M = np.array([[1., 0., 0.], [0, 1, -1j], [0, 1j, 2]]) + sq = sqrtm(M) + assert_allclose(sq @ sq, M, atol=1e-14) + sq = sqrtm(M.astype(np.complex64)) + assert_allclose(sq @ sq, M, atol=1e-6) + + def test_data_size_preservation_uint_in_float_out(self): + M = np.eye(10, dtype=np.uint8) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.uint16) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.uint32) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.uint64) + assert sqrtm(M).dtype == np.float64 + + def test_data_size_preservation_int_in_float_out(self): + M = np.eye(10, dtype=np.int8) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.int16) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.int32) + assert sqrtm(M).dtype == np.float64 + M = np.eye(10, dtype=np.int64) + assert sqrtm(M).dtype == np.float64 + + def test_data_size_preservation_int_in_comp_out(self): + M = np.array([[2, 4], [0, -2]], dtype=np.int8) + assert sqrtm(M).dtype == np.complex128 + M = np.array([[2, 4], [0, -2]], dtype=np.int16) + assert sqrtm(M).dtype == np.complex128 + M = np.array([[2, 4], [0, -2]], dtype=np.int32) + assert sqrtm(M).dtype == np.complex128 + M = np.array([[2, 4], [0, -2]], dtype=np.int64) + assert sqrtm(M).dtype == np.complex128 + + def test_data_size_preservation_float_in_float_out(self): + M = np.eye(10, dtype=np.float16) + assert sqrtm(M).dtype == np.float32 + M = np.eye(10, dtype=np.float32) + assert sqrtm(M).dtype == np.float32 + M = np.eye(10, dtype=np.float64) + assert sqrtm(M).dtype == np.float64 + if hasattr(np, 'float128'): + M = np.eye(10, dtype=np.float128) + assert sqrtm(M).dtype == np.float64 + + def test_data_size_preservation_float_in_comp_out(self): + M = np.array([[2, 4], [0, -2]], dtype=np.float16) + assert sqrtm(M).dtype == np.complex64 + M = np.array([[2, 4], [0, -2]], dtype=np.float32) + assert sqrtm(M).dtype == np.complex64 + M = np.array([[2, 4], [0, -2]], dtype=np.float64) + assert sqrtm(M).dtype == np.complex128 + if hasattr(np, 'float128') and hasattr(np, 'complex256'): + M = np.array([[2, 4], [0, -2]], dtype=np.float128) + assert sqrtm(M).dtype == np.complex128 + + def test_data_size_preservation_comp_in_comp_out(self): + M = np.array([[2j, 4], [0, -2j]], dtype=np.complex64) + assert sqrtm(M).dtype == np.complex64 + M = np.array([[2j, 4], [0, -2j]], dtype=np.complex128) + assert sqrtm(M).dtype == np.complex128 + if hasattr(np, 'complex256'): + M = np.array([[2j, 4], [0, -2j]], dtype=np.complex256) + assert sqrtm(M).dtype == np.complex128 + + @pytest.mark.parametrize('dt', [int, float, np.float32, complex, np.complex64]) + def test_empty(self, dt): + a = np.empty((0, 0), dtype=dt) + s = sqrtm(a) + a0 = np.eye(2, dtype=dt) + s0 = sqrtm(a0) + + assert s.shape == (0, 0) + assert s.dtype == s0.dtype + + def test_cf_noncontig_nd_inputs(self): + # Check that non-contiguous arrays are handled correctly. + # Generate an L, U pair for invertible random matrix. + rng = np.random.default_rng(1738151906092737) + n = 13 + A = rng.uniform(size=(3, 2*n, 2*n)) + L, U = np.tril(A, k=-1) + np.eye(2*n), np.triu(A) + A = L @ U + # Create strided views of 3D array. + A_noncontig_c = A[:, ::2, ::2] + A_noncontig_f = np.asfortranarray(A)[:, 1::2, 1::2] + assert_allclose(sqrtm(A[:, ::2, ::2]), sqrtm(A_noncontig_c)) + assert_allclose(sqrtm(A[:, 1::2, 1::2]), sqrtm(A_noncontig_f)) + + def test_empty_sizes(self): + A = np.empty(shape=[4, 0, 5, 5], dtype=float) + assert_array_equal(sqrtm(A), A) + + def test_negative_strides(self): + rng = np.random.default_rng(1738151906092738) + A = rng.uniform(size=(3, 5, 5)) + A_negneg_orig = A[:, ::-1, ::-1] + A_negneg_copy = A[:, ::-1, ::-1].copy() + assert_allclose(sqrtm(A_negneg_orig), sqrtm(A_negneg_copy)) + + A_posneg_orig = A[:, :, ::-1] + A_posneg_copy = A[:, :, ::-1].copy() + assert_allclose(sqrtm(A_posneg_orig), sqrtm(A_posneg_copy)) + + A_negpos_orig = A[:, ::-1, :] + A_negpos_copy = A[:, ::-1, :].copy() + assert_allclose(sqrtm(A_negpos_orig), sqrtm(A_negpos_copy)) + + +class TestFractionalMatrixPower: + def test_round_trip_random_complex(self): + rng = np.random.default_rng(1234) + for p in range(1, 5): + for n in range(1, 5): + M_unscaled = (rng.standard_normal((n, n)) + + 1j * rng.standard_normal((n, n))) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + M_root = fractional_matrix_power(M, 1/p) + M_round_trip = np.linalg.matrix_power(M_root, p) + assert_allclose(M_round_trip, M) + + def test_round_trip_random_float(self): + # This test is more annoying because it can hit the branch cut; + # this happens when the matrix has an eigenvalue + # with no imaginary component and with a real negative component, + # and it means that the principal branch does not exist. + rng = np.random.default_rng(1234) + for p in range(1, 5): + for n in range(1, 5): + M_unscaled = rng.standard_normal((n, n)) + for scale in np.logspace(-4, 4, 9): + M = M_unscaled * scale + M_root = fractional_matrix_power(M, 1/p) + M_round_trip = np.linalg.matrix_power(M_root, p) + assert_allclose(M_round_trip, M) + + def test_larger_abs_fractional_matrix_powers(self): + rng = np.random.default_rng(1234) + for n in (2, 3, 5): + for i in range(10): + M = rng.standard_normal((n, n)) + 1j * rng.standard_normal((n, n)) + M_one_fifth = fractional_matrix_power(M, 0.2) + # Test the round trip. + M_round_trip = np.linalg.matrix_power(M_one_fifth, 5) + assert_allclose(M, M_round_trip) + # Test a large abs fractional power. + X = fractional_matrix_power(M, -5.4) + Y = np.linalg.matrix_power(M_one_fifth, -27) + assert_allclose(X, Y) + # Test another large abs fractional power. + X = fractional_matrix_power(M, 3.8) + Y = np.linalg.matrix_power(M_one_fifth, 19) + assert_allclose(X, Y) + + def test_random_matrices_and_powers(self): + # Each independent iteration of this fuzz test picks random parameters. + # It tries to hit some edge cases. + rng = np.random.default_rng(1726500458620605) + nsamples = 20 + for i in range(nsamples): + # Sample a matrix size and a random real power. + n = rng.integers(1, 5) + p = rng.random() + + # Sample a random real or complex matrix. + matrix_scale = np.exp(rng.integers(-4, 5)) + A = rng.random(size=[n, n]) + if [True, False][rng.choice(2)]: + A = A + 1j * rng.random(size=[n, n]) + A = A * matrix_scale + + # Check a couple of analytically equivalent ways + # to compute the fractional matrix power. + # These can be compared because they both use the principal branch. + A_power = fractional_matrix_power(A, p) + A_logm = logm(A) + A_power_expm_logm = expm(A_logm * p) + assert_allclose(A_power, A_power_expm_logm) + + def test_al_mohy_higham_2012_experiment_1(self): + # Fractional powers of a tricky upper triangular matrix. + A = _get_al_mohy_higham_2012_experiment_1() + + # Test remainder matrix power. + A_funm_sqrt = funm(A, np.sqrt) + A_sqrtm = sqrtm(A) + A_rem_power = _matfuncs_inv_ssq._remainder_matrix_power(A, 0.5) + A_power = fractional_matrix_power(A, 0.5) + assert_allclose(A_rem_power, A_power, rtol=1e-11) + assert_allclose(A_sqrtm, A_power) + assert_allclose(A_sqrtm, A_funm_sqrt) + + # Test more fractional powers. + for p in (1/2, 5/3): + A_power = fractional_matrix_power(A, p) + A_round_trip = fractional_matrix_power(A_power, 1/p) + assert_allclose(A_round_trip, A, rtol=1e-2) + assert_allclose(np.tril(A_round_trip, 1), np.tril(A, 1)) + + def test_briggs_helper_function(self): + rng = np.random.default_rng(1234) + for a in rng.standard_normal(10) + 1j * rng.standard_normal(10): + for k in range(5): + x_observed = _matfuncs_inv_ssq._briggs_helper_function(a, k) + x_expected = a ** np.exp2(-k) - 1 + assert_allclose(x_observed, x_expected) + + def test_type_preservation_and_conversion(self): + # The fractional_matrix_power matrix function should preserve + # the type of a matrix whose eigenvalues + # are positive with zero imaginary part. + # Test this preservation for variously structured matrices. + complex_dtype_chars = ('F', 'D', 'G') + for matrix_as_list in ( + [[1, 0], [0, 1]], + [[1, 0], [1, 1]], + [[2, 1], [1, 1]], + [[2, 3], [1, 2]]): + + # check that the spectrum has the expected properties + W = scipy.linalg.eigvals(matrix_as_list) + assert_(not any(w.imag or w.real < 0 for w in W)) + + # Check various positive and negative powers + # with absolute values bigger and smaller than 1. + for p in (-2.4, -0.9, 0.2, 3.3): + + # check float type preservation + A = np.array(matrix_as_list, dtype=float) + A_power = fractional_matrix_power(A, p) + assert_(A_power.dtype.char not in complex_dtype_chars) + + # check complex type preservation + A = np.array(matrix_as_list, dtype=complex) + A_power = fractional_matrix_power(A, p) + assert_(A_power.dtype.char in complex_dtype_chars) + + # check float->complex for the matrix negation + A = -np.array(matrix_as_list, dtype=float) + A_power = fractional_matrix_power(A, p) + assert_(A_power.dtype.char in complex_dtype_chars) + + def test_type_conversion_mixed_sign_or_complex_spectrum(self): + complex_dtype_chars = ('F', 'D', 'G') + for matrix_as_list in ( + [[1, 0], [0, -1]], + [[0, 1], [1, 0]], + [[0, 1, 0], [0, 0, 1], [1, 0, 0]]): + + # check that the spectrum has the expected properties + W = scipy.linalg.eigvals(matrix_as_list) + assert_(any(w.imag or w.real < 0 for w in W)) + + # Check various positive and negative powers + # with absolute values bigger and smaller than 1. + for p in (-2.4, -0.9, 0.2, 3.3): + + # check complex->complex + A = np.array(matrix_as_list, dtype=complex) + A_power = fractional_matrix_power(A, p) + assert_(A_power.dtype.char in complex_dtype_chars) + + # check float->complex + A = np.array(matrix_as_list, dtype=float) + A_power = fractional_matrix_power(A, p) + assert_(A_power.dtype.char in complex_dtype_chars) + + @pytest.mark.xfail(reason='Too unstable across LAPACKs.') + def test_singular(self): + # Negative fractional powers do not work with singular matrices. + for matrix_as_list in ( + [[0, 0], [0, 0]], + [[1, 1], [1, 1]], + [[1, 2], [3, 6]], + [[0, 0, 0], [0, 1, 1], [0, -1, 1]]): + + # Check fractional powers both for float and for complex types. + for newtype in (float, complex): + A = np.array(matrix_as_list, dtype=newtype) + for p in (-0.7, -0.9, -2.4, -1.3): + A_power = fractional_matrix_power(A, p) + assert_(np.isnan(A_power).all()) + for p in (0.2, 1.43): + A_power = fractional_matrix_power(A, p) + A_round_trip = fractional_matrix_power(A_power, 1/p) + assert_allclose(A_round_trip, A) + + def test_opposite_sign_complex_eigenvalues(self): + M = [[2j, 4], [0, -2j]] + R = [[1+1j, 2], [0, 1-1j]] + assert_allclose(np.dot(R, R), M, atol=1e-14) + assert_allclose(fractional_matrix_power(M, 0.5), R, atol=1e-14) + + +class TestExpM: + def test_zero(self): + a = array([[0.,0],[0,0]]) + assert_array_almost_equal(expm(a),[[1,0],[0,1]]) + + def test_single_elt(self): + elt = expm(1) + assert_allclose(elt, np.array([[np.e]])) + + @pytest.mark.parametrize('func', [expm, cosm, sinm, tanm, coshm, sinhm, tanhm]) + @pytest.mark.parametrize('dt',[int, float, np.float32, complex, np.complex64]) + @pytest.mark.parametrize('shape', [(0, 0), (1, 1)]) + def test_small_empty_matrix_input(self, func, dt, shape): + # regression test for gh-11082 / gh-20372 - test behavior of expm + # and related functions for small and zero-sized arrays. + A = np.zeros(shape, dtype=dt) + A0 = np.zeros((10, 10), dtype=dt) + result = func(A) + result0 = func(A0) + assert result.shape == shape + assert result.dtype == result0.dtype + + def test_2x2_input(self): + E = np.e + a = array([[1, 4], [1, 1]]) + aa = (E**4 + 1)/(2*E) + bb = (E**4 - 1)/E + assert_allclose(expm(a), array([[aa, bb], [bb/4, aa]])) + assert expm(a.astype(np.complex64)).dtype.char == 'F' + assert expm(a.astype(np.float32)).dtype.char == 'f' + + def test_nx2x2_input(self): + E = np.e + # These are integer matrices with integer eigenvalues + a = np.array([[[1, 4], [1, 1]], + [[1, 3], [1, -1]], + [[1, 3], [4, 5]], + [[1, 3], [5, 3]], + [[4, 5], [-3, -4]]], order='F') + # Exact results are computed symbolically + a_res = np.array([ + [[(E**4+1)/(2*E), (E**4-1)/E], + [(E**4-1)/4/E, (E**4+1)/(2*E)]], + [[1/(4*E**2)+(3*E**2)/4, (3*E**2)/4-3/(4*E**2)], + [E**2/4-1/(4*E**2), 3/(4*E**2)+E**2/4]], + [[3/(4*E)+E**7/4, -3/(8*E)+(3*E**7)/8], + [-1/(2*E)+E**7/2, 1/(4*E)+(3*E**7)/4]], + [[5/(8*E**2)+(3*E**6)/8, -3/(8*E**2)+(3*E**6)/8], + [-5/(8*E**2)+(5*E**6)/8, 3/(8*E**2)+(5*E**6)/8]], + [[-3/(2*E)+(5*E)/2, -5/(2*E)+(5*E)/2], + [3/(2*E)-(3*E)/2, 5/(2*E)-(3*E)/2]] + ]) + assert_allclose(expm(a), a_res) + + def test_readonly(self): + n = 7 + a = np.ones((n, n)) + a.flags.writeable = False + expm(a) + + @pytest.mark.fail_slow(5) + def test_gh18086(self): + A = np.zeros((400, 400), dtype=float) + rng = np.random.default_rng(100) + i = rng.integers(0, 399, 500) + j = rng.integers(0, 399, 500) + A[i, j] = rng.random(500) + # Problem appears when m = 9 + Am = np.empty((5, 400, 400), dtype=float) + Am[0] = A.copy() + m, s = pick_pade_structure(Am) + assert m == 9 + # Check that result is accurate + first_res = expm(A) + np.testing.assert_array_almost_equal(logm(first_res), A) + # Check that result is consistent + for i in range(5): + next_res = expm(A) + np.testing.assert_array_almost_equal(first_res, next_res) + + +class TestExpmFrechet: + + def test_expm_frechet(self): + # a test of the basic functionality + M = np.array([ + [1, 2, 3, 4], + [5, 6, 7, 8], + [0, 0, 1, 2], + [0, 0, 5, 6], + ], dtype=float) + A = np.array([ + [1, 2], + [5, 6], + ], dtype=float) + E = np.array([ + [3, 4], + [7, 8], + ], dtype=float) + expected_expm = scipy.linalg.expm(A) + expected_frechet = scipy.linalg.expm(M)[:2, 2:] + for kwargs in ({}, {'method':'SPS'}, {'method':'blockEnlarge'}): + observed_expm, observed_frechet = expm_frechet(A, E, **kwargs) + assert_allclose(expected_expm, observed_expm) + assert_allclose(expected_frechet, observed_frechet) + + def test_small_norm_expm_frechet(self): + # methodically test matrices with a range of norms, for better coverage + M_original = np.array([ + [1, 2, 3, 4], + [5, 6, 7, 8], + [0, 0, 1, 2], + [0, 0, 5, 6], + ], dtype=float) + A_original = np.array([ + [1, 2], + [5, 6], + ], dtype=float) + E_original = np.array([ + [3, 4], + [7, 8], + ], dtype=float) + A_original_norm_1 = scipy.linalg.norm(A_original, 1) + selected_m_list = [1, 3, 5, 7, 9, 11, 13, 15] + m_neighbor_pairs = zip(selected_m_list[:-1], selected_m_list[1:]) + for ma, mb in m_neighbor_pairs: + ell_a = scipy.linalg._expm_frechet.ell_table_61[ma] + ell_b = scipy.linalg._expm_frechet.ell_table_61[mb] + target_norm_1 = 0.5 * (ell_a + ell_b) + scale = target_norm_1 / A_original_norm_1 + M = scale * M_original + A = scale * A_original + E = scale * E_original + expected_expm = scipy.linalg.expm(A) + expected_frechet = scipy.linalg.expm(M)[:2, 2:] + observed_expm, observed_frechet = expm_frechet(A, E) + assert_allclose(expected_expm, observed_expm) + assert_allclose(expected_frechet, observed_frechet) + + def test_fuzz(self): + rng = np.random.default_rng(1726500908359153) + # try a bunch of crazy inputs + rfuncs = ( + rng.uniform, + rng.normal, + rng.standard_cauchy, + rng.exponential) + ntests = 100 + for i in range(ntests): + rfunc = rfuncs[rng.choice(4)] + target_norm_1 = rng.exponential() + n = rng.integers(2, 16) + A_original = rfunc(size=(n,n)) + E_original = rfunc(size=(n,n)) + A_original_norm_1 = scipy.linalg.norm(A_original, 1) + scale = target_norm_1 / A_original_norm_1 + A = scale * A_original + E = scale * E_original + M = np.vstack([ + np.hstack([A, E]), + np.hstack([np.zeros_like(A), A])]) + expected_expm = scipy.linalg.expm(A) + expected_frechet = scipy.linalg.expm(M)[:n, n:] + observed_expm, observed_frechet = expm_frechet(A, E) + assert_allclose(expected_expm, observed_expm, atol=5e-8) + assert_allclose(expected_frechet, observed_frechet, atol=1e-7) + + def test_problematic_matrix(self): + # this test case uncovered a bug which has since been fixed + A = np.array([ + [1.50591997, 1.93537998], + [0.41203263, 0.23443516], + ], dtype=float) + E = np.array([ + [1.87864034, 2.07055038], + [1.34102727, 0.67341123], + ], dtype=float) + scipy.linalg.norm(A, 1) + sps_expm, sps_frechet = expm_frechet( + A, E, method='SPS') + blockEnlarge_expm, blockEnlarge_frechet = expm_frechet( + A, E, method='blockEnlarge') + assert_allclose(sps_expm, blockEnlarge_expm) + assert_allclose(sps_frechet, blockEnlarge_frechet) + + @pytest.mark.slow + @pytest.mark.skip(reason='this test is deliberately slow') + def test_medium_matrix(self): + # profile this to see the speed difference + n = 1000 + rng = np.random.default_rng(1234) + A = rng.exponential(size=(n, n)) + E = rng.exponential(size=(n, n)) + sps_expm, sps_frechet = expm_frechet( + A, E, method='SPS') + blockEnlarge_expm, blockEnlarge_frechet = expm_frechet( + A, E, method='blockEnlarge') + assert_allclose(sps_expm, blockEnlarge_expm) + assert_allclose(sps_frechet, blockEnlarge_frechet) + + +def _help_expm_cond_search(A, A_norm, X, X_norm, eps, p): + p = np.reshape(p, A.shape) + p_norm = norm(p) + perturbation = eps * p * (A_norm / p_norm) + X_prime = expm(A + perturbation) + scaled_relative_error = norm(X_prime - X) / (X_norm * eps) + return -scaled_relative_error + + +def _normalized_like(A, B): + return A * (scipy.linalg.norm(B) / scipy.linalg.norm(A)) + + +def _relative_error(f, A, perturbation): + X = f(A) + X_prime = f(A + perturbation) + return norm(X_prime - X) / norm(X) + + +class TestExpmConditionNumber: + def test_expm_cond_smoke(self): + rng = np.random.default_rng(1234) + for n in range(1, 4): + A = rng.standard_normal((n, n)) + kappa = expm_cond(A) + assert_array_less(0, kappa) + + def test_expm_bad_condition_number(self): + A = np.array([ + [-1.128679820, 9.614183771e4, -4.524855739e9, 2.924969411e14], + [0, -1.201010529, 9.634696872e4, -4.681048289e9], + [0, 0, -1.132893222, 9.532491830e4], + [0, 0, 0, -1.179475332], + ]) + kappa = expm_cond(A) + assert_array_less(1e36, kappa) + + def test_univariate(self): + rng = np.random.default_rng(1234) + for x in np.linspace(-5, 5, num=11): + A = np.array([[x]]) + assert_allclose(expm_cond(A), abs(x)) + for x in np.logspace(-2, 2, num=11): + A = np.array([[x]]) + assert_allclose(expm_cond(A), abs(x)) + for i in range(10): + A = rng.standard_normal((1, 1)) + assert_allclose(expm_cond(A), np.absolute(A)[0, 0]) + + @pytest.mark.slow + def test_expm_cond_fuzz(self): + rng = np.random.RandomState(12345) + eps = 1e-5 + nsamples = 10 + for i in range(nsamples): + n = rng.randint(2, 5) + A = rng.randn(n, n) + A_norm = scipy.linalg.norm(A) + X = expm(A) + X_norm = scipy.linalg.norm(X) + kappa = expm_cond(A) + + # Look for the small perturbation that gives the greatest + # relative error. + f = functools.partial(_help_expm_cond_search, + A, A_norm, X, X_norm, eps) + guess = np.ones(n*n) + out = minimize(f, guess, method='L-BFGS-B') + xopt = out.x + yopt = f(xopt) + p_best = eps * _normalized_like(np.reshape(xopt, A.shape), A) + p_best_relerr = _relative_error(expm, A, p_best) + assert_allclose(p_best_relerr, -yopt * eps) + + # Check that the identified perturbation indeed gives greater + # relative error than random perturbations with similar norms. + for j in range(5): + p_rand = eps * _normalized_like(rng.randn(*A.shape), A) + assert_allclose(norm(p_best), norm(p_rand)) + p_rand_relerr = _relative_error(expm, A, p_rand) + assert_array_less(p_rand_relerr, p_best_relerr) + + # The greatest relative error should not be much greater than + # eps times the condition number kappa. + # In the limit as eps approaches zero it should never be greater. + assert_array_less(p_best_relerr, (1 + 2*eps) * eps * kappa) + + +class TestKhatriRao: + + def test_basic(self): + a = khatri_rao(array([[1, 2], [3, 4]]), + array([[5, 6], [7, 8]])) + + assert_array_equal(a, array([[5, 12], + [7, 16], + [15, 24], + [21, 32]])) + + b = khatri_rao(np.empty([2, 2]), np.empty([2, 2])) + assert_array_equal(b.shape, (4, 2)) + + def test_number_of_columns_equality(self): + with pytest.raises(ValueError): + a = array([[1, 2, 3], + [4, 5, 6]]) + b = array([[1, 2], + [3, 4]]) + khatri_rao(a, b) + + def test_to_assure_2d_array(self): + with pytest.raises(ValueError): + # both arrays are 1-D + a = array([1, 2, 3]) + b = array([4, 5, 6]) + khatri_rao(a, b) + + with pytest.raises(ValueError): + # first array is 1-D + a = array([1, 2, 3]) + b = array([ + [1, 2, 3], + [4, 5, 6] + ]) + khatri_rao(a, b) + + with pytest.raises(ValueError): + # second array is 1-D + a = array([ + [1, 2, 3], + [7, 8, 9] + ]) + b = array([4, 5, 6]) + khatri_rao(a, b) + + def test_equality_of_two_equations(self): + a = array([[1, 2], [3, 4]]) + b = array([[5, 6], [7, 8]]) + + res1 = khatri_rao(a, b) + res2 = np.vstack([np.kron(a[:, k], b[:, k]) + for k in range(b.shape[1])]).T + + assert_array_equal(res1, res2) + + def test_empty(self): + a = np.empty((0, 2)) + b = np.empty((3, 2)) + res = khatri_rao(a, b) + assert_allclose(res, np.empty((0, 2))) + + a = np.empty((3, 0)) + b = np.empty((5, 0)) + res = khatri_rao(a, b) + assert_allclose(res, np.empty((15, 0))) + +@pytest.mark.parametrize('func', + [logm, sqrtm, signm]) +def test_disp_dep(func): + with pytest.deprecated_call(): + func(np.eye(2), disp=False) + +def test_blocksize_dep(): + with pytest.deprecated_call(): + sqrtm(np.eye(2), blocksize=10) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matmul_toeplitz.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matmul_toeplitz.py new file mode 100644 index 0000000000000000000000000000000000000000..22f8f94fd10a5404d4013adf995bba54f76ff803 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_matmul_toeplitz.py @@ -0,0 +1,136 @@ +"""Test functions for linalg.matmul_toeplitz function +""" + +import numpy as np +from scipy.linalg import toeplitz, matmul_toeplitz + +from pytest import raises as assert_raises +from numpy.testing import assert_allclose + + +class TestMatmulToeplitz: + + def setup_method(self): + self.rng = np.random.RandomState(42) + self.tolerance = 1.5e-13 + + def test_real(self): + cases = [] + + n = 1 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=(n, 1)) + cases.append((x, c, r, False)) + + n = 2 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=(n, 1)) + cases.append((x, c, r, False)) + + n = 101 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=(n, 1)) + cases.append((x, c, r, True)) + + n = 1000 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=(n, 1)) + cases.append((x, c, r, False)) + + n = 100 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=(n, self.rng.randint(1, 10))) + cases.append((x, c, r, False)) + + n = 100 + c = self.rng.normal(size=(n, 1)) + r = self.rng.normal(size=(n, 1)) + x = self.rng.normal(size=(n, self.rng.randint(1, 10))) + cases.append((x, c, r, True)) + + n = 100 + c = self.rng.normal(size=(n, 1)) + r = None + x = self.rng.normal(size=(n, self.rng.randint(1, 10))) + cases.append((x, c, r, True, -1)) + + n = 100 + c = self.rng.normal(size=(n, 1)) + r = None + x = self.rng.normal(size=n) + cases.append((x, c, r, False)) + + n = 101 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n-27) + x = self.rng.normal(size=(n-27, 1)) + cases.append((x, c, r, True)) + + n = 100 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n//4) + x = self.rng.normal(size=(n//4, self.rng.randint(1, 10))) + cases.append((x, c, r, True)) + + [self.do(*i) for i in cases] + + def test_complex(self): + n = 127 + c = self.rng.normal(size=(n, 1)) + self.rng.normal(size=(n, 1))*1j + r = self.rng.normal(size=(n, 1)) + self.rng.normal(size=(n, 1))*1j + x = self.rng.normal(size=(n, 3)) + self.rng.normal(size=(n, 3))*1j + self.do(x, c, r, False) + + n = 100 + c = self.rng.normal(size=(n, 1)) + self.rng.normal(size=(n, 1))*1j + r = self.rng.normal(size=(n//2, 1)) +\ + self.rng.normal(size=(n//2, 1))*1j + x = self.rng.normal(size=(n//2, 3)) +\ + self.rng.normal(size=(n//2, 3))*1j + self.do(x, c, r, False) + + def test_empty(self): + c = [] + r = [] + x = [] + self.do(x, c, r, False) + + x = np.empty((0, 0)) + self.do(x, c, r, False) + + def test_exceptions(self): + + n = 100 + c = self.rng.normal(size=n) + r = self.rng.normal(size=2*n) + x = self.rng.normal(size=n) + assert_raises(ValueError, matmul_toeplitz, (c, r), x, True) + + n = 100 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n) + x = self.rng.normal(size=n-1) + assert_raises(ValueError, matmul_toeplitz, (c, r), x, True) + + n = 100 + c = self.rng.normal(size=n) + r = self.rng.normal(size=n//2) + x = self.rng.normal(size=n//2-1) + assert_raises(ValueError, matmul_toeplitz, (c, r), x, True) + + # For toeplitz matrices, matmul_toeplitz() should be equivalent to @. + def do(self, x, c, r=None, check_finite=False, workers=None): + c = np.ravel(c) + if r is None: + actual = matmul_toeplitz(c, x, check_finite, workers) + else: + r = np.ravel(r) + actual = matmul_toeplitz((c, r), x, check_finite) + desired = toeplitz(c, r) @ x + assert_allclose(actual, desired, + rtol=self.tolerance, atol=self.tolerance) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_procrustes.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_procrustes.py new file mode 100644 index 0000000000000000000000000000000000000000..658cb806d713344f26ac796c28bf1af591627c84 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_procrustes.py @@ -0,0 +1,209 @@ +from itertools import product, permutations + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from pytest import raises as assert_raises + +from scipy.linalg import orthogonal_procrustes +from scipy.sparse._sputils import matrix +from scipy._lib._array_api import make_xp_test_case, xp_assert_close +from scipy.conftest import skip_xp_invalid_arg + +def _centered(A, xp): + mu = xp.mean(A, axis=0) + return A - mu, mu + +@make_xp_test_case(orthogonal_procrustes) +class TestOrthogonalProcrustes: + def test_orthogonal_procrustes_ndim_too_small(self, xp): + rng = np.random.RandomState(1234) + A = xp.asarray(rng.randn(3)) + B = xp.asarray(rng.randn(3)) + assert_raises(ValueError, orthogonal_procrustes, A, B) + + def test_orthogonal_procrustes_shape_mismatch(self, xp): + rng = np.random.RandomState(1234) + shapes = ((3, 3), (3, 4), (4, 3), (4, 4)) + for a, b in permutations(shapes, 2): + A = xp.asarray(rng.randn(*a)) + B = xp.asarray(rng.randn(*b)) + assert_raises(ValueError, orthogonal_procrustes, A, B) + + def test_orthogonal_procrustes_checkfinite_exception(self, xp): + rng = np.random.RandomState(1234) + m, n = 2, 3 + A_good = rng.randn(m, n) + B_good = rng.randn(m, n) + for bad_value in np.inf, -np.inf, np.nan: + A_bad = A_good.copy() + A_bad[1, 2] = bad_value + B_bad = B_good.copy() + B_bad[1, 2] = bad_value + for A, B in ((A_good, B_bad), (A_bad, B_good), (A_bad, B_bad)): + assert_raises(ValueError, orthogonal_procrustes, xp.asarray(A), + xp.asarray(B)) + + def test_orthogonal_procrustes_scale_invariance(self, xp): + rng = np.random.RandomState(1234) + m, n = 4, 3 + for i in range(3): + A_orig = xp.asarray(rng.randn(m, n)) + B_orig = xp.asarray(rng.randn(m, n)) + R_orig, s = orthogonal_procrustes(A_orig, B_orig) + for A_scale in np.square(rng.randn(3)): + for B_scale in np.square(rng.randn(3)): + R, s = orthogonal_procrustes(A_orig * xp.asarray(A_scale), + B_orig * xp.asarray(B_scale)) + xp_assert_close(R, R_orig) + + @skip_xp_invalid_arg() + def test_orthogonal_procrustes_array_conversion(self): + rng = np.random.RandomState(1234) + for m, n in ((6, 4), (4, 4), (4, 6)): + A_arr = rng.randn(m, n) + B_arr = rng.randn(m, n) + As = (A_arr, A_arr.tolist(), matrix(A_arr)) + Bs = (B_arr, B_arr.tolist(), matrix(B_arr)) + R_arr, s = orthogonal_procrustes(A_arr, B_arr) + AR_arr = A_arr.dot(R_arr) + for A, B in product(As, Bs): + R, s = orthogonal_procrustes(A, B) + AR = A_arr.dot(R) + assert_allclose(AR, AR_arr) + + def test_orthogonal_procrustes(self, xp): + rng = np.random.RandomState(1234) + for m, n in ((6, 4), (4, 4), (4, 6)): + # Sample a random target matrix. + B = xp.asarray(rng.randn(m, n)) + # Sample a random orthogonal matrix + # by computing eigh of a sampled symmetric matrix. + X = xp.asarray(rng.randn(n, n)) + w, V = xp.linalg.eigh(X.T + X) + xp_assert_close(xp.linalg.inv(V), V.T) + # Compute a matrix with a known orthogonal transformation that gives B. + A = B @ V.T + # Check that an orthogonal transformation from A to B can be recovered. + R, s = orthogonal_procrustes(A, B) + xp_assert_close(xp.linalg.inv(R), R.T) + xp_assert_close(A @ R, B) + # Create a perturbed input matrix. + A_perturbed = A + 1e-2 * xp.asarray(rng.randn(m, n)) + # Check that the orthogonal procrustes function can find an orthogonal + # transformation that is better than the orthogonal transformation + # computed from the original input matrix. + R_prime, s = orthogonal_procrustes(A_perturbed, B) + xp_assert_close(xp.linalg.inv(R_prime), R_prime.T) + # Compute the naive and optimal transformations of the perturbed input. + naive_approx = A_perturbed @ R + optim_approx = A_perturbed @ R_prime + # Compute the Frobenius norm errors of the matrix approximations. + naive_approx_error = xp.linalg.matrix_norm(naive_approx - B, ord='fro') + optim_approx_error = xp.linalg.matrix_norm(optim_approx - B, ord='fro') + # Check that the orthogonal Procrustes approximation is better. + assert xp.all(optim_approx_error < naive_approx_error) + + def test_orthogonal_procrustes_exact_example(self, xp): + # Check a small application. + # It uses translation, scaling, reflection, and rotation. + # + # | + # a b | + # | + # d c | w + # | + # --------+--- x ----- z --- + # | + # | y + # | + # + A_orig = xp.asarray([[-3, 3], [-2, 3], [-2, 2], [-3, 2]], dtype=xp.float64) + B_orig = xp.asarray([[3, 2], [1, 0], [3, -2], [5, 0]], dtype=xp.float64) + A, A_mu = _centered(A_orig, xp) + B, B_mu = _centered(B_orig, xp) + R, s = orthogonal_procrustes(A, B) + scale = s / xp.linalg.matrix_norm(A)**2 + B_approx = scale * A @ R + B_mu + xp_assert_close(B_approx, B_orig, atol=1e-8) + + def test_orthogonal_procrustes_stretched_example(self, xp): + # Try again with a target with a stretched y axis. + A_orig = xp.asarray([[-3, 3], [-2, 3], [-2, 2], [-3, 2]], dtype=xp.float64) + B_orig = xp.asarray([[3, 40], [1, 0], [3, -40], [5, 0]], dtype=xp.float64) + A, A_mu = _centered(A_orig, xp) + B, B_mu = _centered(B_orig, xp) + R, s = orthogonal_procrustes(A, B) + scale = s / xp.linalg.matrix_norm(A)**2 + B_approx = scale * A @ R + B_mu + expected = xp.asarray([[3, 21], [-18, 0], [3, -21], [24, 0]], dtype=xp.float64) + xp_assert_close(B_approx, expected, atol=1e-8) + # Check disparity symmetry. + expected_disparity = xp.asarray(0.4501246882793018, dtype=xp.float64)[()] + AB_disparity = (xp.linalg.matrix_norm(B_approx - B_orig) + / xp.linalg.matrix_norm(B))**2 + xp_assert_close(AB_disparity, expected_disparity) + R, s = orthogonal_procrustes(B, A) + scale = s / xp.linalg.matrix_norm(B)**2 + A_approx = scale * B @ R + A_mu + BA_disparity = (xp.linalg.matrix_norm(A_approx - A_orig) + / xp.linalg.matrix_norm(A))**2 + xp_assert_close(BA_disparity, expected_disparity) + + def test_orthogonal_procrustes_skbio_example(self, xp): + # This transformation is also exact. + # It uses translation, scaling, and reflection. + # + # | + # | a + # | b + # | c d + # --+--------- + # | + # | w + # | + # | x + # | + # | z y + # | + # + A_orig = xp.asarray([[4, -2], [4, -4], [4, -6], [2, -6]], dtype=xp.float64) + B_orig = xp.asarray([[1, 3], [1, 2], [1, 1], [2, 1]], dtype=xp.float64) + B_standardized = xp.asarray([[-0.13363062, 0.6681531], + [-0.13363062, 0.13363062], + [-0.13363062, -0.40089186], + [0.40089186, -0.40089186]], dtype=xp.float64) + A, A_mu = _centered(A_orig, xp) + B, B_mu = _centered(B_orig, xp) + R, s = orthogonal_procrustes(A, B) + scale = s / xp.linalg.matrix_norm(A)**2 + B_approx = scale * A @ R + B_mu + xp_assert_close(B_approx, B_orig) + xp_assert_close(B / xp.linalg.matrix_norm(B), B_standardized) + + def test_empty(self, xp): + a = xp.empty((0, 0)) + r, s = orthogonal_procrustes(a, a) + xp_assert_close(r, xp.empty((0, 0))) + + a = xp.empty((0, 3)) + r, s = orthogonal_procrustes(a, a) + xp_assert_close(r, xp.eye(3)) + + @pytest.mark.parametrize('shape', [(4, 5), (5, 5), (5, 4)]) + def test_unitary(self, shape, xp): + # gh-12071 added support for unitary matrices; check that it + # works as intended. + m, n = shape + rng = np.random.default_rng(589234981235) + A = xp.asarray(rng.random(shape) + rng.random(shape) * 1j) + Q = xp.asarray(rng.random((n, n)) + rng.random((n, n)) * 1j) + Q, _ = xp.linalg.qr(Q) + B = A @ Q + R, scale = orthogonal_procrustes(A, B) + xp_assert_close(R @ xp.conj(R).T, xp.eye(n, dtype=xp.complex128), atol=1e-14) + xp_assert_close(A @ Q, B) + if shape != (4, 5): # solution is unique + xp_assert_close(R, Q) + _, s, _ = xp.linalg.svd(xp.conj(A).T @ B) + xp_assert_close(scale, xp.sum(s)) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_sketches.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_sketches.py new file mode 100644 index 0000000000000000000000000000000000000000..49c7033ced5403451d855a04b53ae94582a91ed5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_sketches.py @@ -0,0 +1,119 @@ +"""Tests for _sketches.py.""" + +import numpy as np +from numpy.testing import assert_, assert_equal +from scipy.linalg import clarkson_woodruff_transform +from scipy.linalg._sketches import cwt_matrix +from scipy.sparse import issparse, rand +from scipy.sparse.linalg import norm + + +class TestClarksonWoodruffTransform: + """ + Testing the Clarkson Woodruff Transform + """ + # set seed for generating test matrices + rng = np.random.default_rng(1179103485) + + # Test matrix parameters + n_rows = 2000 + n_cols = 100 + density = 0.1 + + # Sketch matrix dimensions + n_sketch_rows = 200 + + # Seeds to test with + seeds = [1755490010, 934377150, 1391612830, 1752708722, 2008891431, + 1302443994, 1521083269, 1501189312, 1126232505, 1533465685] + + A_dense = rng.random((n_rows, n_cols)) + A_csc = rand( + n_rows, n_cols, density=density, format='csc', random_state=rng, + ) + A_csr = rand( + n_rows, n_cols, density=density, format='csr', random_state=rng, + ) + A_coo = rand( + n_rows, n_cols, density=density, format='coo', random_state=rng, + ) + + # Collect the test matrices + test_matrices = [ + A_dense, A_csc, A_csr, A_coo, + ] + + # Test vector with norm ~1 + x = rng.random((n_rows, 1)) / np.sqrt(n_rows) + del rng # Not deterministic in pytest-run-parallel + + def test_sketch_dimensions(self): + for A in self.test_matrices: + for seed in self.seeds: + # seed to ensure backwards compatibility post SPEC7 + sketch = clarkson_woodruff_transform( + A, self.n_sketch_rows, seed=seed + ) + assert_(sketch.shape == (self.n_sketch_rows, self.n_cols)) + + def test_seed_returns_identical_transform_matrix(self): + for seed in self.seeds: + S1 = cwt_matrix( + self.n_sketch_rows, self.n_rows, rng=seed + ).toarray() + S2 = cwt_matrix( + self.n_sketch_rows, self.n_rows, rng=seed + ).toarray() + assert_equal(S1, S2) + + def test_seed_returns_identically(self): + for A in self.test_matrices: + for seed in self.seeds: + sketch1 = clarkson_woodruff_transform( + A, self.n_sketch_rows, rng=seed + ) + sketch2 = clarkson_woodruff_transform( + A, self.n_sketch_rows, rng=seed + ) + if issparse(sketch1): + sketch1 = sketch1.toarray() + if issparse(sketch2): + sketch2 = sketch2.toarray() + assert_equal(sketch1, sketch2) + + def test_sketch_preserves_frobenius_norm(self): + # Given the probabilistic nature of the sketches + # we run the test multiple times and check that + # we pass all/almost all the tries. + n_errors = 0 + for A in self.test_matrices: + if issparse(A): + true_norm = norm(A) + else: + true_norm = np.linalg.norm(A) + for seed in self.seeds: + sketch = clarkson_woodruff_transform( + A, self.n_sketch_rows, rng=seed, + ) + if issparse(sketch): + sketch_norm = norm(sketch) + else: + sketch_norm = np.linalg.norm(sketch) + + if np.abs(true_norm - sketch_norm) > 0.1 * true_norm: + n_errors += 1 + assert_(n_errors == 0) + + def test_sketch_preserves_vector_norm(self): + n_errors = 0 + n_sketch_rows = int(np.ceil(2. / (0.01 * 0.5**2))) + true_norm = np.linalg.norm(self.x) + for seed in self.seeds: + sketch = clarkson_woodruff_transform( + self.x, n_sketch_rows, rng=seed, + ) + sketch_norm = np.linalg.norm(sketch) + + if np.abs(true_norm - sketch_norm) > 0.5 * true_norm: + n_errors += 1 + assert_(n_errors == 0) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solve_toeplitz.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solve_toeplitz.py new file mode 100644 index 0000000000000000000000000000000000000000..8ed1bec48691b94ff6ae997f57523b7248bfe9e5 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solve_toeplitz.py @@ -0,0 +1,137 @@ +"""Test functions for linalg._solve_toeplitz module +""" +import numpy as np +from scipy.linalg._solve_toeplitz import levinson +from scipy.linalg import solve, toeplitz, solve_toeplitz +from numpy.testing import assert_equal, assert_allclose + +import pytest +from pytest import raises as assert_raises + + +def test_solve_equivalence(): + # For toeplitz matrices, solve_toeplitz() should be equivalent to solve(). + random = np.random.RandomState(1234) + for n in (1, 2, 3, 10): + c = random.randn(n) + if random.rand() < 0.5: + c = c + 1j * random.randn(n) + r = random.randn(n) + if random.rand() < 0.5: + r = r + 1j * random.randn(n) + y = random.randn(n) + if random.rand() < 0.5: + y = y + 1j * random.randn(n) + + # Check equivalence when both the column and row are provided. + actual = solve_toeplitz((c,r), y) + desired = solve(toeplitz(c, r=r), y) + assert_allclose(actual, desired) + + # Check equivalence when the column is provided but not the row. + actual = solve_toeplitz(c, b=y) + desired = solve(toeplitz(c), y) + assert_allclose(actual, desired) + + +def test_multiple_rhs(): + random = np.random.RandomState(1234) + c = random.randn(4) + r = random.randn(4) + for offset in [0, 1j]: + for yshape in ((4,), (4, 3)): + y = random.randn(*yshape) + offset + actual = solve_toeplitz((c,r), b=y) + desired = solve(toeplitz(c, r=r), y) + assert_equal(actual.shape, yshape) + assert_equal(desired.shape, yshape) + assert_allclose(actual, desired) + + +def test_native_list_arguments(): + c = [1,2,4,7] + r = [1,3,9,12] + y = [5,1,4,2] + actual = solve_toeplitz((c,r), y) + desired = solve(toeplitz(c, r=r), y) + assert_allclose(actual, desired) + + +def test_zero_diag_error(): + # The Levinson-Durbin implementation fails when the diagonal is zero. + random = np.random.RandomState(1234) + n = 4 + c = random.randn(n) + r = random.randn(n) + y = random.randn(n) + c[0] = 0 + assert_raises(np.linalg.LinAlgError, + solve_toeplitz, (c, r), b=y) + + +def test_wikipedia_counterexample(): + # The Levinson-Durbin implementation also fails in other cases. + # This example is from the talk page of the wikipedia article. + random = np.random.RandomState(1234) + c = [2, 2, 1] + y = random.randn(3) + assert_raises(np.linalg.LinAlgError, solve_toeplitz, c, b=y) + + +def test_reflection_coeffs(): + # check that the partial solutions are given by the reflection + # coefficients + + random = np.random.RandomState(1234) + y_d = random.randn(10) + y_z = random.randn(10) + 1j + reflection_coeffs_d = [1] + reflection_coeffs_z = [1] + for i in range(2, 10): + reflection_coeffs_d.append(solve_toeplitz(y_d[:(i-1)], b=y_d[1:i])[-1]) + reflection_coeffs_z.append(solve_toeplitz(y_z[:(i-1)], b=y_z[1:i])[-1]) + + y_d_concat = np.concatenate((y_d[-2:0:-1], y_d[:-1])) + y_z_concat = np.concatenate((y_z[-2:0:-1].conj(), y_z[:-1])) + _, ref_d = levinson(y_d_concat, b=y_d[1:]) + _, ref_z = levinson(y_z_concat, b=y_z[1:]) + + assert_allclose(reflection_coeffs_d, ref_d[:-1]) + assert_allclose(reflection_coeffs_z, ref_z[:-1]) + + +@pytest.mark.xfail(reason='Instability of Levinson iteration') +def test_unstable(): + # this is a "Gaussian Toeplitz matrix", as mentioned in Example 2 of + # I. Gohbert, T. Kailath and V. Olshevsky "Fast Gaussian Elimination with + # Partial Pivoting for Matrices with Displacement Structure" + # Mathematics of Computation, 64, 212 (1995), pp 1557-1576 + # which can be unstable for levinson recursion. + + # other fast toeplitz solvers such as GKO or Burg should be better. + random = np.random.RandomState(1234) + n = 100 + c = 0.9 ** (np.arange(n)**2) + y = random.randn(n) + + solution1 = solve_toeplitz(c, b=y) + solution2 = solve(toeplitz(c), y) + + assert_allclose(solution1, solution2) + + +@pytest.mark.parametrize('dt_c', [int, float, np.float32, complex, np.complex64]) +@pytest.mark.parametrize('dt_b', [int, float, np.float32, complex, np.complex64]) +def test_empty(dt_c, dt_b): + c = np.array([], dtype=dt_c) + b = np.array([], dtype=dt_b) + x = solve_toeplitz(c, b) + assert x.shape == (0,) + assert x.dtype == solve_toeplitz(np.array([2, 1], dtype=dt_c), + np.ones(2, dtype=dt_b)).dtype + + b = np.empty((0, 0), dtype=dt_b) + x1 = solve_toeplitz(c, b) + assert x1.shape == (0, 0) + assert x1.dtype == x.dtype + diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solvers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solvers.py new file mode 100644 index 0000000000000000000000000000000000000000..1c169308c7f6a98b08c6a868071be312df184061 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_solvers.py @@ -0,0 +1,862 @@ +import os +import numpy as np + +from numpy.testing import assert_array_almost_equal, assert_allclose +import pytest +from pytest import raises as assert_raises + +from scipy.linalg import solve_sylvester +from scipy.linalg import solve_continuous_lyapunov, solve_discrete_lyapunov +from scipy.linalg import solve_continuous_are, solve_discrete_are +from scipy.linalg import block_diag, solve, LinAlgError +from scipy.sparse._sputils import matrix +from scipy.conftest import skip_xp_invalid_arg + + +# dtypes for testing size-0 case following precedent set in gh-20295 +dtypes = [int, float, np.float32, complex, np.complex64] + + +def _load_data(name): + """ + Load npz data file under data/ + Returns a copy of the data, rather than keeping the npz file open. + """ + filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'data', name) + with np.load(filename) as f: + return dict(f.items()) + + +class TestSolveLyapunov: + + cases = [ + # empty case + (np.empty((0, 0)), + np.empty((0, 0))), + (np.array([[1, 2], [3, 4]]), + np.array([[9, 10], [11, 12]])), + # a, q all complex. + (np.array([[1.0+1j, 2.0], [3.0-4.0j, 5.0]]), + np.array([[2.0-2j, 2.0+2j], [-1.0-1j, 2.0]])), + # a real; q complex. + (np.array([[1.0, 2.0], [3.0, 5.0]]), + np.array([[2.0-2j, 2.0+2j], [-1.0-1j, 2.0]])), + # a complex; q real. + (np.array([[1.0+1j, 2.0], [3.0-4.0j, 5.0]]), + np.array([[2.0, 2.0], [-1.0, 2.0]])), + # An example from Kitagawa, 1977 + (np.array([[3, 9, 5, 1, 4], [1, 2, 3, 8, 4], [4, 6, 6, 6, 3], + [1, 5, 2, 0, 7], [5, 3, 3, 1, 5]]), + np.array([[2, 4, 1, 0, 1], [4, 1, 0, 2, 0], [1, 0, 3, 0, 3], + [0, 2, 0, 1, 0], [1, 0, 3, 0, 4]])), + # Companion matrix example. a complex; q real; a.shape[0] = 11 + (np.array([[0.100+0.j, 0.091+0.j, 0.082+0.j, 0.073+0.j, 0.064+0.j, + 0.055+0.j, 0.046+0.j, 0.037+0.j, 0.028+0.j, 0.019+0.j, + 0.010+0.j], + [1.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 1.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 1.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 1.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 1.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 1.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 1.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 1.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 1.000+0.j, 0.000+0.j, + 0.000+0.j], + [0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, + 0.000+0.j, 0.000+0.j, 0.000+0.j, 0.000+0.j, 1.000+0.j, + 0.000+0.j]]), + np.eye(11)), + # https://github.com/scipy/scipy/issues/4176 + (matrix([[0, 1], [-1/2, -1]]), + (matrix([0, 3]).T @ matrix([0, 3]).T.T)), + # https://github.com/scipy/scipy/issues/4176 + (matrix([[0, 1], [-1/2, -1]]), + (np.array(matrix([0, 3]).T @ matrix([0, 3]).T.T))), + ] + + def test_continuous_squareness_and_shape(self): + nsq = np.ones((3, 2)) + sq = np.eye(3) + assert_raises(ValueError, solve_continuous_lyapunov, nsq, sq) + assert_raises(ValueError, solve_continuous_lyapunov, sq, nsq) + assert_raises(ValueError, solve_continuous_lyapunov, sq, np.eye(2)) + + def check_continuous_case(self, a, q): + x = solve_continuous_lyapunov(a, q) + assert_array_almost_equal( + np.dot(a, x) + np.dot(x, a.conj().transpose()), q) + + def check_discrete_case(self, a, q, method=None): + x = solve_discrete_lyapunov(a, q, method=method) + assert_array_almost_equal( + np.dot(np.dot(a, x), a.conj().transpose()) - x, -1.0*q) + + @skip_xp_invalid_arg + def test_cases(self): + for case in self.cases: + self.check_continuous_case(case[0], case[1]) + self.check_discrete_case(case[0], case[1]) + self.check_discrete_case(case[0], case[1], method='direct') + self.check_discrete_case(case[0], case[1], method='bilinear') + + @pytest.mark.parametrize("dtype_a", dtypes) + @pytest.mark.parametrize("dtype_q", dtypes) + def test_size_0(self, dtype_a, dtype_q): + rng = np.random.default_rng(234598235) + + a = np.zeros((0, 0), dtype=dtype_a) + q = np.zeros((0, 0), dtype=dtype_q) + res = solve_continuous_lyapunov(a, q) + + a = (rng.random((5, 5))*100).astype(dtype_a) + q = (rng.random((5, 5))*100).astype(dtype_q) + ref = solve_continuous_lyapunov(a, q) + + assert res.shape == (0, 0) + assert res.dtype == ref.dtype + + +class TestSolveContinuousAre: + mat6 = _load_data('carex_6_data.npz') + mat15 = _load_data('carex_15_data.npz') + mat18 = _load_data('carex_18_data.npz') + mat19 = _load_data('carex_19_data.npz') + mat20 = _load_data('carex_20_data.npz') + cases = [ + # Carex examples taken from (with default parameters): + # [1] P.BENNER, A.J. LAUB, V. MEHRMANN: 'A Collection of Benchmark + # Examples for the Numerical Solution of Algebraic Riccati + # Equations II: Continuous-Time Case', Tech. Report SPC 95_23, + # Fak. f. Mathematik, TU Chemnitz-Zwickau (Germany), 1995. + # + # The format of the data is (a, b, q, r, knownfailure), where + # knownfailure is None if the test passes or a string + # indicating the reason for failure. + # + # Test Case 0: carex #1 + (np.diag([1.], 1), + np.array([[0], [1]]), + block_diag(1., 2.), + 1, + None), + # Test Case 1: carex #2 + (np.array([[4, 3], [-4.5, -3.5]]), + np.array([[1], [-1]]), + np.array([[9, 6], [6, 4.]]), + 1, + None), + # Test Case 2: carex #3 + (np.array([[0, 1, 0, 0], + [0, -1.89, 0.39, -5.53], + [0, -0.034, -2.98, 2.43], + [0.034, -0.0011, -0.99, -0.21]]), + np.array([[0, 0], [0.36, -1.6], [-0.95, -0.032], [0.03, 0]]), + np.array([[2.313, 2.727, 0.688, 0.023], + [2.727, 4.271, 1.148, 0.323], + [0.688, 1.148, 0.313, 0.102], + [0.023, 0.323, 0.102, 0.083]]), + np.eye(2), + None), + # Test Case 3: carex #4 + (np.array([[-0.991, 0.529, 0, 0, 0, 0, 0, 0], + [0.522, -1.051, 0.596, 0, 0, 0, 0, 0], + [0, 0.522, -1.118, 0.596, 0, 0, 0, 0], + [0, 0, 0.522, -1.548, 0.718, 0, 0, 0], + [0, 0, 0, 0.922, -1.64, 0.799, 0, 0], + [0, 0, 0, 0, 0.922, -1.721, 0.901, 0], + [0, 0, 0, 0, 0, 0.922, -1.823, 1.021], + [0, 0, 0, 0, 0, 0, 0.922, -1.943]]), + np.array([[3.84, 4.00, 37.60, 3.08, 2.36, 2.88, 3.08, 3.00], + [-2.88, -3.04, -2.80, -2.32, -3.32, -3.82, -4.12, -3.96]] + ).T * 0.001, + np.array([[1.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.1], + [0.0, 1.0, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.5, 0.1, 0.0, 0.0, 0.1, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.5, 0.0, 0.0, 0.1, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0], + [0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1]]), + np.eye(2), + None), + # Test Case 4: carex #5 + (np.array( + [[-4.019, 5.120, 0., 0., -2.082, 0., 0., 0., 0.870], + [-0.346, 0.986, 0., 0., -2.340, 0., 0., 0., 0.970], + [-7.909, 15.407, -4.069, 0., -6.450, 0., 0., 0., 2.680], + [-21.816, 35.606, -0.339, -3.870, -17.800, 0., 0., 0., 7.390], + [-60.196, 98.188, -7.907, 0.340, -53.008, 0., 0., 0., 20.400], + [0, 0, 0, 0, 94.000, -147.200, 0., 53.200, 0.], + [0, 0, 0, 0, 0, 94.000, -147.200, 0, 0], + [0, 0, 0, 0, 0, 12.800, 0.000, -31.600, 0], + [0, 0, 0, 0, 12.800, 0.000, 0.000, 18.800, -31.600]]), + np.array([[0.010, -0.011, -0.151], + [0.003, -0.021, 0.000], + [0.009, -0.059, 0.000], + [0.024, -0.162, 0.000], + [0.068, -0.445, 0.000], + [0.000, 0.000, 0.000], + [0.000, 0.000, 0.000], + [0.000, 0.000, 0.000], + [0.000, 0.000, 0.000]]), + np.eye(9), + np.eye(3), + None), + # Test Case 5: carex #6 + (mat6['A'], mat6['B'], mat6['Q'], mat6['R'], None), + # Test Case 6: carex #7 + (np.array([[1, 0], [0, -2.]]), + np.array([[1e-6], [0]]), + np.ones((2, 2)), + 1., + 'Bad residual accuracy'), + # Test Case 7: carex #8 + (block_diag(-0.1, -0.02), + np.array([[0.100, 0.000], [0.001, 0.010]]), + np.array([[100, 1000], [1000, 10000]]), + np.ones((2, 2)) + block_diag(1e-6, 0), + None), + # Test Case 8: carex #9 + (np.array([[0, 1e6], [0, 0]]), + np.array([[0], [1.]]), + np.eye(2), + 1., + None), + # Test Case 9: carex #10 + (np.array([[1.0000001, 1], [1., 1.0000001]]), + np.eye(2), + np.eye(2), + np.eye(2), + None), + # Test Case 10: carex #11 + (np.array([[3, 1.], [4, 2]]), + np.array([[1], [1]]), + np.array([[-11, -5], [-5, -2.]]), + 1., + None), + # Test Case 11: carex #12 + (np.array([[7000000., 2000000., -0.], + [2000000., 6000000., -2000000.], + [0., -2000000., 5000000.]]) / 3, + np.eye(3), + np.array([[1., -2., -2.], [-2., 1., -2.], [-2., -2., 1.]]).dot( + np.diag([1e-6, 1, 1e6])).dot( + np.array([[1., -2., -2.], [-2., 1., -2.], [-2., -2., 1.]])) / 9, + np.eye(3) * 1e6, + 'Bad Residual Accuracy'), + # Test Case 12: carex #13 + (np.array([[0, 0.4, 0, 0], + [0, 0, 0.345, 0], + [0, -0.524e6, -0.465e6, 0.262e6], + [0, 0, 0, -1e6]]), + np.array([[0, 0, 0, 1e6]]).T, + np.diag([1, 0, 1, 0]), + 1., + None), + # Test Case 13: carex #14 + (np.array([[-1e-6, 1, 0, 0], + [-1, -1e-6, 0, 0], + [0, 0, 1e-6, 1], + [0, 0, -1, 1e-6]]), + np.ones((4, 1)), + np.ones((4, 4)), + 1., + None), + # Test Case 14: carex #15 + (mat15['A'], mat15['B'], mat15['Q'], mat15['R'], None), + # Test Case 15: carex #16 + (np.eye(64, 64, k=-1) + np.eye(64, 64)*(-2.) + np.rot90( + block_diag(1, np.zeros((62, 62)), 1)) + np.eye(64, 64, k=1), + np.eye(64), + np.eye(64), + np.eye(64), + None), + # Test Case 16: carex #17 + (np.diag(np.ones((20, )), 1), + np.flipud(np.eye(21, 1)), + np.eye(21, 1) * np.eye(21, 1).T, + 1, + 'Bad Residual Accuracy'), + # Test Case 17: carex #18 + (mat18['A'], mat18['B'], mat18['Q'], mat18['R'], None), + # Test Case 18: carex #19 + (mat19['A'], mat19['B'], mat19['Q'], mat19['R'], + 'Bad Residual Accuracy'), + # Test Case 19: carex #20 + (mat20['A'], mat20['B'], mat20['Q'], mat20['R'], + 'Bad Residual Accuracy') + ] + # Makes the minimum precision requirements customized to the test. + # Here numbers represent the number of decimals that agrees with zero + # matrix when the solution x is plugged in to the equation. + # + # res = array([[8e-3,1e-16],[1e-16,1e-20]]) --> min_decimal[k] = 2 + # + # If the test is failing use "None" for that entry. + # + min_decimal = (14, 12, 13, 14, 11, 6, None, 5, 7, 14, 14, + None, 9, 14, 13, 14, None, 12, None, None) + + @pytest.mark.parametrize("j, case", enumerate(cases)) + def test_solve_continuous_are(self, j, case): + """Checks if 0 = XA + A'X - XB(R)^{-1} B'X + Q is true""" + a, b, q, r, knownfailure = case + if knownfailure: + pytest.xfail(reason=knownfailure) + + dec = self.min_decimal[j] + x = solve_continuous_are(a, b, q, r) + res = x @ a + a.conj().T @ x + q + out_fact = x @ b + res -= out_fact @ solve(np.atleast_2d(r), out_fact.conj().T) + assert_array_almost_equal(res, np.zeros_like(res), decimal=dec) + + +class TestSolveDiscreteAre: + cases = [ + # Darex examples taken from (with default parameters): + # [1] P.BENNER, A.J. LAUB, V. MEHRMANN: 'A Collection of Benchmark + # Examples for the Numerical Solution of Algebraic Riccati + # Equations II: Discrete-Time Case', Tech. Report SPC 95_23, + # Fak. f. Mathematik, TU Chemnitz-Zwickau (Germany), 1995. + # [2] T. GUDMUNDSSON, C. KENNEY, A.J. LAUB: 'Scaling of the + # Discrete-Time Algebraic Riccati Equation to Enhance Stability + # of the Schur Solution Method', IEEE Trans.Aut.Cont., vol.37(4) + # + # The format of the data is (a, b, q, r, knownfailure), where + # knownfailure is None if the test passes or a string + # indicating the reason for failure. + # + # TEST CASE 0 : Complex a; real b, q, r + (np.array([[2, 1-2j], [0, -3j]]), + np.array([[0], [1]]), + np.array([[1, 0], [0, 2]]), + np.array([[1]]), + None), + # TEST CASE 1 :Real a, q, r; complex b + (np.array([[2, 1], [0, -1]]), + np.array([[-2j], [1j]]), + np.array([[1, 0], [0, 2]]), + np.array([[1]]), + None), + # TEST CASE 2 : Real a, b; complex q, r + (np.array([[3, 1], [0, -1]]), + np.array([[1, 2], [1, 3]]), + np.array([[1, 1+1j], [1-1j, 2]]), + np.array([[2, -2j], [2j, 3]]), + None), + # TEST CASE 3 : User-reported gh-2251 (Trac #1732) + (np.array([[0.63399379, 0.54906824, 0.76253406], + [0.5404729, 0.53745766, 0.08731853], + [0.27524045, 0.84922129, 0.4681622]]), + np.array([[0.96861695], [0.05532739], [0.78934047]]), + np.eye(3), + np.eye(1), + None), + # TEST CASE 4 : darex #1 + (np.array([[4, 3], [-4.5, -3.5]]), + np.array([[1], [-1]]), + np.array([[9, 6], [6, 4]]), + np.array([[1]]), + None), + # TEST CASE 5 : darex #2 + (np.array([[0.9512, 0], [0, 0.9048]]), + np.array([[4.877, 4.877], [-1.1895, 3.569]]), + np.array([[0.005, 0], [0, 0.02]]), + np.array([[1/3, 0], [0, 3]]), + None), + # TEST CASE 6 : darex #3 + (np.array([[2, -1], [1, 0]]), + np.array([[1], [0]]), + np.array([[0, 0], [0, 1]]), + np.array([[0]]), + None), + # TEST CASE 7 : darex #4 (skipped the gen. Ric. term S) + (np.array([[0, 1], [0, -1]]), + np.array([[1, 0], [2, 1]]), + np.array([[-4, -4], [-4, 7]]) * (1/11), + np.array([[9, 3], [3, 1]]), + None), + # TEST CASE 8 : darex #5 + (np.array([[0, 1], [0, 0]]), + np.array([[0], [1]]), + np.array([[1, 2], [2, 4]]), + np.array([[1]]), + None), + # TEST CASE 9 : darex #6 + (np.array([[0.998, 0.067, 0, 0], + [-.067, 0.998, 0, 0], + [0, 0, 0.998, 0.153], + [0, 0, -.153, 0.998]]), + np.array([[0.0033, 0.0200], + [0.1000, -.0007], + [0.0400, 0.0073], + [-.0028, 0.1000]]), + np.array([[1.87, 0, 0, -0.244], + [0, 0.744, 0.205, 0], + [0, 0.205, 0.589, 0], + [-0.244, 0, 0, 1.048]]), + np.eye(2), + None), + # TEST CASE 10 : darex #7 + (np.array([[0.984750, -.079903, 0.0009054, -.0010765], + [0.041588, 0.998990, -.0358550, 0.0126840], + [-.546620, 0.044916, -.3299100, 0.1931800], + [2.662400, -.100450, -.9245500, -.2632500]]), + np.array([[0.0037112, 0.0007361], + [-.0870510, 9.3411e-6], + [-1.198440, -4.1378e-4], + [-3.192700, 9.2535e-4]]), + np.eye(4)*1e-2, + np.eye(2), + None), + # TEST CASE 11 : darex #8 + (np.array([[-0.6000000, -2.2000000, -3.6000000, -5.4000180], + [1.0000000, 0.6000000, 0.8000000, 3.3999820], + [0.0000000, 1.0000000, 1.8000000, 3.7999820], + [0.0000000, 0.0000000, 0.0000000, -0.9999820]]), + np.array([[1.0, -1.0, -1.0, -1.0], + [0.0, 1.0, -1.0, -1.0], + [0.0, 0.0, 1.0, -1.0], + [0.0, 0.0, 0.0, 1.0]]), + np.array([[2, 1, 3, 6], + [1, 2, 2, 5], + [3, 2, 6, 11], + [6, 5, 11, 22]]), + np.eye(4), + None), + # TEST CASE 12 : darex #9 + (np.array([[95.4070, 1.9643, 0.3597, 0.0673, 0.0190], + [40.8490, 41.3170, 16.0840, 4.4679, 1.1971], + [12.2170, 26.3260, 36.1490, 15.9300, 12.3830], + [4.1118, 12.8580, 27.2090, 21.4420, 40.9760], + [0.1305, 0.5808, 1.8750, 3.6162, 94.2800]]) * 0.01, + np.array([[0.0434, -0.0122], + [2.6606, -1.0453], + [3.7530, -5.5100], + [3.6076, -6.6000], + [0.4617, -0.9148]]) * 0.01, + np.eye(5), + np.eye(2), + None), + # TEST CASE 13 : darex #10 + (np.kron(np.eye(2), np.diag([1, 1], k=1)), + np.kron(np.eye(2), np.array([[0], [0], [1]])), + np.array([[1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, -1, 0], + [0, 0, 0, -1, 1, 0], + [0, 0, 0, 0, 0, 0]]), + np.array([[3, 0], [0, 1]]), + None), + # TEST CASE 14 : darex #11 + (0.001 * np.array( + [[870.1, 135.0, 11.59, .5014, -37.22, .3484, 0, 4.242, 7.249], + [76.55, 897.4, 12.72, 0.5504, -40.16, .3743, 0, 4.53, 7.499], + [-127.2, 357.5, 817, 1.455, -102.8, .987, 0, 11.85, 18.72], + [-363.5, 633.9, 74.91, 796.6, -273.5, 2.653, 0, 31.72, 48.82], + [-960, 1645.9, -128.9, -5.597, 71.42, 7.108, 0, 84.52, 125.9], + [-664.4, 112.96, -88.89, -3.854, 84.47, 13.6, 0, 144.3, 101.6], + [-410.2, 693, -54.71, -2.371, 66.49, 12.49, .1063, 99.97, 69.67], + [-179.9, 301.7, -23.93, -1.035, 60.59, 22.16, 0, 213.9, 35.54], + [-345.1, 580.4, -45.96, -1.989, 105.6, 19.86, 0, 219.1, 215.2]]), + np.array([[4.7600, -0.5701, -83.6800], + [0.8790, -4.7730, -2.7300], + [1.4820, -13.1200, 8.8760], + [3.8920, -35.1300, 24.8000], + [10.3400, -92.7500, 66.8000], + [7.2030, -61.5900, 38.3400], + [4.4540, -36.8300, 20.2900], + [1.9710, -15.5400, 6.9370], + [3.7730, -30.2800, 14.6900]]) * 0.001, + np.diag([50, 0, 0, 0, 50, 0, 0, 0, 0]), + np.eye(3), + None), + # TEST CASE 15 : darex #12 - numerically least accurate example + (np.array([[0, 1e6], [0, 0]]), + np.array([[0], [1]]), + np.eye(2), + np.array([[1]]), + None), + # TEST CASE 16 : darex #13 + (np.array([[16, 10, -2], + [10, 13, -8], + [-2, -8, 7]]) * (1/9), + np.eye(3), + 1e6 * np.eye(3), + 1e6 * np.eye(3), + None), + # TEST CASE 17 : darex #14 + (np.array([[1 - 1/1e8, 0, 0, 0], + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0]]), + np.array([[1e-08], [0], [0], [0]]), + np.diag([0, 0, 0, 1]), + np.array([[0.25]]), + None), + # TEST CASE 18 : darex #15 + (np.eye(100, k=1), + np.flipud(np.eye(100, 1)), + np.eye(100), + np.array([[1]]), + None) + ] + + # Makes the minimum precision requirements customized to the test. + # Here numbers represent the number of decimals that agrees with zero + # matrix when the solution x is plugged in to the equation. + # + # res = array([[8e-3,1e-16],[1e-16,1e-20]]) --> min_decimal[k] = 2 + # + # If the test is failing use "None" for that entry. + # + min_decimal = (12, 14, 13, 14, 13, 16, 18, 14, 14, 13, + 14, 13, 13, 14, 12, 2, 4, 6, 10) + max_tol = [1.5 * 10**-ind for ind in min_decimal] + # relaxed tolerance in gh-18012 after bump to OpenBLAS + max_tol[11] = 2.5e-13 + + # relaxed tolerance in gh-20335 for linux-aarch64 build on Cirrus + # with OpenBLAS from ubuntu jammy + max_tol[15] = 2.0e-2 + + # relaxed tolerance in gh-20335 for OpenBLAS 3.20 on ubuntu jammy + # bump not needed for OpenBLAS 3.26 + max_tol[16] = 2.0e-4 + + @pytest.mark.parametrize("j, case", enumerate(cases)) + def test_solve_discrete_are(self, j, case): + """Checks if X = A'XA-(A'XB)(R+B'XB)^-1(B'XA)+Q) is true""" + a, b, q, r, knownfailure = case + if knownfailure: + pytest.xfail(reason=knownfailure) + + atol = self.max_tol[j] + + x = solve_discrete_are(a, b, q, r) + bH = b.conj().T + xa, xb = x @ a, x @ b + + res = a.conj().T @ xa - x + q + res -= a.conj().T @ xb @ (solve(r + bH @ xb, bH) @ xa) + + # changed from + # assert_array_almost_equal(res, np.zeros_like(res), decimal=dec) + # in gh-18012 as it's easier to relax a tolerance and allclose is + # preferred + assert_allclose(res, np.zeros_like(res), atol=atol) + + def test_infeasible(self): + # An infeasible example taken from https://arxiv.org/abs/1505.04861v1 + A = np.triu(np.ones((3, 3))) + A[0, 1] = -1 + B = np.array([[1, 1, 0], [0, 0, 1]]).T + Q = np.full_like(A, -2) + np.diag([8, -1, -1.9]) + R = np.diag([-10, 0.1]) + assert_raises(LinAlgError, solve_continuous_are, A, B, Q, R) + + +class TestSolveCommonAre: + @pytest.mark.parametrize("solver", [solve_continuous_are, solve_discrete_are]) + def test_with_skipped_array_argument_gh23336(self, solver): + # gh-23336 reported a failure when optional argument `e` was skipped + A = np.array([[-0.9, 0.25], [0, -1.1]]) + B = np.array([[0.23], [0.45]]) + Q = np.eye(2) + R = np.atleast_2d(0.45) + E = np.eye(2) + S = np.array([[0.1], [0.2]]) + + res = solver(A, B, Q, R, s=S) + ref = solver(A, B, Q, R, E, S) + np.testing.assert_allclose(res, ref) + + +def test_solve_generalized_continuous_are(): + cases = [ + # Two random examples differ by s term + # in the absence of any literature for demanding examples. + (np.array([[2.769230e-01, 8.234578e-01, 9.502220e-01], + [4.617139e-02, 6.948286e-01, 3.444608e-02], + [9.713178e-02, 3.170995e-01, 4.387444e-01]]), + np.array([[3.815585e-01, 1.868726e-01], + [7.655168e-01, 4.897644e-01], + [7.951999e-01, 4.455862e-01]]), + np.eye(3), + np.eye(2), + np.array([[6.463130e-01, 2.760251e-01, 1.626117e-01], + [7.093648e-01, 6.797027e-01, 1.189977e-01], + [7.546867e-01, 6.550980e-01, 4.983641e-01]]), + np.zeros((3, 2)), + None), + (np.array([[2.769230e-01, 8.234578e-01, 9.502220e-01], + [4.617139e-02, 6.948286e-01, 3.444608e-02], + [9.713178e-02, 3.170995e-01, 4.387444e-01]]), + np.array([[3.815585e-01, 1.868726e-01], + [7.655168e-01, 4.897644e-01], + [7.951999e-01, 4.455862e-01]]), + np.eye(3), + np.eye(2), + np.array([[6.463130e-01, 2.760251e-01, 1.626117e-01], + [7.093648e-01, 6.797027e-01, 1.189977e-01], + [7.546867e-01, 6.550980e-01, 4.983641e-01]]), + np.ones((3, 2)), + None) + ] + + min_decimal = (10, 10) + + def _test_factory(case, dec): + """Checks if X = A'XA-(A'XB)(R+B'XB)^-1(B'XA)+Q) is true""" + a, b, q, r, e, s, knownfailure = case + if knownfailure: + pytest.xfail(reason=knownfailure) + + x = solve_continuous_are(a, b, q, r, e, s) + res = a.conj().T.dot(x.dot(e)) + e.conj().T.dot(x.dot(a)) + q + out_fact = e.conj().T.dot(x).dot(b) + s + res -= out_fact.dot(solve(np.atleast_2d(r), out_fact.conj().T)) + assert_array_almost_equal(res, np.zeros_like(res), decimal=dec) + + for ind, case in enumerate(cases): + _test_factory(case, min_decimal[ind]) + + +def test_solve_generalized_discrete_are(): + mat20170120 = _load_data('gendare_20170120_data.npz') + + cases = [ + # Two random examples differ by s term + # in the absence of any literature for demanding examples. + (np.array([[2.769230e-01, 8.234578e-01, 9.502220e-01], + [4.617139e-02, 6.948286e-01, 3.444608e-02], + [9.713178e-02, 3.170995e-01, 4.387444e-01]]), + np.array([[3.815585e-01, 1.868726e-01], + [7.655168e-01, 4.897644e-01], + [7.951999e-01, 4.455862e-01]]), + np.eye(3), + np.eye(2), + np.array([[6.463130e-01, 2.760251e-01, 1.626117e-01], + [7.093648e-01, 6.797027e-01, 1.189977e-01], + [7.546867e-01, 6.550980e-01, 4.983641e-01]]), + np.zeros((3, 2)), + None), + (np.array([[2.769230e-01, 8.234578e-01, 9.502220e-01], + [4.617139e-02, 6.948286e-01, 3.444608e-02], + [9.713178e-02, 3.170995e-01, 4.387444e-01]]), + np.array([[3.815585e-01, 1.868726e-01], + [7.655168e-01, 4.897644e-01], + [7.951999e-01, 4.455862e-01]]), + np.eye(3), + np.eye(2), + np.array([[6.463130e-01, 2.760251e-01, 1.626117e-01], + [7.093648e-01, 6.797027e-01, 1.189977e-01], + [7.546867e-01, 6.550980e-01, 4.983641e-01]]), + np.ones((3, 2)), + None), + # user-reported (under PR-6616) 20-Jan-2017 + # tests against the case where E is None but S is provided + (mat20170120['A'], + mat20170120['B'], + mat20170120['Q'], + mat20170120['R'], + None, + mat20170120['S'], + None), + ] + + max_atol = (1.5e-11, 1.5e-11, 3.5e-16) + + def _test_factory(case, atol): + """Checks if X = A'XA-(A'XB)(R+B'XB)^-1(B'XA)+Q) is true""" + a, b, q, r, e, s, knownfailure = case + if knownfailure: + pytest.xfail(reason=knownfailure) + + x = solve_discrete_are(a, b, q, r, e, s) + if e is None: + e = np.eye(a.shape[0]) + if s is None: + s = np.zeros_like(b) + res = a.conj().T.dot(x.dot(a)) - e.conj().T.dot(x.dot(e)) + q + res -= (a.conj().T.dot(x.dot(b)) + s).dot( + solve(r+b.conj().T.dot(x.dot(b)), + (b.conj().T.dot(x.dot(a)) + s.conj().T) + ) + ) + # changed from: + # assert_array_almost_equal(res, np.zeros_like(res), decimal=dec) + # in gh-17950 because of a Linux 32 bit fail. + assert_allclose(res, np.zeros_like(res), atol=atol) + + for ind, case in enumerate(cases): + _test_factory(case, max_atol[ind]) + + +def test_are_validate_args(): + + def test_square_shape(): + nsq = np.ones((3, 2)) + sq = np.eye(3) + for x in (solve_continuous_are, solve_discrete_are): + assert_raises(ValueError, x, nsq, 1, 1, 1) + assert_raises(ValueError, x, sq, sq, nsq, 1) + assert_raises(ValueError, x, sq, sq, sq, nsq) + assert_raises(ValueError, x, sq, sq, sq, sq, nsq) + + def test_compatible_sizes(): + nsq = np.ones((3, 2)) + sq = np.eye(4) + for x in (solve_continuous_are, solve_discrete_are): + assert_raises(ValueError, x, sq, nsq, 1, 1) + assert_raises(ValueError, x, sq, sq, sq, sq, sq, nsq) + assert_raises(ValueError, x, sq, sq, np.eye(3), sq) + assert_raises(ValueError, x, sq, sq, sq, np.eye(3)) + assert_raises(ValueError, x, sq, sq, sq, sq, np.eye(3)) + + def test_symmetry(): + nsym = np.arange(9).reshape(3, 3) + sym = np.eye(3) + for x in (solve_continuous_are, solve_discrete_are): + assert_raises(ValueError, x, sym, sym, nsym, sym) + assert_raises(ValueError, x, sym, sym, sym, nsym) + + def test_singularity(): + sing = np.full((3, 3), 1e12) + sing[2, 2] -= 1 + sq = np.eye(3) + for x in (solve_continuous_are, solve_discrete_are): + assert_raises(ValueError, x, sq, sq, sq, sq, sing) + + assert_raises(ValueError, solve_continuous_are, sq, sq, sq, sing) + + def test_finiteness(): + nm = np.full((2, 2), np.nan) + sq = np.eye(2) + for x in (solve_continuous_are, solve_discrete_are): + assert_raises(ValueError, x, nm, sq, sq, sq) + assert_raises(ValueError, x, sq, nm, sq, sq) + assert_raises(ValueError, x, sq, sq, nm, sq) + assert_raises(ValueError, x, sq, sq, sq, nm) + assert_raises(ValueError, x, sq, sq, sq, sq, nm) + assert_raises(ValueError, x, sq, sq, sq, sq, sq, nm) + + +class TestSolveSylvester: + cases = [ + # empty cases + (np.empty((0, 0)), + np.empty((0, 0)), + np.empty((0, 0))), + (np.empty((0, 0)), + np.empty((2, 2)), + np.empty((0, 2))), + (np.empty((2, 2)), + np.empty((0, 0)), + np.empty((2, 0))), + # a, b, c all real. + (np.array([[1, 2], [0, 4]]), + np.array([[5, 6], [0, 8]]), + np.array([[9, 10], [11, 12]])), + # a, b, c all real, 4x4. a and b have non-trivial 2x2 blocks in their + # quasi-triangular form. + (np.array([[1.0, 0, 0, 0], + [0, 1.0, 2.0, 0.0], + [0, 0, 3.0, -4], + [0, 0, 2, 5]]), + np.array([[2.0, 0, 0, 1.0], + [0, 1.0, 0.0, 0.0], + [0, 0, 1.0, -1], + [0, 0, 1, 1]]), + np.array([[1.0, 0, 0, 0], + [0, 1.0, 0, 0], + [0, 0, 1.0, 0], + [0, 0, 0, 1.0]])), + # a, b, c all complex. + (np.array([[1.0+1j, 2.0], [3.0-4.0j, 5.0]]), + np.array([[-1.0, 2j], [3.0, 4.0]]), + np.array([[2.0-2j, 2.0+2j], [-1.0-1j, 2.0]])), + # a and b real; c complex. + (np.array([[1.0, 2.0], [3.0, 5.0]]), + np.array([[-1.0, 0], [3.0, 4.0]]), + np.array([[2.0-2j, 2.0+2j], [-1.0-1j, 2.0]])), + # a and c complex; b real. + (np.array([[1.0+1j, 2.0], [3.0-4.0j, 5.0]]), + np.array([[-1.0, 0], [3.0, 4.0]]), + np.array([[2.0-2j, 2.0+2j], [-1.0-1j, 2.0]])), + # a complex; b and c real. + (np.array([[1.0+1j, 2.0], [3.0-4.0j, 5.0]]), + np.array([[-1.0, 0], [3.0, 4.0]]), + np.array([[2.0, 2.0], [-1.0, 2.0]])), + # not square matrices, real + (np.array([[8, 1, 6], [3, 5, 7], [4, 9, 2]]), + np.array([[2, 3], [4, 5]]), + np.array([[1, 2], [3, 4], [5, 6]])), + # not square matrices, complex + (np.array([[8, 1j, 6+2j], [3, 5, 7], [4, 9, 2]]), + np.array([[2, 3], [4, 5-1j]]), + np.array([[1, 2j], [3, 4j], [5j, 6+7j]])), + ] + + def check_case(self, a, b, c): + x = solve_sylvester(a, b, c) + assert_array_almost_equal(np.dot(a, x) + np.dot(x, b), c) + + def test_cases(self): + for case in self.cases: + self.check_case(case[0], case[1], case[2]) + + def test_trivial(self): + a = np.array([[1.0, 0.0], [0.0, 1.0]]) + b = np.array([[1.0]]) + c = np.array([2.0, 2.0]).reshape(-1, 1) + x = solve_sylvester(a, b, c) + assert_array_almost_equal(x, np.array([1.0, 1.0]).reshape(-1, 1)) + + # Feel free to adjust this to test fewer dtypes or random selections rather than + # the Cartesian product. It doesn't take very long to test all combinations, + # though, so we'll start there and trim it down as we see fit. + @pytest.mark.parametrize("dtype_a", dtypes) + @pytest.mark.parametrize("dtype_b", dtypes) + @pytest.mark.parametrize("dtype_q", dtypes) + @pytest.mark.parametrize("m", [0, 3]) + @pytest.mark.parametrize("n", [0, 3]) + def test_size_0(self, m, n, dtype_a, dtype_b, dtype_q): + if m == n != 0: + pytest.skip('m = n != 0 is not a case that needs to be tested here.') + + rng = np.random.default_rng(598435298262546) + + a = np.zeros((m, m), dtype=dtype_a) + b = np.zeros((n, n), dtype=dtype_b) + q = np.zeros((m, n), dtype=dtype_q) + res = solve_sylvester(a, b, q) + + a = (rng.random((5, 5))*100).astype(dtype_a) + b = (rng.random((6, 6))*100).astype(dtype_b) + q = (rng.random((5, 6))*100).astype(dtype_q) + ref = solve_sylvester(a, b, q) + + assert res.shape == (m, n) + assert res.dtype == ref.dtype diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_special_matrices.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_special_matrices.py new file mode 100644 index 0000000000000000000000000000000000000000..84696bc2dd35dd2e66709e19e5e68a42178e7c7d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/linalg/tests/test_special_matrices.py @@ -0,0 +1,617 @@ +import pytest +import numpy as np +from numpy import arange, array, eye, copy, sqrt +from numpy.testing import (assert_equal, assert_array_equal, + assert_array_almost_equal, assert_allclose) +from pytest import raises as assert_raises + +from scipy.fft import fft +from scipy.special import comb +from scipy.linalg import (toeplitz, hankel, circulant, hadamard, leslie, dft, + companion, block_diag, + helmert, hilbert, invhilbert, pascal, invpascal, + fiedler, fiedler_companion, eigvals, + convolution_matrix) +from numpy.linalg import cond +from scipy._lib._array_api import (make_xp_test_case, xp_assert_equal, xp_size, + xp_default_dtype) + + +class TestToeplitz: + + def test_basic(self): + y = toeplitz([1, 2, 3]) + assert_array_equal(y, [[1, 2, 3], [2, 1, 2], [3, 2, 1]]) + y = toeplitz([1, 2, 3], [1, 4, 5]) + assert_array_equal(y, [[1, 4, 5], [2, 1, 4], [3, 2, 1]]) + + def test_complex_01(self): + data = (1.0 + arange(3.0)) * (1.0 + 1.0j) + x = copy(data) + t = toeplitz(x) + # Calling toeplitz should not change x. + assert_array_equal(x, data) + # According to the docstring, x should be the first column of t. + col0 = t[:, 0] + assert_array_equal(col0, data) + assert_array_equal(t[0, 1:], data[1:].conj()) + + def test_scalar_00(self): + """Scalar arguments still produce a 2D array.""" + t = toeplitz(10) + assert_array_equal(t, [[10]]) + t = toeplitz(10, 20) + assert_array_equal(t, [[10]]) + + def test_scalar_01(self): + c = array([1, 2, 3]) + t = toeplitz(c, 1) + assert_array_equal(t, [[1], [2], [3]]) + + def test_scalar_02(self): + c = array([1, 2, 3]) + t = toeplitz(c, array(1)) + assert_array_equal(t, [[1], [2], [3]]) + + def test_scalar_03(self): + c = array([1, 2, 3]) + t = toeplitz(c, array([1])) + assert_array_equal(t, [[1], [2], [3]]) + + def test_scalar_04(self): + r = array([10, 2, 3]) + t = toeplitz(1, r) + assert_array_equal(t, [[1, 2, 3]]) + + +class TestHankel: + def test_basic(self): + y = hankel([1, 2, 3]) + assert_array_equal(y, [[1, 2, 3], [2, 3, 0], [3, 0, 0]]) + y = hankel([1, 2, 3], [3, 4, 5]) + assert_array_equal(y, [[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + + +class TestCirculant: + def test_basic(self): + y = circulant([1, 2, 3]) + assert_array_equal(y, [[1, 3, 2], [2, 1, 3], [3, 2, 1]]) + + +class TestHadamard: + + def test_basic(self): + + y = hadamard(1) + assert_array_equal(y, [[1]]) + + y = hadamard(2, dtype=float) + assert_array_equal(y, [[1.0, 1.0], [1.0, -1.0]]) + + y = hadamard(4) + assert_array_equal(y, [[1, 1, 1, 1], + [1, -1, 1, -1], + [1, 1, -1, -1], + [1, -1, -1, 1]]) + + assert_raises(ValueError, hadamard, 0) + assert_raises(ValueError, hadamard, 5) + + +class TestLeslie: + + def test_bad_shapes(self): + assert_raises(ValueError, leslie, [[1, 1], [2, 2]], [3, 4, 5]) + assert_raises(ValueError, leslie, [1, 2], [1, 2]) + assert_raises(ValueError, leslie, [1], []) + + def test_basic(self): + a = leslie([1, 2, 3], [0.25, 0.5]) + expected = array([[1.0, 2.0, 3.0], + [0.25, 0.0, 0.0], + [0.0, 0.5, 0.0]]) + assert_array_equal(a, expected) + + +class TestCompanion: + + def test_bad_shapes(self): + assert_raises(ValueError, companion, [0, 4, 5]) + assert_raises(ValueError, companion, [1]) + assert_raises(ValueError, companion, []) + + def test_basic(self): + c = companion([1, 2, 3]) + expected = array([ + [-2.0, -3.0], + [1.0, 0.0]]) + assert_array_equal(c, expected) + + c = companion([2.0, 5.0, -10.0]) + expected = array([ + [-2.5, 5.0], + [1.0, 0.0]]) + assert_array_equal(c, expected) + + c = companion([(1.0, 2.0, 3.0), + (4.0, 5.0, 6.0)]) + expected = array([ + ([-2.00, -3.00], + [+1.00, +0.00]), + ([-1.25, -1.50], + [+1.00, +0.00]) + ]) + assert_array_equal(c, expected) + + +@make_xp_test_case(block_diag) +class TestBlockDiag: + def test_basic(self, xp): + dtype = xp.asarray(1).dtype + x = block_diag(xp.eye(2, dtype=dtype), xp.asarray([[1, 2], [3, 4], [5, 6]]), + xp.asarray([[1, 2, 3]])) + xp_assert_equal(x, xp.asarray([[1, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 2, 0, 0, 0], + [0, 0, 3, 4, 0, 0, 0], + [0, 0, 5, 6, 0, 0, 0], + [0, 0, 0, 0, 1, 2, 3]])) + + def test_dtype(self, xp): + x = block_diag(xp.asarray([[1.5]])) + assert x.dtype == xp_default_dtype(xp) + + x = block_diag(xp.asarray([[True]])) + assert x.dtype == xp.bool + + def test_mixed_dtypes(self, xp): + actual = block_diag(xp.asarray([[1.]]), xp.asarray([[1j]])) + desired = xp.asarray([[1, 0], [0, 1j]]) + xp_assert_equal(actual, desired) + + def test_scalar_and_1d_args(self, xp): + a = block_diag(xp.asarray(1)) + assert a.shape == (1, 1) + xp_assert_equal(a, xp.asarray([[1]])) + + a = block_diag(xp.asarray([2, 3]), xp.asarray(4)) + xp_assert_equal(a, xp.asarray([[2, 3, 0], [0, 0, 4]])) + + def test_no_args(self): + a = block_diag() + assert a.ndim == 2 + assert a.nbytes == 0 + + def test_empty_matrix_arg(self, xp): + # regression test for gh-4596: check the shape of the result + # for empty matrix inputs. Empty matrices are no longer ignored + # (gh-4908) it is viewed as a shape (1, 0) matrix. + dtype = xp.asarray(1).dtype + a = block_diag(xp.asarray([[1, 0], [0, 1]]), + xp.asarray([], dtype=dtype), + xp.asarray([[2, 3], [4, 5], [6, 7]])) + xp_assert_equal(a, xp.asarray([[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 2, 3], + [0, 0, 4, 5], + [0, 0, 6, 7]])) + + @pytest.mark.skip_xp_backends("dask.array", reason="dask/dask#11800") + def test_zerosized_matrix_arg(self, xp): + # test for gh-4908: check the shape of the result for + # zero-sized matrix inputs, i.e. matrices with shape (0,n) or (n,0). + # note that [[]] takes shape (1,0) + dtype = xp.asarray(1).dtype + a = block_diag(xp.asarray([[1, 0], [0, 1]]), + xp.asarray([[]], dtype=dtype), + xp.asarray([[2, 3], [4, 5], [6, 7]]), + xp.zeros([0, 2], dtype=dtype)) + xp_assert_equal(a, xp.asarray([[1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 2, 3, 0, 0], + [0, 0, 4, 5, 0, 0], + [0, 0, 6, 7, 0, 0]])) + + +class TestHelmert: + + def test_orthogonality(self): + for n in range(1, 7): + H = helmert(n, full=True) + Id = np.eye(n) + assert_allclose(H.dot(H.T), Id, atol=1e-12) + assert_allclose(H.T.dot(H), Id, atol=1e-12) + + def test_subspace(self): + for n in range(2, 7): + H_full = helmert(n, full=True) + H_partial = helmert(n) + for U in H_full[1:, :].T, H_partial.T: + C = np.eye(n) - np.full((n, n), 1 / n) + assert_allclose(U.dot(U.T), C) + assert_allclose(U.T.dot(U), np.eye(n-1), atol=1e-12) + + +class TestHilbert: + + def test_basic(self): + h3 = array([[1.0, 1/2., 1/3.], + [1/2., 1/3., 1/4.], + [1/3., 1/4., 1/5.]]) + assert_array_almost_equal(hilbert(3), h3) + + assert_array_equal(hilbert(1), [[1.0]]) + + h0 = hilbert(0) + assert_equal(h0.shape, (0, 0)) + + +class TestInvHilbert: + + def test_basic(self): + invh1 = array([[1]]) + assert_array_equal(invhilbert(1, exact=True), invh1) + assert_array_equal(invhilbert(1), invh1) + + invh2 = array([[4, -6], + [-6, 12]]) + assert_array_equal(invhilbert(2, exact=True), invh2) + assert_array_almost_equal(invhilbert(2), invh2) + + invh3 = array([[9, -36, 30], + [-36, 192, -180], + [30, -180, 180]]) + assert_array_equal(invhilbert(3, exact=True), invh3) + assert_array_almost_equal(invhilbert(3), invh3) + + invh4 = array([[16, -120, 240, -140], + [-120, 1200, -2700, 1680], + [240, -2700, 6480, -4200], + [-140, 1680, -4200, 2800]]) + assert_array_equal(invhilbert(4, exact=True), invh4) + assert_array_almost_equal(invhilbert(4), invh4) + + invh5 = array([[25, -300, 1050, -1400, 630], + [-300, 4800, -18900, 26880, -12600], + [1050, -18900, 79380, -117600, 56700], + [-1400, 26880, -117600, 179200, -88200], + [630, -12600, 56700, -88200, 44100]]) + assert_array_equal(invhilbert(5, exact=True), invh5) + assert_array_almost_equal(invhilbert(5), invh5) + + invh17 = array([ + [289, -41616, 1976760, -46124400, 629598060, -5540462928, + 33374693352, -143034400080, 446982500250, -1033026222800, + 1774926873720, -2258997839280, 2099709530100, -1384423866000, + 613101997800, -163493866080, 19835652870], + [-41616, 7990272, -426980160, 10627061760, -151103534400, + 1367702848512, -8410422724704, 36616806420480, -115857864064800, + 270465047424000, -468580694662080, 600545887119360, + -561522320049600, 372133135180800, -165537539406000, + 44316454993920, -5395297580640], + [1976760, -426980160, 24337869120, -630981792000, 9228108708000, + -85267724461920, 532660105897920, -2348052711713280, + 7504429831470000, -17664748409880000, 30818191841236800, + -39732544853164800, 37341234283298400, -24857330514030000, + 11100752642520000, -2982128117299200, 364182586693200], + [-46124400, 10627061760, -630981792000, 16826181120000, + -251209625940000, 2358021022156800, -14914482965141760, + 66409571644416000, -214015221119700000, 507295338950400000, + -890303319857952000, 1153715376477081600, -1089119333262870000, + 727848632044800000, -326170262829600000, 87894302404608000, + -10763618673376800], + [629598060, -151103534400, 9228108708000, + -251209625940000, 3810012660090000, -36210360321495360, + 231343968720664800, -1038687206500944000, 3370739732635275000, + -8037460526495400000, 14178080368737885600, -18454939322943942000, + 17489975175339030000, -11728977435138600000, 5272370630081100000, + -1424711708039692800, 174908803442373000], + [-5540462928, 1367702848512, -85267724461920, 2358021022156800, + -36210360321495360, 347619459086355456, -2239409617216035264, + 10124803292907663360, -33052510749726468000, + 79217210949138662400, -140362995650505067440, + 183420385176741672960, -174433352415381259200, + 117339159519533952000, -52892422160973595200, + 14328529177999196160, -1763080738699119840], + [33374693352, -8410422724704, 532660105897920, + -14914482965141760, 231343968720664800, -2239409617216035264, + 14527452132196331328, -66072377044391477760, + 216799987176909536400, -521925895055522958000, + 928414062734059661760, -1217424500995626443520, + 1161358898976091015200, -783401860847777371200, + 354015418167362952000, -96120549902411274240, + 11851820521255194480], + [-143034400080, 36616806420480, -2348052711713280, + 66409571644416000, -1038687206500944000, 10124803292907663360, + -66072377044391477760, 302045152202932469760, + -995510145200094810000, 2405996923185123840000, + -4294704507885446054400, 5649058909023744614400, + -5403874060541811254400, 3654352703663101440000, + -1655137020003255360000, 450325202737117593600, + -55630994283442749600], + [446982500250, -115857864064800, 7504429831470000, + -214015221119700000, 3370739732635275000, -33052510749726468000, + 216799987176909536400, -995510145200094810000, + 3293967392206196062500, -7988661659013106500000, + 14303908928401362270000, -18866974090684772052000, + 18093328327706957325000, -12263364009096700500000, + 5565847995255512250000, -1517208935002984080000, + 187754605706619279900], + [-1033026222800, 270465047424000, -17664748409880000, + 507295338950400000, -8037460526495400000, 79217210949138662400, + -521925895055522958000, 2405996923185123840000, + -7988661659013106500000, 19434404971634224000000, + -34894474126569249192000, 46141453390504792320000, + -44349976506971935800000, 30121928988527376000000, + -13697025107665828500000, 3740200989399948902400, + -463591619028689580000], + [1774926873720, -468580694662080, + 30818191841236800, -890303319857952000, 14178080368737885600, + -140362995650505067440, 928414062734059661760, + -4294704507885446054400, 14303908928401362270000, + -34894474126569249192000, 62810053427824648545600, + -83243376594051600326400, 80177044485212743068000, + -54558343880470209780000, 24851882355348879230400, + -6797096028813368678400, 843736746632215035600], + [-2258997839280, 600545887119360, -39732544853164800, + 1153715376477081600, -18454939322943942000, 183420385176741672960, + -1217424500995626443520, 5649058909023744614400, + -18866974090684772052000, 46141453390504792320000, + -83243376594051600326400, 110552468520163390156800, + -106681852579497947388000, 72720410752415168870400, + -33177973900974346080000, 9087761081682520473600, + -1129631016152221783200], + [2099709530100, -561522320049600, 37341234283298400, + -1089119333262870000, 17489975175339030000, + -174433352415381259200, 1161358898976091015200, + -5403874060541811254400, 18093328327706957325000, + -44349976506971935800000, 80177044485212743068000, + -106681852579497947388000, 103125790826848015808400, + -70409051543137015800000, 32171029219823375700000, + -8824053728865840192000, 1098252376814660067000], + [-1384423866000, 372133135180800, + -24857330514030000, 727848632044800000, -11728977435138600000, + 117339159519533952000, -783401860847777371200, + 3654352703663101440000, -12263364009096700500000, + 30121928988527376000000, -54558343880470209780000, + 72720410752415168870400, -70409051543137015800000, + 48142941226076592000000, -22027500987368499000000, + 6049545098753157120000, -753830033789944188000], + [613101997800, -165537539406000, + 11100752642520000, -326170262829600000, 5272370630081100000, + -52892422160973595200, 354015418167362952000, + -1655137020003255360000, 5565847995255512250000, + -13697025107665828500000, 24851882355348879230400, + -33177973900974346080000, 32171029219823375700000, + -22027500987368499000000, 10091416708498869000000, + -2774765838662800128000, 346146444087219270000], + [-163493866080, 44316454993920, -2982128117299200, + 87894302404608000, -1424711708039692800, + 14328529177999196160, -96120549902411274240, + 450325202737117593600, -1517208935002984080000, + 3740200989399948902400, -6797096028813368678400, + 9087761081682520473600, -8824053728865840192000, + 6049545098753157120000, -2774765838662800128000, + 763806510427609497600, -95382575704033754400], + [19835652870, -5395297580640, 364182586693200, -10763618673376800, + 174908803442373000, -1763080738699119840, 11851820521255194480, + -55630994283442749600, 187754605706619279900, + -463591619028689580000, 843736746632215035600, + -1129631016152221783200, 1098252376814660067000, + -753830033789944188000, 346146444087219270000, + -95382575704033754400, 11922821963004219300] + ]) + assert_array_equal(invhilbert(17, exact=True), invh17) + assert_allclose(invhilbert(17), invh17.astype(float), rtol=1e-12) + + def test_inverse(self): + for n in range(1, 10): + a = hilbert(n) + b = invhilbert(n) + # The Hilbert matrix is increasingly badly conditioned, + # so take that into account in the test + c = cond(a) + assert_allclose(a.dot(b), eye(n), atol=1e-15*c, rtol=1e-15*c) + + +class TestPascal: + + cases = [ + (1, array([[1]]), array([[1]])), + (2, array([[1, 1], + [1, 2]]), + array([[1, 0], + [1, 1]])), + (3, array([[1, 1, 1], + [1, 2, 3], + [1, 3, 6]]), + array([[1, 0, 0], + [1, 1, 0], + [1, 2, 1]])), + (4, array([[1, 1, 1, 1], + [1, 2, 3, 4], + [1, 3, 6, 10], + [1, 4, 10, 20]]), + array([[1, 0, 0, 0], + [1, 1, 0, 0], + [1, 2, 1, 0], + [1, 3, 3, 1]])), + ] + + def check_case(self, n, sym, low): + assert_array_equal(pascal(n), sym) + assert_array_equal(pascal(n, kind='lower'), low) + assert_array_equal(pascal(n, kind='upper'), low.T) + assert_array_almost_equal(pascal(n, exact=False), sym) + assert_array_almost_equal(pascal(n, exact=False, kind='lower'), low) + assert_array_almost_equal(pascal(n, exact=False, kind='upper'), low.T) + + def test_cases(self): + for n, sym, low in self.cases: + self.check_case(n, sym, low) + + def test_big(self): + p = pascal(50) + assert p[-1, -1] == comb(98, 49, exact=True) + + def test_threshold(self): + # Regression test. An early version of `pascal` returned an + # array of type np.uint64 for n=35, but that data type is too small + # to hold p[-1, -1]. The second assert_equal below would fail + # because p[-1, -1] overflowed. + p = pascal(34) + assert_equal(2*p.item(-1, -2), p.item(-1, -1), err_msg="n = 34") + p = pascal(35) + assert_equal(2.*p.item(-1, -2), 1.*p.item(-1, -1), err_msg="n = 35") + + +def test_invpascal(): + + def check_invpascal(n, kind, exact): + ip = invpascal(n, kind=kind, exact=exact) + p = pascal(n, kind=kind, exact=exact) + # Matrix-multiply ip and p, and check that we get the identity matrix. + # We can't use the simple expression e = ip.dot(p), because when + # n < 35 and exact is True, p.dtype is np.uint64 and ip.dtype is + # np.int64. The product of those dtypes is np.float64, which loses + # precision when n is greater than 18. Instead we'll cast both to + # object arrays, and then multiply. + e = ip.astype(object).dot(p.astype(object)) + assert_array_equal(e, eye(n), err_msg=f"n={n} kind={kind!r} exact={exact!r}") + + kinds = ['symmetric', 'lower', 'upper'] + + ns = [1, 2, 5, 18] + for n in ns: + for kind in kinds: + for exact in [True, False]: + check_invpascal(n, kind, exact) + + ns = [19, 34, 35, 50] + for n in ns: + for kind in kinds: + check_invpascal(n, kind, True) + + +def test_dft(): + m = dft(2) + expected = array([[1.0, 1.0], [1.0, -1.0]]) + assert_array_almost_equal(m, expected) + m = dft(2, scale='n') + assert_array_almost_equal(m, expected/2.0) + m = dft(2, scale='sqrtn') + assert_array_almost_equal(m, expected/sqrt(2.0)) + + x = array([0, 1, 2, 3, 4, 5, 0, 1]) + m = dft(8) + mx = m.dot(x) + fx = fft(x) + assert_array_almost_equal(mx, fx) + + +@make_xp_test_case(fiedler) +def test_fiedler(xp): + f = fiedler(xp.asarray([])) + assert xp_size(f) == 0 + + f = fiedler(xp.asarray([123.])) + xp_assert_equal(f, xp.asarray([[0.]])) + + f = fiedler(xp.arange(1, 7)) + des = xp.asarray([[0, 1, 2, 3, 4, 5], + [1, 0, 1, 2, 3, 4], + [2, 1, 0, 1, 2, 3], + [3, 2, 1, 0, 1, 2], + [4, 3, 2, 1, 0, 1], + [5, 4, 3, 2, 1, 0]]) + xp_assert_equal(f, des) + + +def test_fiedler_companion(): + fc = fiedler_companion([]) + assert_equal(fc.size, 0) + fc = fiedler_companion([1.]) + assert_equal(fc.size, 0) + fc = fiedler_companion([1., 2.]) + assert_array_equal(fc, np.array([[-2.]])) + fc = fiedler_companion([1e-12, 2., 3.]) + assert_array_almost_equal(fc, companion([1e-12, 2., 3.])) + with assert_raises(ValueError): + fiedler_companion([0, 1, 2]) + fc = fiedler_companion([1., -16., 86., -176., 105.]) + assert_array_almost_equal(eigvals(fc), + np.array([7., 5., 3., 1.])) + + +class TestConvolutionMatrix: + """ + Test convolution_matrix vs. numpy.convolve for various parameters. + """ + + def create_vector(self, n, cpx): + """Make a complex or real test vector of length n.""" + x = np.linspace(-2.5, 2.2, n) + if cpx: + x = x + 1j*np.linspace(-1.5, 3.1, n) + return x + + def test_bad_n(self): + # n must be a positive integer + with pytest.raises(ValueError, match='n must be a positive integer'): + convolution_matrix([1, 2, 3], 0) + + def test_empty_first_arg(self): + # first arg must have at least one value + with pytest.raises(ValueError, match=r'len\(a\)'): + convolution_matrix([], 4) + + def test_bad_mode(self): + # mode must be in ('full', 'valid', 'same') + with pytest.raises(ValueError, match='mode.*must be one of'): + convolution_matrix((1, 1), 4, mode='invalid argument') + + @pytest.mark.parametrize('cpx', [False, True]) + @pytest.mark.parametrize('na', [1, 2, 9]) + @pytest.mark.parametrize('nv', [1, 2, 9]) + @pytest.mark.parametrize('mode', [None, 'full', 'valid', 'same']) + def test_against_numpy_convolve(self, cpx, na, nv, mode): + a = self.create_vector(na, cpx) + v = self.create_vector(nv, cpx) + if mode is None: + y1 = np.convolve(v, a) + A = convolution_matrix(a, nv) + else: + y1 = np.convolve(v, a, mode) + A = convolution_matrix(a, nv, mode) + y2 = A @ v + assert_array_almost_equal(y1, y2) + + +@pytest.mark.fail_slow(5) # `leslie` has an import in the function +@pytest.mark.parametrize('f, args', [(circulant, ()), + (companion, ()), + (convolution_matrix, (5, 'same')), + (fiedler, ()), + (fiedler_companion, ()), + (hankel, (np.arange(9),)), + (leslie, (np.arange(9),)), + (toeplitz, (np.arange(9),)), + ]) +def test_batch(f, args): + rng = np.random.default_rng(283592436523456) + batch_shape = (2, 3) + m = 10 + A = rng.random(batch_shape + (m,)) + + if f in {hankel}: + message = "Beginning in SciPy 1.19, multidimensional input will be..." + with pytest.warns(FutureWarning, match=message): + f(A, *args) + return + + res = f(A, *args) + ref = np.asarray([f(a, *args) for a in A.reshape(-1, m)]) + ref = ref.reshape(A.shape[:-1] + ref.shape[-2:]) + assert_allclose(res, ref) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e77785efb87fc10ffa9f09b8c5783af1a5001685 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e998dc75a08310d36fe340fbc2a6be9b1cbba16 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform_xp.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform_xp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e719bcaeb1ebfd9944cffe46de052543b7c9c684 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rigid_transform_xp.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_groups.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_groups.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a50eb8895806035646e11113378ab21182c9e6f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_groups.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_spline.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_spline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6adf82c4c4ccec1598bb47fa289d33f3d8cd50db Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_spline.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_xp.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_xp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76e193c6eb14bc9c4c5a0af63524259ee484cd73 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/_rotation_xp.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/rotation.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/rotation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5d904dd11e921a259bae603648d99d5a02a0f5e0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/__pycache__/rotation.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c78889ccc9bd1a944c245f699cedfbe342f572d6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rigid_transform.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rigid_transform.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd2fd16f82be132031a81076fd5b5bd9371a29b9 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rigid_transform.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_groups.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_groups.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..414591fc13abdd0df8f9ceb9ee316232cd85f9e4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_groups.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_spline.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_spline.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3608c7c5ee392bcb0f277a9dd8f217ecf5b23148 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/__pycache__/test_rotation_spline.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rigid_transform.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rigid_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..c38aa5a2989d7136dff736bc4c291f8f0623c03d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rigid_transform.py @@ -0,0 +1,1486 @@ +import pickle +from itertools import product + +import pytest + +import numpy as np +from scipy.spatial.transform import Rotation, RigidTransform +from scipy.spatial.transform._rigid_transform import normalize_dual_quaternion +from scipy._lib._array_api import ( + is_lazy_array, + xp_vector_norm, + is_numpy, + xp_assert_close, + make_xp_test_case, + xp_assert_equal, + xp_promote +) +import scipy._lib.array_api_extra as xpx + +lazy_xp_modules = [RigidTransform] + + +def rotation_to_xp(r: Rotation, xp): + dtype = xpx.default_dtype(xp) + return Rotation.from_quat(xp.asarray(r.as_quat(), dtype=dtype)) + + +def rigid_transform_to_xp(r: RigidTransform, xp): + dtype = xpx.default_dtype(xp) + return RigidTransform.from_matrix(xp.asarray(r.as_matrix(), dtype=dtype)) + + +@make_xp_test_case(RigidTransform.as_matrix) +def test_repr(xp): + actual = repr(RigidTransform.from_matrix(xp.eye(4))) + expected = """\ +RigidTransform.from_matrix(array([[1., 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., 1., 0.], + [0., 0., 0., 1.]]))""" + if is_numpy(xp): + assert actual == expected + else: + assert actual.startswith("RigidTransform.from_matrix(") + + tf = RigidTransform.from_matrix(xp.asarray(RigidTransform.identity(2).as_matrix())) + actual = repr(tf) + expected = """\ +RigidTransform.from_matrix(array([[[1., 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., 1., 0.], + [0., 0., 0., 1.]], + + [[1., 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., 1., 0.], + [0., 0., 0., 1.]]]))""" + if is_numpy(xp): + assert actual == expected + else: + assert actual.startswith("RigidTransform.from_matrix(") + + +@make_xp_test_case(RigidTransform.from_rotation) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_rotation(xp, ndim: int): + atol = 1e-12 + rng = np.random.default_rng(0) + shape = (ndim,) * (ndim - 1) + (4,) + r = rotation_to_xp(Rotation.from_quat(rng.normal(size=shape)), xp=xp) + tf = RigidTransform.from_rotation(r) + xp_assert_close(tf.as_matrix()[..., :3, :3], r.as_matrix(), atol=atol) + xp_assert_close(tf.as_matrix()[..., :3, 3], xp.zeros(shape[:-1] + (3,)), atol=atol) + xp_assert_close(tf.as_matrix()[..., 3, :3], xp.zeros(shape[:-1] + (3,)), atol=atol) + xp_assert_close(tf.as_matrix()[..., 3, 3], xp.ones(shape[:-1]), atol=atol) + assert tf.single == (ndim == 1) + + +@make_xp_test_case(RigidTransform.from_translation) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_translation(xp, ndim: int): + shape = (ndim,) * (ndim - 1) + t = xp.reshape(xp.arange(ndim ** (ndim-1) * 3), shape + (3,)) + tf = RigidTransform.from_translation(t) + + expected = xp.tile(xp.eye(4), shape + (1, 1)) + t_float = xp_promote(t, force_floating=True, xp=xp) + expected = xpx.at(expected)[..., :3, 3].set(t_float) + xp_assert_close(tf.as_matrix(), expected) + assert tf.single == (ndim == 1) + + +def test_from_translation_array_like(): + # Test single translation + t = [1, 2, 3] + tf = RigidTransform.from_translation(t) + tf_expected = RigidTransform.from_translation(np.array(t)) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix()) + assert tf.single + + # Test multiple translations + t = [[1, 2, 3], [4, 5, 6]] + tf = RigidTransform.from_translation(t) + tf_expected = RigidTransform.from_translation(np.array(t)) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix()) + assert not tf.single + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.as_matrix) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_matrix(xp, ndim: int): + atol = 1e-12 + shape = (ndim,) * (ndim - 1) + (4, 4) + dtype = xpx.default_dtype(xp) + + matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + t = xp.reshape(xp.arange(ndim ** (ndim-1) * 3, dtype=dtype), shape[:-2] + (3,)) + matrix = xpx.at(matrix)[..., :3, 3].set(t) + + tf = RigidTransform.from_matrix(matrix) + xp_assert_close(tf.as_matrix(), matrix, atol=atol) + assert tf.single == (ndim == 1) + + # Test non-1 determinant + matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + matrix = xpx.at(matrix)[..., :3, :3].set(xp.eye(3) * 2.0) + tf = RigidTransform.from_matrix(matrix) + expected = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + xp_assert_close(tf.as_matrix(), expected, atol=atol) + + # Test non-orthogonal rotation matrix + matrix = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + # matrix is equivalent to [[1, 1, 0, 0], + # [0, 1, 0, 0], + # [0, 0, 1, 0], + # [0, 0, 0, 1]] + matrix = xpx.at(matrix)[..., 0, 1].set(1.0) + tf = RigidTransform.from_matrix(matrix) + expected = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + expected = xpx.at(expected)[..., 0, 0].set(0.894427) + expected = xpx.at(expected)[..., 0, 1].set(0.447214) + expected = xpx.at(expected)[..., 1, 0].set(-0.447214) + expected = xpx.at(expected)[..., 1, 1].set(0.894427) + xp_assert_close(tf.as_matrix(), expected, atol=1e-6) + + # Test invalid matrix + invalid = xp.tile(xp.eye(4), shape[:-2] + (1, 1)) + invalid = xpx.at(invalid)[..., 3, 3].set(2) # Invalid last row + if is_lazy_array(invalid): + tf = RigidTransform.from_matrix(invalid) + assert xp.all(xp.isnan(tf.as_matrix())) + else: + with pytest.raises(ValueError): + RigidTransform.from_matrix(invalid) + + +def test_from_matrix_array_like(): + # Test single transform matrix + matrix = [[1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]] + expected = np.eye(4) + tf = RigidTransform.from_matrix(matrix) + xp_assert_close(tf.as_matrix(), expected) + assert tf.single + + # Test multiple transform matrices + matrices = [matrix, matrix] + tf = RigidTransform.from_matrix(matrices) + for i in range(len(matrices)): + xp_assert_close(tf.as_matrix()[i, ...], expected) + assert not tf.single + + +@make_xp_test_case(RigidTransform.from_components) +@pytest.mark.parametrize("r_ndim", range(1, 3)) +@pytest.mark.parametrize("t_ndim", range(1, 3)) +def test_from_components(xp, r_ndim: int, t_ndim: int): + atol = 1e-12 + dims = (6, 5, 4, 3) # Common shape + q_shape = dims[:r_ndim - 1][::-1] + (4,) + t_shape = dims[:t_ndim - 1][::-1] + (3,) + tf_shape = np.broadcast_shapes(q_shape[:-1], t_shape[:-1]) + (4, 4) + rng = np.random.default_rng(0) + + t = xp.reshape(xp.arange(np.prod(t_shape[:-1]) * 3), t_shape) + r = rotation_to_xp(Rotation.from_quat(rng.random(size=q_shape)), xp=xp) + tf = RigidTransform.from_components(t, r) + + expected = xp.zeros(tf_shape) + expected = xpx.at(expected)[..., :3, :3].set(r.as_matrix()) + t_float = xp_promote(t, force_floating=True, xp=xp) + expected = xpx.at(expected)[..., :3, 3].set(t_float) + expected = xpx.at(expected)[..., 3, 3].set(1) + xp_assert_close(tf.as_matrix(), expected, atol=atol) + assert tf.single == (r_ndim == 1 and t_ndim == 1) + + +def test_from_components_array_like(): + rng = np.random.default_rng(123) + # Test single rotation and translation + t = [1, 2, 3] + r = Rotation.random(rng=rng) + tf = RigidTransform.from_components(t, r) + tf_expected = RigidTransform.from_components(np.array(t), r) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + assert tf.single + + # Test multiple rotations and translations + t = [[1, 2, 3], [4, 5, 6]] + r = Rotation.random(len(t), rng=rng) + tf = RigidTransform.from_components(t, r) + tf_expected = RigidTransform.from_components(np.array(t), r) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + assert not tf.single + + +@make_xp_test_case(RigidTransform.as_components) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_components(xp, ndim): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + shape = (ndim,) * (ndim - 1) + rng = np.random.default_rng(123) + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + r = rotation_to_xp(Rotation.from_quat(rng.random(shape + (4,))), xp=xp) + tf = RigidTransform.from_components(t, r) + new_t, new_r = tf.as_components() + assert xp.all(new_r.approx_equal(r, atol=atol)) + xp_assert_close(new_t, t, atol=atol) + + +@make_xp_test_case(RigidTransform.from_exp_coords) +@pytest.mark.parametrize("dim", range(1, 4)) +def test_from_exp_coords(xp, dim: int): + shape = (dim,) * (dim - 1) + # example from 3.3 of + # https://hades.mech.northwestern.edu/images/2/25/MR-v2.pdf + dtype = xpx.default_dtype(xp) + angle1 = np.deg2rad(30.0) + mat = xp.asarray([ + [np.cos(angle1), -np.sin(angle1), 0.0, 1.0], + [np.sin(angle1), np.cos(angle1), 0.0, 2.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ], dtype=dtype) + mat = xp.tile(mat, shape + (1, 1)) + tf1 = RigidTransform.from_matrix(mat) + angle2 = np.deg2rad(60.0) + mat = xp.asarray([ + [np.cos(angle2), -np.sin(angle2), 0.0, 2.0], + [np.sin(angle2), np.cos(angle2), 0.0, 1.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ], dtype=dtype) + mat = xp.tile(mat, shape + (1, 1)) + tf2 = RigidTransform.from_matrix(mat) + expected = tf2 * tf1.inv() + deg2rag = xp.asarray(np.deg2rad(30.0)) + exp_coords = deg2rag * xp.asarray([0.0, 0.0, 1.0, 3.37, -3.37, 0.0]) + exp_coords = xp.tile(exp_coords, shape + (1,)) + actual = RigidTransform.from_exp_coords(exp_coords) + xp_assert_close(actual.as_matrix(), expected.as_matrix(), atol=1e-2) + + # test cases generated by comparison to pytransform3d + exp_coords = xp.asarray([ + [-2.01041204, -0.52983629, 0.65773501, + 0.10386614, 0.05855009, 0.54959179], + [-0.22537438, -0.24132627, -2.4747121, + -0.09158594, 1.88075832, -0.03197204] + ]) + exp_coords = xp.tile(exp_coords, shape + (1,)) + expected_matrix = xp.asarray([ + [[0.76406621, 0.10504613, -0.63652819, -0.10209961], + [0.59956454, -0.47987325, 0.64050295, 0.40158789], + [-0.2381705, -0.87102639, -0.42963687, 0.19637636], + [0., 0., 0., 1.]], + [[-0.78446989, 0.61157488, 0.10287448, 1.33330055], + [-0.58017785, -0.78232107, 0.22664378, 0.52660831], + [0.21909052, 0.11810973, 0.96852952, -0.02968529], + [0., 0., 0., 1.]] + ]) + expected_matrix = xp.tile(expected_matrix, shape + (1, 1)) + xp_assert_close( + RigidTransform.from_exp_coords(exp_coords).as_matrix(), + expected_matrix, atol=1e-8) + + # identity + expected_matrix = xp.tile(xp.eye(4), shape + (1, 1)) + exp_coords = xp.zeros(shape + (6,), dtype=dtype) + xp_assert_close( + RigidTransform.from_exp_coords(exp_coords).as_matrix(), + expected_matrix, atol=1e-12) + + # only translation + expected_matrix = xp.asarray([ + [[1.0, 0.0, 0.0, 3.0], + [0.0, 1.0, 0.0, -5.4], + [0.0, 0.0, 1.0, 100.2], + [0.0, 0.0, 0.0, 1.0]], + [[1.0, 0.0, 0.0, -3.0], + [0.0, 1.0, 0.0, 13.3], + [0.0, 0.0, 1.0, 1.3], + [0.0, 0.0, 0.0, 1.0]] + ]) + expected_matrix = xp.tile(expected_matrix, shape + (1, 1, 1)) + exp_coords = xp.asarray([ + [0.0, 0.0, 0.0, 3.0, -5.4, 100.2], + [0.0, 0.0, 0.0, -3.0, 13.3, 1.3], + ]) + exp_coords = xp.tile(exp_coords, shape + (1, 1)) + actual = RigidTransform.from_exp_coords(exp_coords) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12) + + # only rotation + angles = xp.asarray([[34, -12, 0.5], [-102, -55, 30]]) + angles = xp.tile(angles, shape + (1, 1)) + rot = Rotation.from_euler('zyx', angles, degrees=True) + rotvec = rot.as_rotvec() + expected_matrix = xp.tile(xp.eye(4), shape + (2, 1, 1)) + expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(rot.as_matrix()) + exp_coords = xp.concat((rotvec, xp.zeros_like(rotvec)), axis=-1) + actual = RigidTransform.from_exp_coords(exp_coords) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=1e-12) + + +def test_from_exp_coords_array_like(): + rng = np.random.default_rng(123) + # Test single transform + t = np.array([1, 2, 3]) + r = Rotation.random(rng=rng) + tf_expected = RigidTransform.from_components(t, r) + exp_coords = tf_expected.as_exp_coords().tolist() + assert isinstance(exp_coords, list) + tf = RigidTransform.from_exp_coords(exp_coords) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + + # Test multiple transforms + t = [[1, 2, 3], [4, 5, 6]] + r = Rotation.random(len(t), rng=rng) + tf_expected = RigidTransform.from_components(t, r) + exp_coords = tf_expected.as_exp_coords().tolist() + assert isinstance(exp_coords, list) + tf = RigidTransform.from_exp_coords(exp_coords) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + + +@make_xp_test_case(RigidTransform.as_exp_coords) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_exp_coords(xp, ndim: int): + shape = (ndim,) * (ndim - 1) + # identity + expected = xp.zeros(shape + (6,)) + actual = RigidTransform.from_exp_coords(expected).as_exp_coords() + xp_assert_close(actual, expected, atol=1e-12) + + rng = np.random.default_rng(10) + + # pure rotation + rot_vec = xp.asarray(rng.normal(scale=0.1, size=shape + (1000, 3))) + tf = RigidTransform.from_rotation(Rotation.from_rotvec(rot_vec)) + exp_coords = tf.as_exp_coords() + xp_assert_close(exp_coords[..., :3], rot_vec, atol=1e-12) + expected = xp.zeros_like(rot_vec) + xp_assert_close(exp_coords[..., 3:], expected, atol=1e-16) + + # pure translation + translation = xp.asarray(rng.normal(scale=100.0, size=shape + (1000, 3))) + tf = RigidTransform.from_translation(translation) + exp_coords = tf.as_exp_coords() + xp_assert_close(exp_coords[..., :3], expected, atol=1e-16) + xp_assert_close(exp_coords[..., 3:], translation, atol=1e-15) + + +@make_xp_test_case(RigidTransform.from_dual_quat) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_dual_quat(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-7 + shape = (ndim,) * (ndim - 1) + + # identity + dq = xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype) + dq = xp.tile(dq, shape + (1,)) + expected = xp.tile(xp.eye(4), shape + (1, 1)) + xp_assert_close(RigidTransform.from_dual_quat(dq).as_matrix(), expected, atol=atol) + dq = xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype) + dq = xp.tile(dq, shape + (1,)) + xp_assert_close(RigidTransform.from_dual_quat(dq, scalar_first=True).as_matrix(), + expected, atol=atol) + + # only translation + dq = xp.asarray([0, 0, 0, 1, 0.25, 0.15, -0.7, 0], dtype=dtype) + dq = xp.tile(dq, shape + (1,)) + actual = RigidTransform.from_dual_quat(dq) + expected_matrix = xp.asarray([ + [1, 0, 0, 0.5], + [0, 1, 0, 0.3], + [0, 0, 1, -1.4], + [0, 0, 0, 1] + ]) + expected_matrix = xp.tile(expected_matrix, shape + (1, 1)) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol) + dq = xp.asarray([1, 0, 0, 0, 0, 0.25, 0.15, -0.7], dtype=dtype) + dq = xp.tile(dq, shape + (1,)) + actual = RigidTransform.from_dual_quat(dq, scalar_first=True) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol) + + # only rotation + angles = xp.asarray([65, -13, 90], dtype=dtype) + angles = xp.tile(angles, shape + (1,)) + actual_rot = Rotation.from_euler("xyz", angles, degrees=True) + qrot = actual_rot.as_quat() + dq = xp.concat((qrot, xp.zeros_like(qrot)), axis=-1) + actual = RigidTransform.from_dual_quat(dq) + expected_matrix = xp.tile(xp.eye(4), shape + (1, 1)) + expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix()) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol) + + qrot = actual_rot.as_quat(scalar_first=True) + dq = xp.concat((qrot, xp.zeros_like(qrot)), axis=-1) + actual = RigidTransform.from_dual_quat(dq, scalar_first=True) + expected_matrix = xp.tile(xp.eye(4), shape + (1, 1)) + expected_matrix = xpx.at(expected_matrix)[..., :3, :3].set(actual_rot.as_matrix()) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol) + + # rotation and translation + # rtol is set to 1e-7 because xp_assert_close deviates from + # np.testing.assert_allclose in that it does not automatically default to 1e-7 for + # floating point inputs. + # See https://numpy.org/doc/2.2/reference/generated/numpy.testing.assert_allclose.html + dq = xp.asarray( + [[0.0617101, -0.06483886, 0.31432811, 0.94508498, + 0.04985168, -0.26119618, 0.1691491, -0.07743254], + [0.19507259, 0.49404931, -0.06091285, 0.8450749, + 0.65049656, -0.30782513, 0.16566752, 0.04174544]]) + dq = xp.tile(dq, shape + (1, 1)) + actual = RigidTransform.from_dual_quat(dq) + expected_matrix = xp.asarray( + [[[0.79398752, -0.60213598, -0.08376202, 0.24605262], + [0.58613113, 0.79477941, -0.15740392, -0.4932833], + [0.16135089, 0.07588122, 0.98397557, 0.34262676], + [0., 0., 0., 1.]], + [[0.50440981, 0.2957028, 0.81125249, 1.20934468], + [0.08979911, 0.91647262, -0.3898898, -0.70540077], + [-0.8587822, 0.26951399, 0.43572393, -0.47776265], + [0., 0., 0., 1.]]]) + expected_matrix = xp.tile(expected_matrix, shape + (1, 1, 1)) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol, rtol=1e-7) + + dq = xp.asarray( + [[0.94508498, 0.0617101, -0.06483886, 0.31432811, + -0.07743254, 0.04985168, -0.26119618, 0.1691491], + [0.8450749, 0.19507259, 0.49404931, -0.06091285, + 0.04174544, 0.65049656, -0.30782513, 0.16566752]]) + dq = xp.tile(dq, shape + (1, 1)) + actual = RigidTransform.from_dual_quat(dq, scalar_first=True) + xp_assert_close(actual.as_matrix(), expected_matrix, atol=atol, rtol=1e-7) + + # unnormalized dual quaternions + + # invalid real quaternion with norm 0 + dq = xp.zeros(shape + (8,)) + actual = RigidTransform.from_dual_quat(dq) + expected = xp.tile(xp.eye(4), shape + (1, 1)) + xp_assert_close(actual.as_matrix(), expected, atol=atol) + + # real quaternion with norm != 1 + unnormalized_dual_quat = xp.asarray( + [-0.2547655, 1.23506123, 0.20230088, 0.24247194, # norm 1.3 + 0.38559628, 0.08184063, 0.1755943, -0.1582222] # orthogonal + ) + xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.3)[()], + atol=atol) + xp_assert_close(xp.vecdot(unnormalized_dual_quat[:4], + unnormalized_dual_quat[4:])[()], + xp.asarray(0.0)[()], atol=1e-8) + + dq = xp.tile(unnormalized_dual_quat, shape + (1,)) + dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat() + + expected_ones = xp.ones(shape) if shape != () else xp.asarray(1.0)[()] + expected_zeros = xp.zeros(shape) if shape != () else xp.asarray(0.0)[()] + xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones, + atol=1e-12) + vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:]) + vecdot = vecdot[()] if vecdot.shape == () else vecdot + xp_assert_close(vecdot, expected_zeros, atol=atol) + + # real and dual quaternion are not orthogonal + unnormalized_dual_quat = xp.asarray( + [0.20824458, 0.75098079, 0.54542913, -0.30849493, # unit norm + -0.16051025, 0.10742978, 0.21277201, 0.20596935] # not orthogonal + ) + xp_assert_close(xp_vector_norm(unnormalized_dual_quat[:4]), xp.asarray(1.0)[()], + atol=atol) + assert xp.vecdot(unnormalized_dual_quat[:4], unnormalized_dual_quat[4:]) != 0.0 + dq = xp.tile(unnormalized_dual_quat, shape + (1,)) + dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat() + + xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones, + atol=1e-12) + vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:]) + vecdot = vecdot[()] if vecdot.shape == () else vecdot + xp_assert_close(vecdot, expected_zeros, atol=atol) + + # invalid real quaternion with norm 0, non-orthogonal dual quaternion + unnormalized_dual_quat = xp.asarray( + [0.0, 0.0, 0.0, 0.0, -0.16051025, 0.10742978, 0.21277201, 0.20596935]) + assert xp.vecdot(xp.asarray([0.0, 0, 0, 1]), unnormalized_dual_quat[4:]) != 0.0 + dq = xp.tile(unnormalized_dual_quat, shape + (1,)) + dual_quat = RigidTransform.from_dual_quat(dq).as_dual_quat() + + xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected_ones, + atol=1e-12) + vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:]) + vecdot = vecdot[()] if vecdot.shape == () else vecdot + xp_assert_close(vecdot, expected_zeros, atol=atol) + + # compensation for precision loss in real quaternion + rng = np.random.default_rng(1000) + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype) + r = Rotation.from_quat(q) + random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat() + + # ensure that random quaternions are not normalized + random_dual_quats = xpx.at(random_dual_quats)[..., :4].add(0.01) + assert not xp.any(xpx.isclose(xp_vector_norm(random_dual_quats[..., :4], axis=-1), + 1.0, atol=0.0001)) + dual_quat_norm = RigidTransform.from_dual_quat( + random_dual_quats).as_dual_quat() + xp_assert_close(xp_vector_norm(dual_quat_norm[..., :4], axis=-1), expected_ones, + atol=atol) + + # compensation for precision loss in dual quaternion, results in violation + # of orthogonality constraint + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype) + r = Rotation.from_quat(q) + random_dual_quats = RigidTransform.from_components(t, r).as_dual_quat() + + # ensure that random quaternions are not normalized + random_dual_quats = xpx.at(random_dual_quats)[..., 4:].add(0.1) + q_norm = xp.vecdot(random_dual_quats[..., :4], random_dual_quats[..., 4:]) + assert not xp.any(xpx.isclose(q_norm, 0.0, atol=0.0001)) + dual_quat_norm = RigidTransform.from_dual_quat( + random_dual_quats).as_dual_quat() + vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:]) + vecdot = vecdot[()] if vecdot.shape == () else vecdot + xp_assert_close(vecdot, expected_zeros, atol=atol) + xp_assert_close(random_dual_quats[..., :4], dual_quat_norm[..., :4], atol=atol) + + +def test_from_dual_quat_array_like(): + rng = np.random.default_rng(123) + # Test single transform + t = np.array([1, 2, 3]) + r = Rotation.random(rng=rng) + tf_expected = RigidTransform.from_components(t, r) + dual_quat = tf_expected.as_dual_quat().tolist() + assert isinstance(dual_quat, list) + tf = RigidTransform.from_dual_quat(dual_quat) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + + # Test multiple transforms + t = [[1, 2, 3], [4, 5, 6]] + r = Rotation.random(len(t), rng=rng) + tf_expected = RigidTransform.from_components(t, r) + dual_quat = tf_expected.as_dual_quat().tolist() + assert isinstance(dual_quat, list) + tf = RigidTransform.from_dual_quat(dual_quat) + xp_assert_close(tf.as_matrix(), tf_expected.as_matrix(), atol=1e-12) + + +@make_xp_test_case(RigidTransform.as_dual_quat) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_dual_quat(xp, ndim: int): + dtype = xpx.default_dtype(xp) + shape = (ndim,) * (ndim - 1) + # identity + expected = xp.asarray([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0], dtype=dtype) + actual = rigid_transform_to_xp(RigidTransform.identity(), xp).as_dual_quat() + xp_assert_close(actual, expected, atol=1e-12) + + expected = xp.asarray([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + tf = rigid_transform_to_xp(RigidTransform.identity(), xp) + actual = tf.as_dual_quat(scalar_first=True) + xp_assert_close(actual, expected, atol=1e-12) + + rng = np.random.default_rng(10) + + # only rotation + for _ in range(10): + q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype) + real_part = Rotation.from_quat(q).as_quat() + dual_part = xp.zeros_like(real_part) + expected = xp.concat((real_part, dual_part), axis=-1) + actual = RigidTransform.from_dual_quat(expected).as_dual_quat() + # because of double cover: + actual = actual * xp.sign(actual[..., 0, None]) + expected = expected * xp.sign(expected[..., 0, None]) + xp_assert_close(actual, expected, atol=1e-12) + + # only translation + for _ in range(10): + tf = 0.5 * xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + expected = xp.zeros(shape + (8,), dtype=dtype) + expected = xpx.at(expected)[..., 3].set(1.0) + expected = xpx.at(expected)[..., 4:7].set(tf) + actual = RigidTransform.from_dual_quat(expected).as_dual_quat() + # because of double cover: + actual = actual * xp.sign(actual[..., 0, None]) + expected = expected * xp.sign(expected[..., 0, None]) + xp_assert_close(actual, expected, atol=1e-12) + + # rotation and translation + for _ in range(10): + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)) + expected = RigidTransform.from_components(t, r).as_dual_quat() + actual = RigidTransform.from_dual_quat(expected).as_dual_quat() + # because of double cover: + actual = actual * xp.sign(actual[..., 0, None]) + expected = expected * xp.sign(expected[..., 0, None]) + xp_assert_close(actual, expected, atol=1e-12) + + +@make_xp_test_case(RigidTransform.from_components, RigidTransform.as_components, + RigidTransform.from_exp_coords, RigidTransform.as_exp_coords, + RigidTransform.from_matrix, RigidTransform.as_matrix, + RigidTransform.from_dual_quat, RigidTransform.as_dual_quat) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_as_internal_consistency(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-12 + n = 10 + rng = np.random.default_rng(10) + shape = (n,) + (ndim,) * (ndim - 1) + + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)) + tf0 = RigidTransform.from_components(t, r) + + tf1 = RigidTransform.from_components(*tf0.as_components()) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + tf1 = RigidTransform.from_components(tf0.translation, tf0.rotation) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords()) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + tf1 = RigidTransform.from_matrix(tf0.as_matrix()) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + tf1 = RigidTransform.from_dual_quat(tf0.as_dual_quat()) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + # exp_coords small rotation + t = xp.asarray(rng.normal(scale=1000.0, size=shape + (3,)), dtype=dtype) + rotvec = xp.asarray(rng.normal(scale=1e-10, size=shape + (3,)), dtype=dtype) + r = Rotation.from_rotvec(rotvec) + tf0 = RigidTransform.from_components(t, r) + tf1 = RigidTransform.from_exp_coords(tf0.as_exp_coords()) + xp_assert_close(tf0.as_matrix(), tf1.as_matrix(), atol=atol) + + +def test_identity(): + # We do not use xp here because identity always returns numpy arrays + atol = 1e-12 + # Test single identity + tf = RigidTransform.identity() + xp_assert_close(tf.as_matrix(), np.eye(4), atol=atol) + # Test multiple identities + tf = RigidTransform.identity(5) + xp_assert_close(tf.as_matrix(), np.array([np.eye(4)] * 5), atol=atol) + # Test shape + tf = RigidTransform.identity(shape=3) + expected = np.tile(np.eye(4), (3, 1, 1)) + xp_assert_close(tf.as_matrix(), expected, atol=atol) + tf = RigidTransform.identity(shape=(2, 3)) + expected = np.tile(np.eye(4), (2, 3, 1, 1)) + xp_assert_close(tf.as_matrix(), expected, atol=atol) + # Test errors + with pytest.raises(ValueError, match="Only one of `num` and `shape` can be."): + RigidTransform.identity(10, shape=(2, 3)) + with pytest.raises(TypeError, match="takes from 0 to 1 positional arguments"): + RigidTransform.identity(None, (-1, 3)) # Shape is kwarg only + with pytest.raises(ValueError, match="`shape` must be an int or a tuple of ints"): + RigidTransform.identity(shape="invalid") + + +@make_xp_test_case(RigidTransform.apply) +def test_apply(xp): + atol = 1e-12 + # Broadcast shape: (6, 5, 4, 2) ( + (3,) for vectors, + (4,) for rotations) + vector_shapes = [(), (1,), (2,), (1, 2), (5, 1, 2)] + tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (1, 4, 2), (5, 4, 2), (6, 5, 4, 2)] + rng = np.random.default_rng(123) + + for tf_shape, v_shape in product(tf_shapes, vector_shapes): + # Random rotation and translation + t = xp.asarray(rng.normal(size=tf_shape + (3,))) + q = xp.asarray(rng.normal(size=tf_shape + (4,))) + r = Rotation.from_quat(q) + tf = RigidTransform.from_components(t, r) + + vecs = xp.asarray(rng.normal(size=v_shape + (3,))) + expected = t + r.apply(vecs) + res = tf.apply(vecs) + assert res.shape == np.broadcast_shapes(tf_shape, v_shape) + (3,) + xp_assert_close(res, expected, atol=atol) + + +def test_apply_array_like(): + rng = np.random.default_rng(123) + # Single vector + t = np.array([1, 2, 3]) + r = Rotation.random(rng=rng) + tf = RigidTransform.from_components(t, r) + vec = [1, 0, 0] + expected = t + r.apply(vec) + xp_assert_close(tf.apply(vec), expected, atol=1e-12) + + # Multiple vectors + t = np.array([[1, 2, 3], [4, 5, 6]]) + r = Rotation.random(len(t), rng=rng) + tf = RigidTransform.from_components(t, r) + vec = [[1, 0, 0], [0, 1, 0]] + expected = t + r.apply(vec) + xp_assert_close(tf.apply(vec), expected, atol=1e-12) + + +@make_xp_test_case(RigidTransform.apply) +def test_inverse_apply(xp): + atol = 1e-12 + # Broadcast shape: (6, 5, 4, 2) ( + (3,) for vectors, + (4,) for rotations) + vector_shapes = [(), (1,), (2,), (1, 2), (5, 1, 2)] + tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (1, 4, 2), (5, 4, 2), (6, 5, 4, 2)] + rng = np.random.default_rng(123) + + for tf_shape, v_shape in product(tf_shapes, vector_shapes): + # Random rotation and translation + t = xp.asarray(rng.normal(size=tf_shape + (3,))) + q = xp.asarray(rng.normal(size=tf_shape + (4,))) + r = Rotation.from_quat(q) + tf = RigidTransform.from_components(t, r) + + vecs = xp.asarray(rng.normal(size=v_shape + (3,))) + expected = tf.inv().apply(vecs) + res = tf.apply(vecs, inverse=True) + assert res.shape == np.broadcast_shapes(tf_shape, v_shape) + (3,) + xp_assert_close(res, expected, atol=atol) + + +@make_xp_test_case(RigidTransform.apply) +def test_rotation_alone(xp): + atol = 1e-12 + + r = Rotation.from_euler('z', xp.asarray(90), degrees=True) + tf = RigidTransform.from_rotation(r) + vec = xp.asarray([1, 0, 0]) + expected = r.apply(vec) + xp_assert_close(tf.apply(vec), expected, atol=atol) + + +@make_xp_test_case(RigidTransform.apply) +def test_translation_alone(xp): + atol = 1e-12 + t = xp.asarray([1.0, 2, 3]) + tf = RigidTransform.from_translation(t) + vec = xp.asarray([5.0, 6, 7]) + expected = t + vec + xp_assert_close(tf.apply(vec), expected, atol=atol) + + +@make_xp_test_case(RigidTransform.apply, RigidTransform.__mul__) +def test_composition(xp): + atol = 1e-12 + tf_shapes = [(), (1,), (2,), (1, 2), (4, 2), (5, 4, 2)] + dtype = xpx.default_dtype(xp) + rng = np.random.default_rng(123) + + for tf_shape1, tf_shape2 in product(tf_shapes, repeat=2): + # Random rotation and translation + t1 = xp.asarray(rng.normal(size=tf_shape1 + (3,))) + q1 = xp.asarray(rng.normal(size=tf_shape1 + (4,))) + r1 = Rotation.from_quat(q1) + tf1 = RigidTransform.from_components(t1, r1) + + t2 = xp.asarray(rng.normal(size=tf_shape2 + (3,))) + q2 = xp.asarray(rng.normal(size=tf_shape2 + (4,))) + r2 = Rotation.from_quat(q2) + tf2 = RigidTransform.from_components(t2, r2) + + composed = tf2 * tf1 + vec = xp.asarray(rng.normal(size=(3,)), dtype=dtype) + expected = tf2.apply(tf1.apply(vec)) + res = composed.apply(vec) + assert res.shape == np.broadcast_shapes(tf_shape1, tf_shape2) + (3,) + xp_assert_close(res, expected, atol=atol) + + expected = t2 + r2.apply(t1 + r1.apply(vec)) + xp_assert_close(composed.apply(vec), expected, atol=atol) + assert composed.single == (tf1.single and tf2.single) + + +@make_xp_test_case(RigidTransform.__pow__, RigidTransform.__mul__) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_pow(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + num = 10 + rng = np.random.default_rng(100) + shape = (num,) + (ndim,) * (ndim - 1) + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + q = xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype) + r = Rotation.from_quat(q) + p = RigidTransform.from_components(t, r) + p_inv = p.inv() + + # Test the short-cuts and other integers + for n in [-5, -2, -1, 0, 1, 2, 5]: + q = p**n + # Regression test for gh-24436 + assert isinstance(q._matrix, type(p._matrix)) + r = RigidTransform.from_matrix(xp.tile(xp.eye(4), shape + (1, 1))) + for _ in range(abs(n)): + if n > 0: + r = r * p + else: + r = r * p_inv + xp_assert_close(q.as_matrix(), r.as_matrix(), atol=atol) + + # Test shape preservation of single + single_tf = RigidTransform.identity() + assert (single_tf**n).as_matrix().shape == (4, 4) + + # Test fractional powers + q = p**0.5 + xp_assert_close((q * q).as_matrix(), p.as_matrix(), atol=atol) + q = p**-0.5 + xp_assert_close((q * q).as_matrix(), p.inv().as_matrix(), atol=atol) + q = p** 1.5 + xp_assert_close((q * q).as_matrix(), (p**3).as_matrix(), atol=atol) + q = p** -1.5 + xp_assert_close((q * q).as_matrix(), (p**-3).as_matrix(), atol=atol) + + # pow function + tf = pow(RigidTransform.from_matrix(xp.eye(4)), 2) + xp_assert_close(tf.as_matrix(), xp.eye(4), atol=atol) + + +@make_xp_test_case(RigidTransform.__pow__) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_pow_equivalence_with_rotation(xp, ndim: int): + atol = 1e-12 + num = 10 + rng = np.random.default_rng(100) + dtype = xpx.default_dtype(xp) + shape = (num,) + (ndim,) * (ndim - 1) + + r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)) + p = RigidTransform.from_rotation(r) + for n in [-5, -2, -1.5, -1, -0.5, 0.0, 0.5, 1, 1.5, 2, 5]: + xp_assert_close((p**n).rotation.as_matrix(), (r**n).as_matrix(), atol=atol) + + +@make_xp_test_case(RigidTransform.inv, RigidTransform.__mul__) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_inverse(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + rng = np.random.default_rng(100) + shape = (ndim,) * (ndim - 1) + + # Test inverse transform + r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)) + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + tf = RigidTransform.from_components(t, r) + + # Test that tf * tf.inv() equals identity + tf_inv = tf.inv() + composed = tf * tf_inv + expected = xp.tile(xp.eye(4), shape + (1, 1)) + xp_assert_close(composed.as_matrix(), expected, atol=atol) + + +@make_xp_test_case(RigidTransform.as_matrix) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_properties(xp, ndim: int): + atol = 1e-12 if xpx.default_dtype(xp) == xp.float64 else 1e-6 + shape = (ndim,) * (ndim - 1) + dtype = xpx.default_dtype(xp) + rng = np.random.default_rng(100) + + # Test rotation and translation properties + r = Rotation.from_quat(xp.asarray(rng.normal(size=shape + (4,)), dtype=dtype)) + t = xp.asarray(rng.normal(size=shape + (3,)), dtype=dtype) + tf = RigidTransform.from_components(t, r) + + xp_assert_close(tf.rotation.as_matrix(), r.as_matrix(), atol=atol) + assert xp.all(tf.rotation.approx_equal(r, atol=atol)) + xp_assert_close(tf.translation, t, atol=atol) + # Test that we don't return views that would modify the original array + xpx.at(tf.translation)[..., 0].set(0.0) + xp_assert_close(tf.translation, t, atol=atol) + assert tf.single == (shape == ()) + + +@make_xp_test_case(RigidTransform.__getitem__) +def test_indexing(xp): + atol = 1e-12 + + # Test indexing for multiple transforms + r = Rotation.from_euler('zyx', xp.asarray([[90, 0, 0], [0, 90, 0]]), degrees=True) + t = xp.asarray([[1.0, 2, 3], [4, 5, 6]]) + tf = RigidTransform.from_components(t, r) + + # Test single index + xp_assert_close(tf[0].as_matrix()[:3, :3], r[0].as_matrix(), atol=atol) + xp_assert_close(tf[0].as_matrix()[:3, 3], t[0, ...], atol=atol) + + # Test slice + tf_slice = tf[0:2] + xp_assert_close(tf_slice.as_matrix()[:, :3, :3], r[0:2].as_matrix(), atol=atol) + xp_assert_close(tf_slice.as_matrix()[:, :3, 3], t[0:2, ...], atol=atol) + + # Test boolean indexing + tf_masked = tf[xp.asarray([True, True])] + xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r.as_matrix(), atol=atol) + xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t, atol=atol) + + tf_masked = tf[xp.asarray([False, True])] + xp_assert_close(tf_masked.as_matrix()[:, :3, :3], + r[xp.asarray([False, True])].as_matrix(), atol=atol) + xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[xp.asarray([False, True])], + atol=atol) + + tf_masked = tf[xp.asarray([False, False])] + assert len(tf_masked) == 0 + + # Test integer array indexing + idx = xp.asarray([0, 1]) + xp_assert_close(tf[idx].as_matrix()[:, :3, :3], r[idx].as_matrix(), atol=atol) + xp_assert_close(tf[idx].as_matrix()[:, :3, 3], t, atol=atol) + + +def test_indexing_array_like(): + atol = 1e-12 + + r = Rotation.from_euler('zyx', np.array([[90, 0, 0], [0, 90, 0]]), degrees=True) + t = np.array([[1.0, 2, 3], [4, 5, 6]]) + tf = RigidTransform.from_components(t, r) + + tf_masked = tf[[False, True]] + xp_assert_close(tf_masked.as_matrix()[:, :3, :3], r[[False, True]].as_matrix(), + atol=atol) + xp_assert_close(tf_masked.as_matrix()[:, :3, 3], t[[False, True]], atol=atol) + tf_masked = tf[[False, False]] + assert len(tf_masked) == 0 + + +@make_xp_test_case(RigidTransform.concatenate) +def test_concatenate(xp): + atol = 1e-12 + + # Test concatenation of transforms + t1 = xp.asarray([1, 0, 0]) + r1 = Rotation.from_euler('z', xp.asarray(90), degrees=True) + tf1 = RigidTransform.from_components(t1, r1) + + t2 = xp.asarray([0, 1, 0]) + r2 = Rotation.from_euler('x', xp.asarray(90), degrees=True) + tf2 = RigidTransform.from_components(t2, r2) + + # Concatenate single transforms + concatenated1 = RigidTransform.concatenate([tf1, tf2]) + xp_assert_close(concatenated1[0].as_matrix(), tf1.as_matrix(), atol=atol) + xp_assert_close(concatenated1[1].as_matrix(), tf2.as_matrix(), atol=atol) + + # Concatenate multiple transforms + concatenated2 = RigidTransform.concatenate([tf1, concatenated1]) + xp_assert_close(concatenated2[0].as_matrix(), tf1.as_matrix(), atol=atol) + xp_assert_close(concatenated2[1].as_matrix(), tf1.as_matrix(), atol=atol) + xp_assert_close(concatenated2[2].as_matrix(), tf2.as_matrix(), atol=atol) + + # Test ND concatenation + tf3 = RigidTransform.from_translation(xp.reshape(xp.arange(18), (3, 2, 3))) + tf4 = RigidTransform.from_translation(xp.reshape(xp.arange(18) + 18, (3, 2, 3))) + concatenated3 = RigidTransform.concatenate([tf3, tf4]) + xp_assert_close(concatenated3.as_matrix()[:3, ...], tf3.as_matrix(), atol=atol) + xp_assert_close(concatenated3.as_matrix()[3:, ...], tf4.as_matrix(), atol=atol) + + +@make_xp_test_case(RigidTransform.from_matrix) +def test_input_validation(xp): + # Test invalid matrix shapes + inputs = [xp.eye(3), xp.zeros((4, 3)), []] + for input in inputs: + with pytest.raises(ValueError, match="Expected `matrix` to have shape"): + RigidTransform.from_matrix(input) + + # Test invalid last row + for ndim in range(3): + shape = (ndim,) * (ndim - 1) + matrix = xp.zeros(shape + (4, 4)) + matrix = xpx.at(matrix)[...].set(xp.eye(4)) + matrix = xpx.at(matrix)[..., 3, :].set(xp.asarray([1.0, 0, 0, 1])) + if is_lazy_array(matrix): + matrix = RigidTransform.from_matrix(matrix).as_matrix() + assert xp.all(xp.isnan(matrix[..., 3, :])) + else: + with pytest.raises(ValueError, match="last row of transformation matrix"): + RigidTransform.from_matrix(matrix) + + # Test left handed rotation matrix + matrix = xp.eye(4) + matrix = xpx.at(matrix)[0, 0].set(-1) + if is_lazy_array(matrix): + matrix = RigidTransform.from_matrix(matrix).as_matrix() + assert xp.all(xp.isnan(matrix[..., :3, :3])) + else: + with pytest.raises(ValueError, match="Non-positive determinant"): + RigidTransform(matrix, normalize=True) + + # Test non-Rotation input + with pytest.raises(TypeError, + match="Expected `rotation` to be a `Rotation` instance"): + RigidTransform.from_rotation(xp.eye(3)) + + +@make_xp_test_case(RigidTransform.mean) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_mean(xp, ndim: int): + atol = 1e-12 + rng = np.random.default_rng(123) + + dtype = xpx.default_dtype(xp) + t = xp.asarray(rng.normal(size=(ndim,) * (ndim - 1) + (3,)), dtype=dtype) + q = xp.asarray(rng.normal(size=(ndim,) * (ndim - 1) + (4,)), dtype=dtype) + r = Rotation.from_quat(q) + tf = RigidTransform.from_components(t, r) + + # Unweighted mean + axis = tuple(range(t.ndim - 1)) + t_mean = xp.mean(t, axis=axis) + r_mean = r.mean() + tf_mean = tf.mean() + assert tf_mean.shape == () + xp_assert_close(tf_mean.as_matrix(), + RigidTransform.from_components(t_mean, r_mean).as_matrix(), + atol=atol) + + # Weighted mean + if ndim == 1: + weights = None + t_mean = t + else: + weights = xp.asarray(rng.random(size=(ndim,) * (ndim - 1)), dtype=dtype) + norm = xp.sum(weights[..., None], axis=axis) + wsum = xp.sum(t * weights[..., None], axis=axis) + t_mean = wsum/norm + r_mean = r.mean(weights=weights) + tf_mean = tf.mean(weights=weights) + assert tf_mean.shape == () + xp_assert_close(tf_mean.as_matrix(), + RigidTransform.from_components(t_mean, r_mean).as_matrix(), + atol=atol) + + +@make_xp_test_case( + RigidTransform.from_rotation, RigidTransform.mean, Rotation.magnitude +) +@pytest.mark.parametrize("ndim", range(1, 5)) +def test_mean_axis(xp, ndim: int): + axes = xp.tile(xp.concat((-xp.eye(3), xp.eye(3))), (3,) * (ndim - 1) + (1, 1)) + theta = xp.pi / 4 + r = Rotation.from_rotvec(theta * axes) + tf = RigidTransform.from_rotation(r) + + # Test mean over last axis + desired = xp.full(axes.shape[:-2], 0.0) + if ndim == 1: + desired = desired[()] + atol = 1e-6 if xpx.default_dtype(xp) is xp.float32 else 1e-10 + xp_assert_close(tf.mean(axis=-1).rotation.magnitude(), desired, atol=atol) + + # Test tuple axes + desired = xp.full(axes.shape[1:-2], 0.0) + if ndim < 3: + desired = desired[()] + xp_assert_close(tf.mean(axis=(0, -1)).rotation.magnitude(), desired, atol=atol) + + # Empty axis tuple should return RigidTransform unchanged + tf_mean = tf.mean(axis=()) + xp_assert_close(tf_mean.as_matrix(), tf.as_matrix(), atol=atol) + + +@make_xp_test_case(RigidTransform.mean, Rotation.magnitude) +def test_mean_compare_axis(xp): + # Create a random set of transforms and compare the mean over an axis with + # the mean without axis of the sliced transform + atol = 1e-10 if xpx.default_dtype(xp) == xp.float64 else 1e-6 + rng = np.random.default_rng(0) + q = xp.asarray(rng.normal(size=(4, 5, 6, 4)), dtype=xpx.default_dtype(xp)) + r = Rotation.from_quat(q) + t = xp.asarray(rng.normal(size=(4, 5, 6, 3)), dtype=xpx.default_dtype(xp)) + tf = RigidTransform.from_components(t, r) + + mean_0 = tf.mean(axis=0) + for i in range(q.shape[1]): + for j in range(q.shape[2]): + r_slice = Rotation.from_quat(q[:, i, j, ...]) + t_slice = t[:, i, j, ...] + mean_slice_tf = RigidTransform.from_components(t_slice, r_slice).mean() + xp_assert_close( + (mean_0[i][j].rotation * mean_slice_tf.rotation.inv()).magnitude(), + xp.asarray(0.0)[()], atol=atol, + ) + xp_assert_close( + mean_0[i][j].translation, mean_slice_tf.translation, atol=atol, + ) + mean_1_2 = tf.mean(axis=(1, 2)) + for i in range(q.shape[0]): + r_slice = Rotation.from_quat(q[i, ...]) + t_slice = t[i, ...] + mean_slice_tf = RigidTransform.from_components(t_slice, r_slice).mean() + xp_assert_close( + (mean_1_2[i].rotation * mean_slice_tf.rotation.inv()).magnitude(), + xp.asarray(0.0)[()], atol=atol, + ) + xp_assert_close( + mean_1_2[i].translation, mean_slice_tf.translation, atol=atol, + ) + + +@make_xp_test_case(RigidTransform.mean) +def test_mean_invalid_weights(xp): + tf = RigidTransform.from_matrix(xp.tile(xp.eye(4), (4, 1, 1))) + if is_lazy_array(tf.as_matrix()): + m = tf.mean(weights=-xp.ones(4)) + assert xp.all(xp.isnan(m.as_matrix())) + else: + with pytest.raises(ValueError, match="non-negative"): + tf.mean(weights=-xp.ones(4)) + + # Test weight shape mismatch + tf = RigidTransform.from_matrix(xp.eye(4)) + with pytest.raises(ValueError, match="Expected `weights` to"): + tf.mean(weights=xp.ones((2,))) + tf = RigidTransform.from_matrix(xp.tile(xp.eye(4), (3, 2, 1, 1, 1))) + with pytest.raises(ValueError, match="Expected `weights` to"): + tf.mean(weights=xp.ones((2, 1))) + + +@make_xp_test_case(RigidTransform.from_translation) +def test_translation_validation(xp): + # Test invalid translation shapes + with pytest.raises(ValueError, match="Expected `translation` to have shape"): + RigidTransform.from_translation(xp.asarray([1, 2])) + + with pytest.raises(ValueError, match="Expected `translation` to have shape"): + RigidTransform.from_translation(xp.zeros((2, 2))) + + +@make_xp_test_case(RigidTransform.apply) +def test_vector_validation(xp): + tf = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp) + + # Test invalid vector shapes + with pytest.raises(ValueError, match="Expected vector to have shape"): + tf.apply(xp.asarray([1, 2])) + + with pytest.raises(ValueError, match="Expected vector to have shape"): + tf.apply(xp.zeros((2, 2))) + + with pytest.raises(ValueError, match="operands could not be broadcast"): + tf.apply(xp.zeros((1, 4, 3))) + + +@make_xp_test_case(RigidTransform.__getitem__) +def test_indexing_validation(xp): + tf = RigidTransform.from_matrix(xp.eye(4)) + + # Test indexing on single transform + with pytest.raises(TypeError, match="Single transform is not subscriptable"): + tf[0] + + with pytest.raises(TypeError, match="Single transform is not subscriptable"): + tf[0:1] + + # Test length on single transform + with pytest.raises(TypeError, match="Single transform has no len"): + len(tf) + + +@make_xp_test_case(RigidTransform.__mul__) +def test_composition_validation(xp): + tf2 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]])) + tf3 = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + + # Test incompatible shapes + with pytest.raises(ValueError, match="Cannot broadcast"): + tf2 * tf3 + + tf4 = RigidTransform.from_matrix(xp.tile(xp.eye(4), (1, 4, 1, 1))) + # Test invalid broadcasting shape + with pytest.raises(ValueError, match="Cannot broadcast"): + tf2 * tf4 + + +@make_xp_test_case(RigidTransform.concatenate) +def test_concatenate_validation(xp): + tf = RigidTransform.from_matrix(xp.eye(4)) + + # Test invalid inputs + with pytest.raises(TypeError, + match="input must contain RigidTransform objects"): + RigidTransform.concatenate([tf, xp.eye(4)]) + + # Test incompatible shapes + tf2 = RigidTransform.from_translation(xp.ones((1, 1, 1, 3))) + # Frameworks have a highly heterogeneous way of reporting errors for this case + with pytest.raises((ValueError, TypeError, RuntimeError)): + RigidTransform.concatenate([tf, tf2]) + + +@make_xp_test_case(RigidTransform.__setitem__) +def test_setitem(xp): + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + single = RigidTransform.from_translation(xp.asarray([1, 1, 1])) + double = RigidTransform.from_translation(xp.asarray([[2, 2, 2], [3, 3, 3]])) + triple = RigidTransform.from_translation(xp.asarray([[3, 3, 3], + [4, 4, 4], + [5, 5, 5]])) + + # Test indexing with integer index + tf[0] = single + xp_assert_close(tf.translation, xp.asarray([[1.0, 1, 1], [4, 5, 6], [7, 8, 9]])) + + # Test indexing with slice + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + tf[:2] = double + xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [3, 3, 3], [7, 8, 9]])) + + # Test indexing with ellipsis + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + tf[...] = triple + xp_assert_close(tf.translation, xp.asarray([[3.0, 3, 3], [4, 4, 4], [5, 5, 5]])) + + # Test indexing with boolean array + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + mask = xp.asarray([True, False, True]) + tf[mask] = double + xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [4, 5, 6], [3, 3, 3]])) + + +@make_xp_test_case(RigidTransform.__setitem__) +@pytest.mark.skip_xp_backends("array_api_strict", + reason="doesn't support fancy indexing __setitem__") +def test_setitem_fancy_indexing(xp): + double = RigidTransform.from_translation(xp.asarray([[2, 2, 2], [3, 3, 3]])) + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) + idx = xp.asarray([0, 2]) + tf[idx] = double + xp_assert_close(tf.translation, xp.asarray([[2.0, 2, 2], [4, 5, 6], [3, 3, 3]])) + + +@make_xp_test_case(RigidTransform.__setitem__) +def test_setitem_validation(xp): + tf = RigidTransform.from_translation(xp.asarray([[1, 2, 3], [4, 5, 6]])) + single = RigidTransform.from_matrix(xp.eye(4)) + + # Test setting item on single transform + with pytest.raises(TypeError, match="Single transform is not subscriptable"): + single[0] = tf + + # Test invalid value type + with pytest.raises(TypeError, match="value must be a RigidTransform"): + tf[0] = xp.eye(4) + + +@pytest.mark.skip_xp_backends("jax.numpy", + reason="JAX does not support memory sharing") +@make_xp_test_case(RigidTransform.as_matrix) +def test_copy_flag(xp): + # Test that copy=True creates new memory + matrix = xp.eye(4) + tf = RigidTransform(matrix, normalize=False, copy=True) + matrix[0, 0] = 2 + assert tf.as_matrix()[0, 0] == 1 + + # Test that copy=False shares memory + matrix = xp.eye(4) + tf = RigidTransform(matrix, normalize=False, copy=False) + matrix[0, 0] = 2 + assert tf.as_matrix()[0, 0] == 2 + + +@make_xp_test_case(normalize_dual_quaternion) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_normalize_dual_quaternion(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + rng = np.random.default_rng(100) + shape = (ndim,) * (ndim - 1) + + dual_quat = normalize_dual_quaternion(xp.zeros((1, 8))) + xp_assert_close(xp_vector_norm(dual_quat[0, :4], axis=-1), xp.asarray(1.0)[()], + atol=1e-12) + xp_assert_close(xp.vecdot(dual_quat[0, :4], dual_quat[0, 4:])[()], + xp.asarray(0.0)[()], atol=1e-12) + + dual_quat = xp.asarray(rng.normal(size=shape + (8,)), dtype=dtype) + dual_quat = normalize_dual_quaternion(dual_quat) + expected = xp.ones(shape) if shape != () else xp.asarray(1.0)[()] + xp_assert_close(xp_vector_norm(dual_quat[..., :4], axis=-1), expected, atol=atol) + expected = xp.zeros(shape) if shape != () else xp.asarray(0.0)[()] + vecdot = xp.vecdot(dual_quat[..., :4], dual_quat[..., 4:]) + vecdot = vecdot[()] if vecdot.shape == () else vecdot + xp_assert_close(vecdot, expected, atol=atol) + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.from_rotation, + RigidTransform.from_translation, RigidTransform.from_components, + RigidTransform.from_exp_coords, RigidTransform.from_dual_quat) +def test_empty_transform_construction(xp): + tf = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + assert len(tf) == 0 + assert not tf.single + + tf = RigidTransform.from_rotation(Rotation.from_quat(xp.zeros((0, 4)))) + assert len(tf) == 0 + assert not tf.single + + tf = RigidTransform.from_translation(xp.empty((0, 3))) + assert len(tf) == 0 + assert not tf.single + + empty_rot = Rotation.from_quat(xp.zeros((0, 4))) + tf = RigidTransform.from_components(xp.empty((0, 3)), empty_rot) + assert len(tf) == 0 + assert not tf.single + + tf = RigidTransform.from_exp_coords(xp.empty((0, 6))) + assert len(tf) == 0 + assert not tf.single + + tf = RigidTransform.from_dual_quat(xp.empty((0, 8))) + assert len(tf) == 0 + assert not tf.single + + tf = RigidTransform.identity(0) + assert len(tf) == 0 + assert not tf.single + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.as_components, + RigidTransform.as_exp_coords, RigidTransform.as_dual_quat) +def test_empty_transform_representation(xp): + tf = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + + assert len(tf.rotation) == 0 + assert tf.translation.shape == (0, 3) + + t, r = tf.as_components() + assert t.shape == (0, 3) + assert len(r) == 0 + + assert tf.as_matrix().shape == (0, 4, 4) + assert tf.as_exp_coords().shape == (0, 6) + assert tf.as_dual_quat().shape == (0, 8) + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.apply) +def test_empty_transform_application(xp): + tf = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + + assert tf.apply(xp.zeros((3,))).shape == (0, 3) + assert tf.apply(xp.empty((0, 3))).shape == (0, 3) + + with pytest.raises(ValueError, match="operands could not be broadcast together"): + tf.apply(xp.zeros((2, 3))) + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.__mul__) +def test_empty_transform_composition(xp): + tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + tf_single = RigidTransform.from_matrix(xp.eye(4)) + tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp) + + assert len(tf_empty * tf_empty) == 0 + assert len(tf_empty * tf_single) == 0 + assert len(tf_single * tf_empty) == 0 + + with pytest.raises(ValueError, match="Cannot broadcast"): + tf_many * tf_empty + + with pytest.raises(ValueError, match="Cannot broadcast"): + tf_empty * tf_many + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.concatenate) +def test_empty_transform_concatenation(xp): + tf_empty = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + tf_single = RigidTransform.from_matrix(xp.eye(4)) + tf_many = rigid_transform_to_xp(RigidTransform.identity(2), xp=xp) + + assert len(RigidTransform.concatenate([tf_empty, tf_empty])) == 0 + assert len(RigidTransform.concatenate([tf_empty, tf_single])) == 1 + assert len(RigidTransform.concatenate([tf_single, tf_empty])) == 1 + assert len(RigidTransform.concatenate([tf_empty, tf_many])) == 2 + assert len(RigidTransform.concatenate([tf_many, tf_empty])) == 2 + assert len(RigidTransform.concatenate([tf_many, tf_empty, tf_single])) == 3 + + +@make_xp_test_case(RigidTransform.from_matrix, RigidTransform.inv, + RigidTransform.__pow__) +def test_empty_transform_inv_and_pow(xp): + tf = RigidTransform.from_matrix(xp.empty((0, 4, 4))) + assert len(tf.inv()) == 0 + assert len(tf ** 0) == 0 + assert len(tf ** 1) == 0 + assert len(tf ** -1) == 0 + assert len(tf ** 0.5) == 0 + + +@make_xp_test_case(RigidTransform.__getitem__) +def test_empty_transform_indexing(xp): + tf_many = rigid_transform_to_xp(RigidTransform.identity(3), xp=xp) + tf_zero = tf_many[xp.asarray([], dtype=xp.int32)] + assert len(tf_zero) == 0 + + # Array API does not specify out-of-bounds indexing. Only check for numpy. + if is_numpy(xp): + assert len(tf_zero[:5]) == 0 # Slices can go out of bounds. + + with pytest.raises(IndexError): + tf_zero[0] + + with pytest.raises(IndexError): + tf_zero[xp.asarray([0, 2])] + + with pytest.raises(IndexError): + tf_zero[xp.asarray([False, True])] + + +@make_xp_test_case(RigidTransform.from_matrix) +@pytest.mark.skip_xp_backends("array_api_strict", + reason="array API doesn't support pickling") +def test_pickling(xp): + # Note: Array API makes no provision for arrays to be pickleable, so + # it's OK to skip this test for the backends that don't support it + mat = xp.eye(4) + mat = xpx.at(mat)[0, 3].set(2.0) + tf = RigidTransform.from_matrix(mat) + pkl = pickle.dumps(tf) + unpickled = pickle.loads(pkl) + xp_assert_close(tf.as_matrix(), unpickled.as_matrix(), atol=1e-15) + + +@make_xp_test_case(RigidTransform.as_matrix, RigidTransform.__iter__) +def test_rigid_transform_iter(xp): + r = rigid_transform_to_xp(RigidTransform.identity(3), xp) + for i, r_i in enumerate(r): + assert isinstance(r_i, RigidTransform) + xp_assert_equal(r_i.as_matrix(), r[i].as_matrix()) + if i > len(r): + raise RuntimeError("Iteration exceeded length of transforms") + + +@make_xp_test_case() +@pytest.mark.parametrize("dim", range(1, 5)) +def test_shape_property(xp, dim: int): + shape = (dim,) * (dim - 1) + tf = RigidTransform.from_translation(xp.zeros(shape + (3,))) + assert tf.shape == shape + + +def test_non_writeable(): + mat = np.eye(4) + mat.flags.writeable = False + RigidTransform.from_matrix(mat) # Regression test against gh-24378 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rotation.py b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rotation.py new file mode 100644 index 0000000000000000000000000000000000000000..4ec9f5ff08f7bba36f0923c049292a459dfcb51c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/scipy/spatial/transform/tests/test_rotation.py @@ -0,0 +1,3161 @@ +import math + +import pytest + +import numpy as np +from numpy.testing import assert_equal +from scipy.spatial.transform import Rotation, Slerp +import scipy.spatial.transform._rotation_cy as cython_backend +import scipy.spatial.transform._rotation_xp as xp_backend +from scipy.stats import special_ortho_group +from itertools import permutations, product +from contextlib import contextmanager +import warnings +from scipy._lib._array_api import ( + xp_assert_equal, + array_namespace, + is_numpy, + is_lazy_array, + xp_vector_norm, + xp_assert_close, + eager_warns, + xp_default_dtype, + make_xp_test_case, + make_xp_pytest_marks, + xp_device_type, +) +import scipy._lib.array_api_extra as xpx + +import pickle +import copy + + +lazy_xp_modules = [Rotation, Slerp] + +# from_quat and as_quat are used in almost all tests, so we mark them module-wide +pytestmark = make_xp_pytest_marks(Rotation.as_quat, Rotation.from_quat) + + +def basis_vec(axis): + if axis == 'x': + return [1, 0, 0] + elif axis == 'y': + return [0, 1, 0] + elif axis == 'z': + return [0, 0, 1] + + +def rotation_to_xp(r: Rotation, xp): + dtype = xpx.default_dtype(xp) + return Rotation.from_quat(xp.asarray(r.as_quat(), dtype=dtype)) + + +def test_init_non_array(): + Rotation((0, 0, 0, 1)) + Rotation([0, 0, 0, 1]) + Rotation([[[0, 0, 0, 1]]]) + + +def test_cython_backend_selection(): + r = Rotation.from_quat(np.array([0, 0, 0, 1])) + assert r._backend is cython_backend + r = Rotation.from_quat(np.array([[0, 0, 0, 1]])) + assert r._backend is cython_backend + r = Rotation.from_quat(np.array([[[0, 0, 0, 1]]])) + assert r._backend is xp_backend + + +def test_numpy_float32_inputs(): + Rotation.from_quat(np.array([1, 0, 0, 0], dtype=np.float32)) + + +def test_generic_quat_matrix(xp): + x = xp.asarray([[3.0, 4, 0, 0], [5, 12, 0, 0]]) + r = Rotation.from_quat(x) + expected_quat = x / xp.asarray([[5.0], [13.0]]) + xp_assert_close(r.as_quat(), expected_quat) + + +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_from_single_nd_quaternion(xp, ndim: int): + x = xp.asarray([3.0, 4, 0, 0]) + x = xp.reshape(x, (1,) * (ndim - 1) + (4,)) + r = Rotation.from_quat(x) + expected_quat = x / 5 + xp_assert_close(r.as_quat(), expected_quat) + + +@make_xp_test_case(Rotation.as_matrix) +def test_from_quat_scalar_first(xp): + rng = np.random.RandomState(0) + + r = Rotation.from_quat(xp.asarray([1, 0, 0, 0]), scalar_first=True) + xp_assert_close(r.as_matrix(), xp.eye(3), rtol=1e-15, atol=1e-16) + + q = xp.tile(xp.asarray([1, 0, 0, 0]), (10, 1)) + r = Rotation.from_quat(q, scalar_first=True) + xp_assert_close( + r.as_matrix(), xp.tile(xp.eye(3), (10, 1, 1)), rtol=1e-15, atol=1e-16 + ) + + q = xp.asarray(rng.randn(100, 4)) + q /= xp_vector_norm(q, axis=1)[:, None] + for i in range(q.shape[0]): # Array API conforming loop + qi = q[i, ...] + r = Rotation.from_quat(qi, scalar_first=True) + xp_assert_close(xp.roll(r.as_quat(), 1), qi, rtol=1e-15) + + r = Rotation.from_quat(q, scalar_first=True) + xp_assert_close(xp.roll(r.as_quat(), 1, axis=1), q, rtol=1e-15) + + +def test_from_quat_array_like(): + rng = np.random.default_rng(123) + # Single rotation + r_expected = Rotation.random(rng=rng) + r = Rotation.from_quat(r_expected.as_quat().tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(3, rng=rng) + r = Rotation.from_quat(r_expected.as_quat().tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + # Tensor of rotations + q = rng.normal(size=(3, 4, 5, 4)) + q /= xp_vector_norm(q, axis=-1)[..., None] + r_expected = Rotation.from_quat(q) + r = Rotation.from_quat(q) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +def test_from_quat_int_dtype(xp): + r = Rotation.from_quat(xp.asarray([1, 0, 0, 0])) + assert r.as_quat().dtype == xp_default_dtype(xp) + + +def test_quat_canonical(xp): + # Case 0: w < 0 + q = xp.asarray([0.0, 0, 0, -1]) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), -q) + # Case 1: w == 0, x < 0 + q = xp.asarray([-1.0, 0, 0, 0]) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), -q) + # Case 2: w == 0, x == 0, y < 0 + q = xp.asarray([0.0, -1, 0, 0]) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), -q) + # Case 3: w == 0, x == 0, y == 0, z < 0 + q = xp.asarray([0.0, 0, -1, 0]) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), -q) + # Other cases: w > 0, y < 0 + q = xp.asarray([0.0, -0.1, 0, 0.9]) + q = q / xp_vector_norm(q) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), q) + # Other cases: w > 0, z < 0 + q = xp.asarray([0.0, 0.0, -0.1, 0.9]) + q = q / xp_vector_norm(q) + xp_assert_close(Rotation.from_quat(q).as_quat(canonical=True), q) + + +@make_xp_test_case(Rotation.from_euler) +def test_as_quat_scalar_first(xp): + rng = np.random.RandomState(0) + + r = Rotation.from_euler('xyz', xp.zeros(3)) + xp_assert_close(r.as_quat(scalar_first=True), xp.asarray([1.0, 0, 0, 0]), + rtol=1e-15, atol=1e-16) + + r = Rotation.from_euler('xyz', xp.zeros((10, 3))) + xp_assert_close(r.as_quat(scalar_first=True), + xp.tile(xp.asarray([1.0, 0, 0, 0]), (10, 1)), + rtol=1e-15, atol=1e-16) + + q = xp.asarray(rng.randn(100, 4)) + q /= xp_vector_norm(q, axis=1)[:, None] + for i in range(q.shape[0]): # Array API conforming loop + qi = q[i, ...] + r = Rotation.from_quat(qi) + xp_assert_close(r.as_quat(scalar_first=True), xp.roll(qi, 1), + rtol=1e-15) + + xp_assert_close(r.as_quat(canonical=True, scalar_first=True), + xp.roll(r.as_quat(canonical=True), 1), + rtol=1e-15) + + r = Rotation.from_quat(q) + xp_assert_close(r.as_quat(scalar_first=True), xp.roll(q, 1, axis=1), + rtol=1e-15) + + xp_assert_close(r.as_quat(canonical=True, scalar_first=True), + xp.roll(r.as_quat(canonical=True), 1, axis=1), rtol=1e-15) + + +def test_from_square_quat_matrix(xp): + # Ensure proper norm array broadcasting + x = xp.asarray([ + [3.0, 0, 0, 4], + [5, 0, 12, 0], + [0, 0, 0, 1], + [-1, -1, -1, 1], + [0, 0, 0, -1], # Check double cover + [-1, -1, -1, -1] # Check double cover + ]) + r = Rotation.from_quat(x) + expected_quat = x / xp.asarray([[5.0], [13], [1], [2], [1], [2]]) + xp_assert_close(r.as_quat(), expected_quat) + + +def test_quat_double_to_canonical_single_cover(xp): + x = xp.asarray([ + [-1.0, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, -1, 0], + [0, 0, 0, -1], + [-1, -1, -1, -1] + ]) + r = Rotation.from_quat(x) + expected_quat = xp.abs(x) / xp_vector_norm(x, axis=1)[:, None] + xp_assert_close(r.as_quat(canonical=True), expected_quat) + + +@make_xp_test_case(Rotation.inv, Rotation.__mul__) +def test_quat_double_cover(xp): + # See the Rotation.from_quat() docstring for scope of the quaternion + # double cover property. + # Check from_quat and as_quat(canonical=False) + q = xp.asarray([0.0, 0, 0, -1]) + r = Rotation.from_quat(q) + xp_assert_equal(q, r.as_quat(canonical=False)) + # Check composition and inverse + q = xp.asarray([1.0, 0, 0, 1])/math.sqrt(2) # 90 deg rotation about x + r = Rotation.from_quat(q) + r3 = r*r*r + xp_assert_close(r.as_quat(canonical=False)*math.sqrt(2), + xp.asarray([1.0, 0, 0, 1])) + xp_assert_close(r.inv().as_quat(canonical=False)*math.sqrt(2), + xp.asarray([-1.0, 0, 0, 1])) + xp_assert_close(r3.as_quat(canonical=False)*math.sqrt(2), + xp.asarray([1.0, 0, 0, -1])) + xp_assert_close(r3.inv().as_quat(canonical=False)*math.sqrt(2), + xp.asarray([-1.0, 0, 0, -1])) + + # More sanity checks + xp_assert_close((r*r.inv()).as_quat(canonical=False), + xp.asarray([0.0, 0, 0, 1]), atol=2e-16) + xp_assert_close((r3*r3.inv()).as_quat(canonical=False), + xp.asarray([0.0, 0, 0, 1]), atol=2e-16) + xp_assert_close((r*r3).as_quat(canonical=False), + xp.asarray([0.0, 0, 0, -1]), atol=2e-16) + xp_assert_close((r.inv() * r3.inv()).as_quat(canonical=False), + xp.asarray([0.0, 0, 0, -1]), atol=2e-16) + + +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_from_quat_wrong_shape(xp, ndim: int): + quat = xp.zeros((*((1,) * ndim), 5)) + with pytest.raises(ValueError, match="Expected `quat` to have shape"): + Rotation.from_quat(quat) + + +def test_zero_norms_from_quat(xp): + x = xp.asarray([ + [3, 4, 0, 0], + [0, 0, 0, 0], + [5, 0, 12, 0] + ]) + if is_lazy_array(x): + assert xp.all(xp.isnan(Rotation.from_quat(x).as_quat()[1, ...])) + else: + with pytest.raises(ValueError): + Rotation.from_quat(x) + + +@make_xp_test_case(Rotation.as_matrix) +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_as_matrix_single_nd_quaternion(xp, ndim: int): + quat = xp.asarray([0, 0, 1, 1]) + quat = xp.reshape(quat, (1,) * (ndim - 1) + (4,)) + mat = Rotation.from_quat(quat).as_matrix() + expected_mat = xp.asarray([ + [0.0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + expected_mat = xp.reshape(expected_mat, (1,) * (ndim - 1) + (3, 3)) + xp_assert_close(mat, expected_mat, atol=1e-16) + + +@make_xp_test_case(Rotation.as_matrix) +def test_as_matrix_from_square_input(xp): + quats = xp.asarray([ + [0, 0, 1, 1], + [0, 1, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, -1] + ]) + mat = Rotation.from_quat(quats).as_matrix() + assert_equal(mat.shape, (4, 3, 3)) + + expected0 = xp.asarray([ + [0.0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + xp_assert_close(mat[0, ...], expected0, atol=1e-16) + + expected1 = xp.asarray([ + [0.0, 0, 1], + [0, 1, 0], + [-1, 0, 0] + ]) + xp_assert_close(mat[1, ...], expected1, atol=1e-16) + xp_assert_close(mat[2, ...], xp.eye(3)) + xp_assert_close(mat[3, ...], xp.eye(3)) + + +@make_xp_test_case(Rotation.as_matrix) +def test_as_matrix_from_generic_input(xp): + quats = xp.asarray([ + [0, 0, 1, 1], + [0, 1, 0, 1], + [1, 2, 3, 4] + ]) + mat = Rotation.from_quat(quats).as_matrix() + assert_equal(mat.shape, (3, 3, 3)) + + expected0 = xp.asarray([ + [0.0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + xp_assert_close(mat[0, ...], expected0, atol=1e-16) + + expected1 = xp.asarray([ + [0.0, 0, 1], + [0, 1, 0], + [-1, 0, 0] + ]) + xp_assert_close(mat[1, ...], expected1, atol=1e-16) + + expected2 = xp.asarray([ + [0.4, -2, 2.2], + [2.8, 1, 0.4], + [-1, 2, 2] + ]) / 3 + xp_assert_close(mat[2, ...], expected2) + + +@make_xp_test_case(Rotation.from_matrix) +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_from_single_nd_matrix(xp, ndim: int): + mat = xp.asarray([ + [0, 0, 1], + [1, 0, 0], + [0, 1, 0] + ]) + mat = xp.reshape(mat, (1,) * (ndim - 1) + (3, 3)) + expected_quat = xp.asarray([0.5, 0.5, 0.5, 0.5]) + expected_quat = xp.reshape(expected_quat, (1,) * (ndim - 1) + (4,)) + xp_assert_close(Rotation.from_matrix(mat).as_quat(), expected_quat) + + +@make_xp_test_case(Rotation.from_matrix) +def test_from_matrix_calculation(xp): + atol = 1e-8 + expected_quat = xp.asarray([1.0, 1, 6, 1]) / math.sqrt(39) + mat = xp.asarray([ + [-0.8974359, -0.2564103, 0.3589744], + [0.3589744, -0.8974359, 0.2564103], + [0.2564103, 0.3589744, 0.8974359] + ]) + xp_assert_close(Rotation.from_matrix(mat).as_quat(), expected_quat, atol=atol) + xp_assert_close(Rotation.from_matrix(xp.reshape(mat, (1, 3, 3))).as_quat(), + xp.reshape(expected_quat, (1, 4)), + atol=atol) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix) +def test_matrix_calculation_pipeline(xp): + mat = xp.asarray(special_ortho_group.rvs(3, size=10, random_state=0)) + xp_assert_close(Rotation.from_matrix(mat).as_matrix(), mat) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix) +def test_from_matrix_ortho_output(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + rnd = np.random.RandomState(0) + mat = xp.asarray(rnd.random_sample((100, 3, 3)), dtype=dtype) + dets = xp.linalg.det(mat) + for i in range(dets.shape[0]): + # Make sure we have a right-handed rotation matrix + if dets[i] < 0: + mat = xpx.at(mat)[i, ...].set(-mat[i, ...]) + ortho_mat = Rotation.from_matrix(mat).as_matrix() + + mult_result = xp.matmul(ortho_mat, xp.matrix_transpose(ortho_mat)) + + eye3d = xp.zeros((100, 3, 3)) + xp.eye(3) + xp_assert_close(mult_result, eye3d, atol=atol) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix) +def test_from_matrix_normalize(xp): + mat = xp.asarray([ + [1, 1, 0], + [0, 1, 0], + [0, 0, 1]]) + expected = xp.asarray([[ 0.894427, 0.447214, 0.0], + [-0.447214, 0.894427, 0.0], + [ 0.0, 0.0, 1.0]]) + xp_assert_close(Rotation.from_matrix(mat).as_matrix(), expected, atol=1e-6) + + mat = xp.asarray([ + [0, -0.5, 0 ], + [0.5, 0 , 0 ], + [0, 0 , 0.5]]) + expected = xp.asarray([[0.0, -1, 0], + [ 1, 0, 0], + [ 0, 0, 1]]) + xp_assert_close(Rotation.from_matrix(mat).as_matrix(), expected, atol=1e-6) + + # Test a mix of normalized and non-normalized matrices + mat = xp.stack([mat, xp.eye(3)]) + expected = xp.stack([expected, xp.eye(3)]) + xp_assert_close(Rotation.from_matrix(mat).as_matrix(), expected, atol=1e-6) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix) +def test_from_matrix_assume_valid(xp): + rng = np.random.default_rng(0) + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + # Test that normal matrices remain unchanged + rot = rotation_to_xp(Rotation.from_quat(rng.normal(size=(10, 4))), xp) + rot_no_norm = Rotation.from_matrix(rot.as_matrix(), assume_valid=True) + assert xp.all(rot.approx_equal(rot_no_norm, atol=atol)) + # We make no guarantees about matrices that are not orthogonal or do not + # have unit norm + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix) +def test_from_matrix_non_positive_determinant(xp): + mat = xp.eye(3) + mat = xpx.at(mat)[0, 0].set(0) + if is_lazy_array(mat): + assert xp.all(xp.isnan(Rotation.from_matrix(mat).as_matrix())) + else: + with pytest.raises(ValueError, match="Non-positive determinant"): + Rotation.from_matrix(mat) + + mat = xpx.at(mat)[0, 0].set(-1) + if is_lazy_array(mat): + assert xp.all(xp.isnan(Rotation.from_matrix(mat).as_matrix())) + else: + with pytest.raises(ValueError, match="Non-positive determinant"): + Rotation.from_matrix(mat) + + +def test_from_matrix_array_like(): + rng = np.random.default_rng(123) + # Single rotation + r_expected = Rotation.random(rng=rng) + r = Rotation.from_matrix(r_expected.as_matrix().tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(3, rng=rng) + r = Rotation.from_matrix(r_expected.as_matrix().tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +@make_xp_test_case(Rotation.from_matrix) +def test_from_matrix_int_dtype(xp): + mat = xp.asarray([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + r = Rotation.from_matrix(mat) + assert r.as_quat().dtype == xp_default_dtype(xp) + + +@make_xp_test_case(Rotation.from_rotvec) +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_from_nd_single_rotvec(xp, ndim: int): + atol = 1e-7 + rotvec = xp.asarray([1, 0, 0]) + rotvec = xp.reshape(rotvec, (1,) * (ndim - 1) + (3,)) + expected_quat = xp.asarray([0.4794255, 0, 0, 0.8775826]) + expected_quat = xp.reshape(expected_quat, (1,) * (ndim - 1) + (4,)) + result = Rotation.from_rotvec(rotvec) + xp_assert_close(result.as_quat(), expected_quat, atol=atol) + + +@make_xp_test_case(Rotation.from_rotvec) +def test_from_generic_rotvec(xp): + atol = 1e-7 + rotvec = xp.asarray([ + [1, 2, 2], + [1, -1, 0.5], + [0, 0, 0]]) + expected_quat = xp.asarray([ + [0.3324983, 0.6649967, 0.6649967, 0.0707372], + [0.4544258, -0.4544258, 0.2272129, 0.7316889], + [0, 0, 0, 1] + ]) + xp_assert_close(Rotation.from_rotvec(rotvec).as_quat(), expected_quat, atol=atol) + + +@make_xp_test_case(Rotation.from_rotvec) +def test_from_rotvec_small_angle(xp): + rotvec = xp.asarray([ + [5e-4 / math.sqrt(3), -5e-4 / math.sqrt(3), 5e-4 / math.sqrt(3)], + [0.2, 0.3, 0.4], + [0, 0, 0] + ]) + + quat = Rotation.from_rotvec(rotvec).as_quat() + # cos(theta/2) ~~ 1 for small theta + xp_assert_close(quat[0, 3], xp.asarray(1.0)[()]) + # sin(theta/2) / theta ~~ 0.5 for small theta + xp_assert_close(quat[0, :3], rotvec[0, ...] * 0.5) + + xp_assert_close(quat[1, 3], xp.asarray(0.9639685)[()]) + xp_assert_close(quat[1, :3], + xp.asarray([ + 0.09879603932153465, + 0.14819405898230198, + 0.19759207864306931])) + + xp_assert_equal(quat[2, ...], xp.asarray([0.0, 0, 0, 1])) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.as_rotvec) +def test_from_rotvec_array_like(): + rng = np.random.default_rng(123) + # Single rotation + r_expected = Rotation.random(rng=rng) + r = Rotation.from_rotvec(r_expected.as_rotvec().tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(3, rng=rng) + r = Rotation.from_rotvec(r_expected.as_rotvec().tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +@make_xp_test_case(Rotation.from_rotvec) +def test_from_rotvec_int_dtype(xp): + rotvec = xp.asarray([1, 0, 0]) + r = Rotation.from_rotvec(rotvec) + assert r.as_quat().dtype == xp_default_dtype(xp) + + +@make_xp_test_case(Rotation.from_rotvec) +def test_degrees_from_rotvec(xp): + rotvec1 = xp.asarray([1 / 3 ** (1/3)] * 3) + rot1 = Rotation.from_rotvec(rotvec1, degrees=True) + quat1 = rot1.as_quat() + + # deg2rad is not implemented in Array API -> / 180 * xp.pi + rotvec2 = xp.asarray(rotvec1 / 180 * xp.pi) + rot2 = Rotation.from_rotvec(rotvec2) + quat2 = rot2.as_quat() + + xp_assert_close(quat1, quat2) + + +@make_xp_test_case(Rotation.from_rotvec) +def test_malformed_1d_from_rotvec(xp): + with pytest.raises(ValueError, match='Expected `rot_vec` to have shape'): + Rotation.from_rotvec(xp.asarray([1, 2])) + + +@make_xp_test_case(Rotation.from_rotvec) +@pytest.mark.parametrize("ndim", range(1, 6)) +def test_malformed_nd_from_rotvec(xp, ndim: int): + shape = (1,) * (ndim - 1) + (2,) + with pytest.raises(ValueError, match='Expected `rot_vec` to have shape'): + Rotation.from_rotvec(xp.ones(shape)) + + +@make_xp_test_case(Rotation.as_rotvec) +@pytest.mark.skip_xp_backends("dask.array", + reason="missing required linalg.cross function") +def test_as_generic_rotvec(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-15 if dtype == xp.float64 else 1e-7 + quat = xp.asarray([ + [1, 2, -1, 0.5], + [1, -1, 1, 0.0003], + [0, 0, 0, 1] + ]) + quat /= xp_vector_norm(quat, axis=-1, keepdims=True) + + rotvec = Rotation.from_quat(quat).as_rotvec() + angle = xp_vector_norm(rotvec, axis=-1) + + xp_assert_close(quat[:, 3], xp.cos(angle / 2)) + xp_assert_close(xp.linalg.cross(rotvec, quat[:, :3]), xp.zeros((3, 3)), atol=atol) + + +@make_xp_test_case(Rotation.as_rotvec) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_rotvec_single_nd_input(xp, ndim: int): + quat = xp.asarray([1, 2, -3, 2]) + quat = xp.reshape(quat, (1,) * (ndim - 1) + (4,)) + expected_rotvec = xp.asarray([0.5772381, 1.1544763, -1.7317144]) + expected_rotvec = xp.reshape(expected_rotvec, (1,) * (ndim - 1) + (3,)) + actual_rotvec = Rotation.from_quat(quat).as_rotvec() + + assert_equal(actual_rotvec.shape, expected_rotvec.shape) + xp_assert_close(actual_rotvec, expected_rotvec) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_rotvec) +def test_as_rotvec_degrees(xp): + # x->y, y->z, z->x + mat = xp.asarray([[0, 0, 1], [1, 0, 0], [0, 1, 0]]) + rot = Rotation.from_matrix(mat) + rotvec = rot.as_rotvec(degrees=True) + angle = xp_vector_norm(rotvec, axis=-1) + xp_assert_close(angle, xp.asarray(120.0)[()]) + xp_assert_close(rotvec[0], rotvec[1]) + xp_assert_close(rotvec[1], rotvec[2]) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.as_rotvec) +def test_rotvec_calc_pipeline(xp): + # Include small angles + rotvec = xp.asarray([ + [0, 0, 0], + [1, -1, 2], + [-3e-4, 3.5e-4, 7.5e-5] + ]) + xp_assert_close(Rotation.from_rotvec(rotvec).as_rotvec(), rotvec) + xp_assert_close(Rotation.from_rotvec(rotvec, degrees=True).as_rotvec(degrees=True), + rotvec) + + +@make_xp_test_case(Rotation.from_mrp) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_mrp_single_nd_input(xp, ndim: int): + mrp = xp.asarray([0, 0, 1.0]) + mrp = xp.reshape(mrp, (1,) * (ndim - 1) + (3,)) + expected_quat = xp.asarray([0.0, 0, 1, 0]) + expected_quat = xp.reshape(expected_quat, (1,) * (ndim - 1) + (4,)) + result = Rotation.from_mrp(mrp) + xp_assert_close(result.as_quat(), expected_quat, atol=1e-12) + # Regression test for gh-24555 + assert isinstance(result._quat, type(array_namespace(mrp).empty(0))) + + +def test_from_mrp_array_like(): + rng = np.random.default_rng(123) + # Single rotation + r_expected = Rotation.random(rng=rng) + r = Rotation.from_mrp(r_expected.as_mrp().tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(3, rng=rng) + r = Rotation.from_mrp(r_expected.as_mrp().tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +@make_xp_test_case(Rotation.from_mrp) +def test_from_mrp_int_dtype(xp): + mrp = xp.asarray([0, 0, 1]) + r = Rotation.from_mrp(mrp) + assert r.as_quat().dtype == xp_default_dtype(xp) + + +@make_xp_test_case(Rotation.from_mrp) +def test_from_generic_mrp(xp): + mrp = xp.asarray([ + [1, 2, 2], + [1, -1, 0.5], + [0, 0, 0]]) + expected_quat = xp.asarray([ + [0.2, 0.4, 0.4, -0.8], + [0.61538462, -0.61538462, 0.30769231, -0.38461538], + [0, 0, 0, 1]]) + xp_assert_close(Rotation.from_mrp(mrp).as_quat(), expected_quat) + + +@make_xp_test_case(Rotation.from_mrp) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_malformed_nd_from_mrp(xp, ndim: int): + shape = (1,) * (ndim - 1) + (2,) + with pytest.raises(ValueError, match='Expected `mrp` to have shape'): + Rotation.from_mrp(xp.ones(shape)) + + +@make_xp_test_case(Rotation.as_mrp) +def test_as_generic_mrp(xp): + quat = xp.asarray([ + [1, 2, -1, 0.5], + [1, -1, 1, 0.0003], + [0, 0, 0, 1]]) + quat /= xp_vector_norm(quat, axis=1)[:, None] + + expected_mrp = xp.asarray([ + [0.33333333, 0.66666667, -0.33333333], + [0.57725028, -0.57725028, 0.57725028], + [0, 0, 0]]) + xp_assert_close(Rotation.from_quat(quat).as_mrp(), expected_mrp) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_mrp) +def test_past_180_degree_rotation(xp): + # ensure that a > 180 degree rotation is returned as a <180 rotation in MRPs + # in this case 270 should be returned as -90 + expected_mrp = xp.asarray([-math.tan(xp.pi / 2 / 4), 0.0, 0]) + xp_assert_close( + Rotation.from_euler('xyz', xp.asarray([270, 0, 0]), degrees=True).as_mrp(), + expected_mrp, + ) + + +@make_xp_test_case(Rotation.as_mrp) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_mrp_single_nd_input(xp, ndim: int): + quat = xp.asarray([1, 2, -3, 2]) + quat = xp.reshape(quat, (1,) * (ndim - 1) + (4,)) + expected_mrp = xp.asarray([0.16018862, 0.32037724, -0.48056586]) + expected_mrp = xp.reshape(expected_mrp, (1,) * (ndim - 1) + (3,)) + actual_mrp = Rotation.from_quat(quat).as_mrp() + + assert_equal(actual_mrp.shape, expected_mrp.shape) + xp_assert_close(actual_mrp, expected_mrp) + + +@make_xp_test_case(Rotation.from_mrp, Rotation.as_mrp) +def test_mrp_calc_pipeline(xp): + actual_mrp = xp.asarray([ + [0, 0, 0], + [1, -1, 2], + [0.41421356, 0, 0], + [0.1, 0.2, 0.1]]) + expected_mrp = xp.asarray([ + [0, 0, 0], + [-0.16666667, 0.16666667, -0.33333333], + [0.41421356, 0, 0], + [0.1, 0.2, 0.1]]) + xp_assert_close(Rotation.from_mrp(actual_mrp).as_mrp(), expected_mrp) + + +@make_xp_test_case(Rotation.from_euler) +def test_from_euler_single_rotation(xp): + quat = Rotation.from_euler("z", xp.asarray(90), degrees=True).as_quat() + expected_quat = xp.asarray([0.0, 0, 1, 1]) / math.sqrt(2) + xp_assert_close(quat, expected_quat) + + +@make_xp_test_case(Rotation.from_euler) +def test_from_euler_input_validation(xp): + # Single sequence with multiple angles + with pytest.raises(ValueError, match="Expected last dimension of `angles` to"): + Rotation.from_euler("X", xp.asarray([0, 90])) + # Multiple sequences with single angle + with pytest.raises(ValueError, match="Expected last dimension of `angles` to"): + Rotation.from_euler("XYZ", xp.asarray([90])) + + +@make_xp_test_case(Rotation.from_euler) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_euler_nd_rotation(xp, ndim: int): + angles = xp.reshape(xp.asarray([0, 0, 90]), (1,) * (ndim - 1) + (3,)) + quat = Rotation.from_euler("xyz", angles, degrees=True).as_quat() + expected_quat = xp.asarray([0.0, 0, 1, 1]) / math.sqrt(2) + expected_quat = xp.reshape(expected_quat, (1,) * (ndim - 1) + (4,)) + xp_assert_close(quat, expected_quat) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_single_intrinsic_extrinsic_rotation(xp): + extrinsic = Rotation.from_euler('z', xp.asarray(90), degrees=True).as_matrix() + intrinsic = Rotation.from_euler('Z', xp.asarray(90), degrees=True).as_matrix() + xp_assert_close(extrinsic, intrinsic) + + +@make_xp_test_case(Rotation.from_euler) +def test_from_euler_rotation_order(xp): + # Intrinsic rotation is same as extrinsic with order reversed + rnd = np.random.RandomState(0) + a = xp.asarray(rnd.randint(low=0, high=180, size=(6, 3))) + b = xp.flip(a, axis=-1) + x = Rotation.from_euler('xyz', a, degrees=True).as_quat() + y = Rotation.from_euler('ZYX', b, degrees=True).as_quat() + xp_assert_close(x, y) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_from_euler_elementary_extrinsic_rotation(xp): + atol = 1e-12 + # Simple test to check if extrinsic rotations are implemented correctly + mat = Rotation.from_euler('zx', xp.asarray([90, 90]), degrees=True).as_matrix() + expected_mat = xp.asarray([ + [0.0, -1, 0], + [0, 0, -1], + [1, 0, 0] + ]) + xp_assert_close(mat, expected_mat, atol=atol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_from_euler_intrinsic_rotation_312(xp): + atol = 1e-7 + angles = xp.asarray([ + [30, 60, 45], + [30, 60, 30], + [45, 30, 60] + ]) + mat = Rotation.from_euler('ZXY', angles, degrees=True).as_matrix() + + xp_assert_close(mat[0, ...], xp.asarray([ + [0.3061862, -0.2500000, 0.9185587], + [0.8838835, 0.4330127, -0.1767767], + [-0.3535534, 0.8660254, 0.3535534] + ]), atol=atol) + + xp_assert_close(mat[1, ...], xp.asarray([ + [0.5334936, -0.2500000, 0.8080127], + [0.8080127, 0.4330127, -0.3995191], + [-0.2500000, 0.8660254, 0.4330127] + ]), atol=atol) + + xp_assert_close(mat[2, ...], xp.asarray([ + [0.0473672, -0.6123725, 0.7891491], + [0.6597396, 0.6123725, 0.4355958], + [-0.7500000, 0.5000000, 0.4330127] + ]), atol=atol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_from_euler_intrinsic_rotation_313(xp): + angles = xp.asarray([ + [30, 60, 45], + [30, 60, 30], + [45, 30, 60] + ]) + mat = Rotation.from_euler('ZXZ', angles, degrees=True).as_matrix() + + xp_assert_close(mat[0, ...], xp.asarray([ + [0.43559574, -0.78914913, 0.4330127], + [0.65973961, -0.04736717, -0.750000], + [0.61237244, 0.61237244, 0.500000] + ])) + + xp_assert_close(mat[1, ...], xp.asarray([ + [0.6250000, -0.64951905, 0.4330127], + [0.64951905, 0.1250000, -0.750000], + [0.4330127, 0.750000, 0.500000] + ])) + + xp_assert_close(mat[2, ...], xp.asarray([ + [-0.1767767, -0.91855865, 0.35355339], + [0.88388348, -0.30618622, -0.35355339], + [0.4330127, 0.25000000, 0.8660254] + ])) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_from_euler_extrinsic_rotation_312(xp): + angles = xp.asarray([ + [30, 60, 45], + [30, 60, 30], + [45, 30, 60] + ]) + mat = Rotation.from_euler('zxy', angles, degrees=True).as_matrix() + + xp_assert_close(mat[0, ...], xp.asarray([ + [0.91855865, 0.1767767, 0.35355339], + [0.25000000, 0.4330127, -0.8660254], + [-0.30618622, 0.88388348, 0.35355339] + ])) + + xp_assert_close(mat[1, ...], xp.asarray([ + [0.96650635, -0.0580127, 0.2500000], + [0.25000000, 0.4330127, -0.8660254], + [-0.0580127, 0.89951905, 0.4330127] + ])) + + xp_assert_close(mat[2, ...], xp.asarray([ + [0.65973961, -0.04736717, 0.7500000], + [0.61237244, 0.61237244, -0.5000000], + [-0.43559574, 0.78914913, 0.4330127] + ])) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix) +def test_from_euler_extrinsic_rotation_313(xp): + angles = xp.asarray([ + [30, 60, 45], + [30, 60, 30], + [45, 30, 60] + ]) + mat = Rotation.from_euler('zxz', angles, degrees=True).as_matrix() + + xp_assert_close(mat[0, ...], xp.asarray([ + [0.43559574, -0.65973961, 0.61237244], + [0.78914913, -0.04736717, -0.61237244], + [0.4330127, 0.75000000, 0.500000] + ])) + + xp_assert_close(mat[1, ...], xp.asarray([ + [0.62500000, -0.64951905, 0.4330127], + [0.64951905, 0.12500000, -0.750000], + [0.4330127, 0.75000000, 0.500000] + ])) + + xp_assert_close(mat[2, ...], xp.asarray([ + [-0.1767767, -0.88388348, 0.4330127], + [0.91855865, -0.30618622, -0.250000], + [0.35355339, 0.35355339, 0.8660254] + ])) + + +def test_from_euler_array_like(): + rng = np.random.default_rng(123) + order = "xyz" + # Single rotation + r_expected = Rotation.random(rng=rng) + r = Rotation.from_euler(order, r_expected.as_euler(order).tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(3, rng=rng) + r = Rotation.from_euler(order, r_expected.as_euler(order).tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +def test_from_euler_scalar(): + rng = np.random.default_rng(123) + deg = rng.uniform(low=-180, high=180) + r_expected = Rotation.from_euler("x", deg, degrees=True) + r = Rotation.from_euler("x", float(deg), degrees=True) + assert r_expected.approx_equal(r, atol=1e-12) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_euler) +@pytest.mark.parametrize("seq_tuple", permutations("xyz")) +@pytest.mark.parametrize("intrinsic", (False, True)) +def test_as_euler_asymmetric_axes(xp, seq_tuple, intrinsic): + # helper function for mean error tests + def test_stats(error, mean_max, rms_max): + mean = xp.mean(error, axis=0) + std = xp.std(error, axis=0) + rms = xp.hypot(mean, std) + assert xp.all(xp.abs(mean) < mean_max) + assert xp.all(rms < rms_max) + + rnd = np.random.RandomState(0) + n = 1000 + angles = np.empty((n, 3)) + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles[:, 1] = rnd.uniform(low=-np.pi / 2, high=np.pi / 2, size=(n,)) + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles = xp.asarray(angles) + + seq = "".join(seq_tuple) + if intrinsic: + # Extrinsic rotation (wrt to global world) at lower case + # intrinsic (WRT the object itself) lower case. + seq = seq.upper() + rotation = Rotation.from_euler(seq, angles) + angles_quat = rotation.as_euler(seq) + xp_assert_close(angles, angles_quat, atol=0, rtol=1e-12) + test_stats(angles_quat - angles, 1e-15, 1e-14) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_euler) +@pytest.mark.parametrize("seq_tuple", permutations("xyz")) +@pytest.mark.parametrize("intrinsic", (False, True)) +def test_as_euler_symmetric_axes(xp, seq_tuple, intrinsic): + # helper function for mean error tests + def test_stats(error, mean_max, rms_max): + mean = xp.mean(error, axis=0) + std = xp.std(error, axis=0) + rms = xp.hypot(mean, std) + assert xp.all(xp.abs(mean) < mean_max) + assert xp.all(rms < rms_max) + + rnd = np.random.RandomState(0) + n = 1000 + angles = np.empty((n, 3)) + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles[:, 1] = rnd.uniform(low=0, high=np.pi, size=(n,)) + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles = xp.asarray(angles) + + # Rotation of the form A/B/A are rotation around symmetric axes + seq = "".join([seq_tuple[0], seq_tuple[1], seq_tuple[0]]) + if intrinsic: + seq = seq.upper() + rotation = Rotation.from_euler(seq, angles) + angles_quat = rotation.as_euler(seq) + xp_assert_close(angles, angles_quat, atol=0, rtol=1.1e-13) + test_stats(angles_quat - angles, 1e-16, 1e-14) + + +@contextmanager +def maybe_warn_gimbal_lock(should_warn, xp): + if should_warn: + # We can only warn on non-lazy backends because we'd need to condition on + # traced booleans + with eager_warns(UserWarning, match="Gimbal lock", xp=xp): + yield + + else: + with warnings.catch_warnings(): + warnings.simplefilter("error") + yield + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix, Rotation.as_euler) +@pytest.mark.parametrize("seq_tuple", permutations("xyz")) +@pytest.mark.parametrize("intrinsic", (False, True)) +@pytest.mark.parametrize("suppress_warnings", (False, True)) +def test_as_euler_degenerate_asymmetric_axes( + xp, seq_tuple, intrinsic, suppress_warnings +): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + # Since we cannot check for angle equality, we check for rotation matrix + # equality + angles = xp.asarray([ + [45, 90, 35], + [35, -90, 20], + [35, 90, 25], + [25, -90, 15]]) + + seq = "".join(seq_tuple) + if intrinsic: + # Extrinsic rotation (wrt to global world) at lower case + # Intrinsic (WRT the object itself) upper case. + seq = seq.upper() + rotation = Rotation.from_euler(seq, angles, degrees=True) + mat_expected = rotation.as_matrix() + + with maybe_warn_gimbal_lock(not suppress_warnings, xp): + angle_estimates = rotation.as_euler( + seq, degrees=True, suppress_warnings=suppress_warnings + ) + mat_estimated = Rotation.from_euler(seq, angle_estimates, degrees=True).as_matrix() + + xp_assert_close(mat_expected, mat_estimated, atol=atol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_matrix, Rotation.as_euler) +@pytest.mark.parametrize("seq_tuple", permutations("xyz")) +@pytest.mark.parametrize("intrinsic", (False, True)) +@pytest.mark.parametrize("suppress_warnings", (False, True)) +def test_as_euler_degenerate_symmetric_axes( + xp, seq_tuple, intrinsic, suppress_warnings +): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + # Since we cannot check for angle equality, we check for rotation matrix + # equality + angles = xp.asarray([ + [15, 0, 60], + [35, 0, 75], + [60, 180, 35], + [15, -180, 25]]) + + # Rotation of the form A/B/A are rotation around symmetric axes + seq = "".join([seq_tuple[0], seq_tuple[1], seq_tuple[0]]) + if intrinsic: + # Extrinsic rotation (wrt to global world) at lower case + # Intrinsic (WRT the object itself) upper case. + seq = seq.upper() + rotation = Rotation.from_euler(seq, angles, degrees=True) + mat_expected = rotation.as_matrix() + + with maybe_warn_gimbal_lock(not suppress_warnings, xp): + angle_estimates = rotation.as_euler( + seq, degrees=True, suppress_warnings=suppress_warnings + ) + mat_estimated = Rotation.from_euler(seq, angle_estimates, degrees=True).as_matrix() + + xp_assert_close(mat_expected, mat_estimated, atol=atol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_euler) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_as_euler_nd_rotation(xp, ndim: int): + mat = xp.asarray([ + [0.0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + mat = xp.reshape(mat, (1,) * (ndim - 1) + (3, 3)) + angles = Rotation.from_matrix(mat).as_euler("xyz", degrees=True) + expected_angles = xp.asarray([0, 0, 90.0]) + expected_angles = xp.reshape(expected_angles, (1,) * (ndim - 1) + (3,)) + xp_assert_close(angles, expected_angles, atol=1e-12) + + +@make_xp_test_case(Rotation.as_matrix, Rotation.inv) +def test_inv(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-7 + rnd = np.random.RandomState(0) + n = 10 + # preserve use of old random_state during SPEC 7 transition + p = Rotation.random(num=n, random_state=rnd) + p = rotation_to_xp(p, xp) + p_mat = p.as_matrix() + q_mat = p.inv().as_matrix() + + result1 = p_mat @ q_mat + result2 = q_mat @ p_mat + + eye3d = xp.empty((n, 3, 3)) + eye3d = xpx.at(eye3d)[..., :3, :3].set(xp.eye(3)) + + xp_assert_close(result1, eye3d, atol=atol) + xp_assert_close(result2, eye3d, atol=atol) + + # Batched version + batch_shape = (10, 3, 7) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + quat = xp.asarray(rnd.normal(size=batch_shape + (4,)), dtype=dtype) + r = Rotation.from_quat(quat) + p_mat = r.as_matrix() + q_mat = r.inv().as_matrix() + result1 = p_mat @ q_mat + result2 = q_mat @ p_mat + eye_nd = xp.empty(batch_shape + (3, 3)) + eye_nd = xpx.at(eye_nd)[..., :3, :3].set(xp.eye(3)) + xp_assert_close(result1, eye_nd, atol=atol) + xp_assert_close(result2, eye_nd, atol=atol) + + +@make_xp_test_case(Rotation.inv, Rotation.as_matrix) +def test_inv_single_rotation(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-7 + rng = np.random.default_rng(146972845698875399755764481408308808739) + p = rotation_to_xp(Rotation.random(rng=rng), xp) + q = p.inv() + + p_mat = p.as_matrix() + q_mat = q.as_matrix() + res1 = xp.matmul(p_mat, q_mat) + res2 = xp.matmul(q_mat, p_mat) + + eye = xp.eye(3) + + xp_assert_close(res1, eye, atol=atol) + xp_assert_close(res2, eye, atol=atol) + + x = rotation_to_xp(Rotation.random(num=1, rng=rng), xp) + y = x.inv() + + x_matrix = x.as_matrix() + y_matrix = y.as_matrix() + result1 = xp.linalg.matmul(x_matrix, y_matrix) + result2 = xp.linalg.matmul(y_matrix, x_matrix) + + eye3d = xp.empty((1, 3, 3)) + eye3d = xpx.at(eye3d)[..., :3, :3].set(xp.eye(3)) + + xp_assert_close(result1, eye3d, atol=atol) + xp_assert_close(result2, eye3d, atol=atol) + + +@make_xp_test_case(Rotation.magnitude, Rotation.inv) +def test_identity_magnitude(xp): + n = 10 + r = rotation_to_xp(Rotation.identity(n), xp) + expected = xp.zeros(n) + xp_assert_close(r.magnitude(), expected) + xp_assert_close(r.inv().magnitude(), expected) + + +@make_xp_test_case(Rotation.magnitude, Rotation.inv) +def test_single_identity_magnitude(xp): + r = rotation_to_xp(Rotation.identity(), xp) + assert r.magnitude() == 0 + assert r.inv().magnitude() == 0 + + +@make_xp_test_case(Rotation.inv, Rotation.magnitude) +def test_identity_invariance(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-7 + n = 10 + p = rotation_to_xp(Rotation.random(n, rng=0), xp) + q = rotation_to_xp(Rotation.identity(n), xp) + result = p * q + xp_assert_close(p.as_quat(), result.as_quat()) + + result = result * p.inv() + xp_assert_close(result.magnitude(), xp.zeros(n), atol=atol) + + +@make_xp_test_case(Rotation.inv, Rotation.magnitude) +def test_single_identity_invariance(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-7 + n = 10 + p = rotation_to_xp(Rotation.random(n, rng=0), xp) + q = rotation_to_xp(Rotation.identity(), xp) + result = p * q + xp_assert_close(p.as_quat(), result.as_quat()) + + result = result * p.inv() + xp_assert_close(result.magnitude(), xp.zeros(n), atol=atol) + + +def test_identity_shape(): # Not an xp test, identity is using numpy only for now + r = Rotation.identity(shape=()) + assert r.as_quat().shape == (4,) + r = Rotation.identity(shape=5) # Shape can be int + assert r.as_quat().shape == (5, 4) + r = Rotation.identity(shape=(2, 3)) + assert r.as_quat().shape == (2, 3, 4) + # Test values + r = Rotation.identity(shape=(2, 2, 3)) + xp_assert_equal(r.as_quat().reshape(-1, 4), np.tile(np.eye(4)[-1], (2 * 2 * 3, 1))) + # Errors + with pytest.raises(ValueError, match="`shape` must be an int or a tuple of ints"): + Rotation.identity(shape=2.5) + with pytest.raises(ValueError, match="Only one of `num` or `shape` can be"): + Rotation.identity(num=3, shape=(2, 2)) + with pytest.raises(TypeError, match="takes from 0 to 1 positional arguments"): + Rotation.identity(3, 3) + + +@make_xp_test_case(Rotation.magnitude) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_magnitude(xp, ndim: int): + quat_shape = (1,) * (ndim - 1) + (4,) + quat = xp.reshape(xp.eye(4), quat_shape + (4,)) + r = Rotation.from_quat(quat) + result = r.magnitude() + expected_result = xp.asarray([xp.pi, xp.pi, xp.pi, 0]) + expected_result = xp.reshape(expected_result, quat_shape) + xp_assert_close(result, expected_result) + + r = Rotation.from_quat(-quat) + result = r.magnitude() + xp_assert_close(result, expected_result) + + +@make_xp_test_case(Rotation.magnitude) +def test_magnitude_single_rotation(xp): + r = Rotation.from_quat(xp.eye(4)) + result1 = r[0].magnitude() + xp_assert_close(result1, xp.asarray(xp.pi)[()]) + + result2 = r[3].magnitude() + xp_assert_close(result2, xp.asarray(0.0)[()]) + + +@make_xp_test_case(Rotation.inv, Rotation.magnitude, Rotation.approx_equal) +def test_approx_equal(xp): + rng = np.random.default_rng(146972845698875399755764481408308808739) + p = Rotation.random(10, rng=rng) + q = Rotation.random(10, rng=rng) + r_mag = (p * q.inv()).magnitude() + p = rotation_to_xp(p, xp) + q = rotation_to_xp(q, xp) + # ensure we get mix of Trues and Falses + atol = xp.asarray(np.median(r_mag)) + xp_assert_equal(p.approx_equal(q, atol), (xp.asarray(r_mag) < atol)) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.approx_equal) +def test_approx_equal_single_rotation(xp): + # also tests passing single argument to approx_equal + p = Rotation.from_rotvec(xp.asarray([0, 0, 1e-9])) # less than default atol of 1e-8 + q = Rotation.from_quat(xp.eye(4)) + assert p.approx_equal(q[3]) + assert not p.approx_equal(q[0]) + + # test passing atol and using degrees + assert not p.approx_equal(q[3], atol=1e-10) + assert not p.approx_equal(q[3], atol=1e-8, degrees=True) + with pytest.warns(UserWarning, match="atol must be set"): + assert p.approx_equal(q[3], degrees=True) + + +@make_xp_test_case(Rotation.inv, Rotation.magnitude, Rotation.approx_equal) +def test_approx_equal_batched(xp): + # Same shapes + batch_shape = (2, 10, 3) + rng = np.random.default_rng(0) + p = Rotation.from_quat(rng.normal(size=batch_shape + (4,))) + q = Rotation.from_quat(rng.normal(size=batch_shape + (4,))) + r_mag = (p * q.inv()).magnitude() # Must be computed as numpy array for np.median + p = rotation_to_xp(p, xp) + q = rotation_to_xp(q, xp) + assert r_mag.shape == batch_shape + # ensure we get mix of Trues and Falses + atol = xp.asarray(np.median(r_mag)) + xp_assert_equal(p.approx_equal(q, atol), (xp.asarray(r_mag) < atol)) + + # Broadcastable shapes of same length + p = Rotation.from_quat(rng.normal(size=batch_shape + (4,))) + q = Rotation.from_quat(rng.normal(size=(1, 10, 1, 4))) + r_mag = (p * q.inv()).magnitude() + p = rotation_to_xp(p, xp) + q = rotation_to_xp(q, xp) + assert r_mag.shape == batch_shape + atol = xp.asarray(np.median(r_mag)) + xp_assert_equal(p.approx_equal(q, atol), (xp.asarray(r_mag) < atol)) + + # Broadcastable shapes of different length + p = Rotation.from_quat(rng.normal(size=batch_shape + (4,))) + q = Rotation.from_quat(rng.normal(size=(1, 3, 4))) + r_mag = (p * q.inv()).magnitude() + p = rotation_to_xp(p, xp) + q = rotation_to_xp(q, xp) + assert r_mag.shape == batch_shape + atol = xp.asarray(np.median(r_mag)) + xp_assert_equal(p.approx_equal(q, atol), (xp.asarray(r_mag) < atol)) + + +@make_xp_test_case(Rotation.approx_equal) +def test_approx_equal_batched_input_validation(xp): + p = Rotation.from_quat(xp.ones((2, 3, 4))) + q = Rotation.from_quat(xp.ones((3, 2, 4))) + with pytest.raises(ValueError, match="broadcastable shapes"): + p.approx_equal(q) + + p = Rotation.from_quat(xp.ones((2, 4))) + q = Rotation.from_quat(xp.ones((3, 4))) + with pytest.raises(ValueError, match="broadcastable shapes"): + p.approx_equal(q) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.mean, Rotation.magnitude) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_mean(xp, ndim: int): + axes = xp.concat((-xp.eye(3), xp.eye(3))) + axes = xp.reshape(axes, (1,) * (ndim - 1) + (6, 3)) + thetas = xp.linspace(0, xp.pi / 2, 100) + desired = xp.asarray(0.0)[()] + atol = 1e-6 if xp_default_dtype(xp) is xp.float32 else 1e-10 + for t in thetas: + r_mean = Rotation.from_rotvec(t * axes).mean() + assert r_mean.shape == () + xp_assert_close(r_mean.magnitude(), desired, atol=atol) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.mean, Rotation.magnitude) +@pytest.mark.parametrize("ndim", range(1, 5)) +def test_mean_axis(xp, ndim: int): + axes = xp.tile(xp.concat((-xp.eye(3), xp.eye(3))), (3,) * (ndim - 1) + (1, 1)) + theta = xp.pi / 4 + r = Rotation.from_rotvec(theta * axes) + + # Test mean over last axis + desired = xp.full(axes.shape[:-2], 0.0) + if ndim == 1: + desired = desired[()] + atol = 1e-6 if xp_default_dtype(xp) is xp.float32 else 1e-10 + xp_assert_close(r.mean(axis=-1).magnitude(), desired, atol=atol) + + # Test tuple axes + desired = xp.full(axes.shape[1:-2], 0.0) + if ndim < 3: + desired = desired[()] + xp_assert_close(r.mean(axis=(0, -1)).magnitude(), desired, atol=atol) + + # Empty axis tuple should return Rotation unchanged + r_mean = r.mean(axis=()) + xp_assert_close(r_mean.as_quat(canonical=True), r.as_quat(canonical=True), + atol=atol) + + +@make_xp_test_case(Rotation.mean, Rotation.magnitude) +def test_mean_compare_axis(xp): + # Create a random set of rotations and compare the mean over an axis with the + # mean without axis of the sliced quaternion + atol = 1e-10 if xpx.default_dtype(xp) == xp.float64 else 1e-6 + rng = np.random.default_rng(0) + q = xp.asarray(rng.normal(size=(4, 5, 6, 4)), dtype=xpx.default_dtype(xp)) + r = Rotation.from_quat(q) + + mean_0 = r.mean(axis=0) + for i in range(q.shape[1]): + for j in range(q.shape[2]): + mean_slice = Rotation.from_quat(q[:, i, j, ...]).mean() + xp_assert_close((mean_0[i][j] * mean_slice.inv()).magnitude(), + xp.asarray(0.0)[()], atol=atol) + mean_1_2 = r.mean(axis=(1, 2)) + for i in range(q.shape[0]): + mean_slice = Rotation.from_quat(q[i, ...]).mean() + xp_assert_close((mean_1_2[i] * mean_slice.inv()).magnitude(), + xp.asarray(0.0)[()], atol=atol) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.mean, Rotation.inv, + Rotation.magnitude) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_weighted_mean(xp, ndim: int): + # test that doubling a weight is equivalent to including a rotation twice. + thetas = xp.linspace(0, xp.pi / 2, 100) + + # Create batched copies of the same setup + batch_shape = (ndim,) * (ndim - 1) + axes = xp.asarray([[0.0, 0, 0], [1, 0, 0], [1, 0, 0]]) + weights = xp.asarray([1, 2]) + axes = xp.tile(axes, batch_shape + (1, 1)) + weights = xp.tile(weights, batch_shape + (1,)) + + expected = xp.asarray(0.0)[()] + for t in thetas: + rw = Rotation.from_rotvec(t * axes[..., :2, :]) + mw = rw.mean(weights=weights) + + r = Rotation.from_rotvec(t * axes) + m = r.mean() + assert m.shape == () + xp_assert_close((m * mw.inv()).magnitude(), expected, atol=1e-6) + + +@make_xp_test_case(Rotation.mean) +def test_mean_input_validation(xp): + r = Rotation.from_quat(xp.eye(4)) + if is_lazy_array(r.as_quat()): + m = r.mean(weights=-xp.ones(4)) + assert xp.all(xp.isnan(m._quat)) + else: + with pytest.raises(ValueError, match="non-negative"): + r.mean(weights=-xp.ones(4)) + + # Test weight shape mismatch + r = Rotation.from_quat(xp.ones((3, 4))) + with pytest.raises(ValueError, match="Expected `weights` to"): + r.mean(weights=xp.ones((2,))) + r = Rotation.from_quat(xp.ones((2, 3, 4))) + with pytest.raises(ValueError, match="Expected `weights` to"): + r.mean(weights=xp.ones((2, 2))) + + # Test wrong axis + with pytest.raises(ValueError, match=r"axis .* is out of bounds"): + r.mean(axis=3) + with pytest.raises(ValueError, match=r"axis .* is out of bounds"): + r.mean(axis=(-1, 2)) + with pytest.raises(ValueError, match="`axis` must be None, int, or tuple of ints."): + r.mean(axis="0") + with pytest.raises(ValueError, match=r"axis .* is out of bounds"): + r.mean(axis=-12) + + +@make_xp_test_case(Rotation.reduce) +def test_reduction_no_indices(xp): + r = Rotation.from_quat(xp.asarray([0.0, 0.0, 0.0, 1.0])) + result = r.reduce(return_indices=False) + assert isinstance(result, Rotation) + + +@make_xp_test_case(Rotation.reduce) +def test_reduction_none_indices(xp): + r = Rotation.from_quat(xp.asarray([0.0, 0.0, 0.0, 1.0])) + result = r.reduce(return_indices=True) + assert type(result) is tuple + assert len(result) == 3 + + reduced, left_best, right_best = result + assert left_best is None + assert right_best is None + + +@make_xp_test_case(Rotation.reduce, Rotation.inv, Rotation.magnitude) +def test_reduction_scalar_calculation(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + rng = np.random.default_rng(146972845698875399755764481408308808739) + l_np = Rotation.random(5, rng=rng) + r_np = Rotation.random(10, rng=rng) + p_np = Rotation.random(7, rng=rng) + l = rotation_to_xp(l_np, xp) + r = rotation_to_xp(r_np, xp) + p = rotation_to_xp(p_np, xp) + reduced, left_best, right_best = p.reduce(l, r, return_indices=True) + + # Loop implementation of the vectorized calculation in Rotation.reduce + scalars = np.zeros((len(l_np), len(p_np), len(r_np))) + for i, li in enumerate(l_np): + for j, pj in enumerate(p_np): + for k, rk in enumerate(r_np): + scalars[i, j, k] = np.abs((li * pj * rk).as_quat()[3]) + scalars = np.reshape(np.moveaxis(scalars, 1, 0), (scalars.shape[1], -1)) + + max_ind = np.argmax(np.reshape(scalars, (len(p), -1)), axis=1) + left_best_check = xp.asarray(max_ind // len(r)) + right_best_check = xp.asarray(max_ind % len(r)) + assert xp.all(left_best == left_best_check) + assert xp.all(right_best == right_best_check) + + reduced_check = l[left_best_check] * p * r[right_best_check] + mag = (reduced.inv() * reduced_check).magnitude() + xp_assert_close(mag, xp.zeros(len(p)), atol=atol) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.apply) +def test_apply_single_rotation_single_point(xp): + dtype = xpx.default_dtype(xp) + mat = xp.asarray([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + r_1d = Rotation.from_matrix(mat) + r_2d = Rotation.from_matrix(xp.expand_dims(mat, axis=0)) + + v_1d = xp.asarray([1.0, 2, 3], dtype=dtype) + v_2d = xp.expand_dims(v_1d, axis=0) + v1d_rotated = xp.asarray([-2.0, 1, 3], dtype=dtype) + v2d_rotated = xp.expand_dims(v1d_rotated, axis=0) + + xp_assert_close(r_1d.apply(v_1d), v1d_rotated) + xp_assert_close(r_1d.apply(v_2d), v2d_rotated) + xp_assert_close(r_2d.apply(v_1d), v2d_rotated) + xp_assert_close(r_2d.apply(v_2d), v2d_rotated) + + v1d_inverse = xp.asarray([2.0, -1, 3], dtype=dtype) + v2d_inverse = xp.expand_dims(v1d_inverse, axis=0) + + xp_assert_close(r_1d.apply(v_1d, inverse=True), v1d_inverse) + xp_assert_close(r_1d.apply(v_2d, inverse=True), v2d_inverse) + xp_assert_close(r_2d.apply(v_1d, inverse=True), v2d_inverse) + xp_assert_close(r_2d.apply(v_2d, inverse=True), v2d_inverse) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.apply) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_apply_single_rotation_multiple_points(xp, ndim: int): + dtype = xpx.default_dtype(xp) + mat = xp.asarray([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + r1 = Rotation.from_matrix(mat) + r2 = Rotation.from_matrix(xp.expand_dims(mat, axis=0)) + + rng = np.random.default_rng(0) + batch_shape = (ndim,) * (ndim - 1) + v = xp.asarray(rng.normal(size=batch_shape + (2, 3)), dtype=dtype) + v_rotated = xp.stack([-v[..., 1], v[..., 0], v[..., 2]], axis=-1) + + xp_assert_close(r1.apply(v), v_rotated) + xp_assert_close(r2.apply(v), v_rotated) + + v_inverse = xp.stack([v[..., 1], -v[..., 0], v[..., 2]], axis=-1) + + xp_assert_close(r1.apply(v, inverse=True), v_inverse) + xp_assert_close(r2.apply(v, inverse=True), v_inverse) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.apply) +@pytest.mark.parametrize("ndim", range(1, 5)) +def test_apply_multiple_rotations_single_point(xp, ndim: int): + dtype = xpx.default_dtype(xp) + mat = np.empty((2, 3, 3)) + mat[0] = np.array([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + mat[1] = np.array([ + [1, 0, 0], + [0, 0, -1], + [0, 1, 0] + ]) + mat = xp.asarray(mat, dtype=dtype) + batch_shape = (ndim,) * (ndim - 1) + mat = xp.tile(mat, batch_shape + (1, 1, 1)) + r = Rotation.from_matrix(mat) + + v1 = xp.asarray([1, 2, 3]) + v2 = xp.expand_dims(v1, axis=0) + + v_rotated = xp.asarray([[-2.0, 1, 3], [1, -3, 2]]) + v_rotated = xp.tile(v_rotated, batch_shape + (1, 1)) + + xp_assert_close(r.apply(v1), v_rotated) + xp_assert_close(r.apply(v2), v_rotated) + + v_inverse = xp.asarray([[2.0, -1, 3], [1, 3, -2]]) + v_inverse = xp.tile(v_inverse, batch_shape + (1, 1)) + + xp_assert_close(r.apply(v1, inverse=True), v_inverse) + xp_assert_close(r.apply(v2, inverse=True), v_inverse) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.apply) +@pytest.mark.parametrize("ndim", range(1, 5)) +def test_apply_multiple_rotations_multiple_points(xp, ndim: int): + dtype = xpx.default_dtype(xp) + mat = np.empty((2, 3, 3)) + mat[0] = np.array([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + mat[1] = np.array([ + [1, 0, 0], + [0, 0, -1], + [0, 1, 0] + ]) + mat = xp.asarray(mat, dtype=dtype) + batch_shape = (ndim,) * (ndim - 1) + mat = xp.tile(mat, batch_shape + (1, 1, 1)) + r = Rotation.from_matrix(mat) + + v = xp.asarray([[1, 2, 3], [4, 5, 6]], dtype=dtype) + v_rotated = xp.asarray([[-2.0, 1, 3], [4, -6, 5]], dtype=dtype) + v_rotated = xp.tile(v_rotated, batch_shape + (1, 1)) + xp_assert_close(r.apply(v), v_rotated) + + v_inverse = xp.asarray([[2.0, -1, 3], [4, 6, -5]], dtype=dtype) + v_inverse = xp.tile(v_inverse, batch_shape + (1, 1)) + xp_assert_close(r.apply(v, inverse=True), v_inverse) + + +@make_xp_test_case(Rotation.apply) +def test_apply_shapes(xp): + rng = np.random.default_rng(0) + # Broadcast shape: (6, 5, 4, 2) ( + (3,) for vectors, + (4,) for rotations) + vector_shapes = [(), (1,), (2,), (1, 2), (5, 1, 2)] + rot_shapes = [(), (1,), (2,), (1, 2), (4, 2), (1, 4, 2), (5, 4, 2), (6, 5, 4, 2)] + + for q_shape, v_shape in product(rot_shapes, vector_shapes): + v = xp.asarray(rng.normal(size=v_shape + (3,))) + q = xp.asarray(rng.normal(size=q_shape + (4,))) + r = Rotation.from_quat(q) + shape = np.broadcast_shapes(q_shape, v_shape) + (3,) + x = r.apply(v) + assert x.shape == shape + x = r.apply(v, inverse=True) + assert x.shape == shape + + +def test_apply_array_like(): + rng = np.random.default_rng(123) + # Single vector + r = Rotation.random(rng=rng) + t = rng.uniform(-100, 100, size=(3,)) + v = r.apply(t.tolist()) + v_expected = r.apply(t) + xp_assert_close(v, v_expected, atol=1e-12) + # Multiple vectors + t = rng.uniform(-100, 100, size=(2, 3)) + v = r.apply(t.tolist()) + v_expected = r.apply(t) + xp_assert_close(v, v_expected, atol=1e-12) + + +@make_xp_test_case(Rotation.apply) +def test_apply_input_validation(xp): + r = Rotation.from_quat(xp.ones(4)) + with pytest.raises(ValueError, match="Expected input of shape"): + r.apply(xp.ones(2)) + with pytest.raises(ValueError, match="Expected input of shape"): + r.apply(xp.ones((2, 2))) + r = Rotation.from_quat(xp.ones((2, 4))) + with pytest.raises(ValueError, match="Cannot broadcast"): + r.apply(xp.ones((3, 3))) + r = Rotation.from_quat(xp.ones((1, 7, 2, 4))) + with pytest.raises(ValueError, match="Cannot broadcast"): + r.apply(xp.ones((2, 2, 3))) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.as_matrix, Rotation.__getitem__) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_getitem(xp, ndim: int): + rng = np.random.default_rng(0) + quat = rng.normal(size=(2, ) + (ndim,) * (ndim - 1) + (4,)) + mat = xp.asarray(Rotation.from_quat(quat).as_matrix()) + r = Rotation.from_matrix(mat) + + xp_assert_close(r[0].as_matrix(), mat[0, ...], atol=1e-15) + xp_assert_close(r[1].as_matrix(), mat[1, ...], atol=1e-15) + xp_assert_close(r[:-1].as_matrix(), xp.expand_dims(mat[0, ...], axis=0), atol=1e-15) + + +@make_xp_test_case(Rotation.__getitem__) +def test_getitem_single(xp): + with pytest.raises(TypeError, match='not subscriptable'): + Rotation.from_quat(xp.asarray([0, 0, 0, 1]))[0] + + +@make_xp_test_case(Rotation.from_matrix, Rotation.__getitem__, Rotation.as_matrix) +def test_getitem_array_like(): + mat = np.array([[[0.0, -1, 0], + [1, 0, 0], + [0, 0, 1]], + [[1, 0, 0], + [0, 0, -1], + [0, 1, 0]]]) + r = Rotation.from_matrix(mat) + xp_assert_close(r[[0]].as_matrix(), mat[[0]], atol=1e-15) + xp_assert_close(r[[0, 1]].as_matrix(), mat[[0, 1]], atol=1e-15) + + +@make_xp_test_case(Rotation.__setitem__) +def test_setitem_single(xp): + r = Rotation.from_quat(xp.asarray([0, 0, 0, 1])) + with pytest.raises(TypeError, match='not subscriptable'): + r[0] = Rotation.from_quat(xp.asarray([0, 0, 0, 1])) + + +@make_xp_test_case(Rotation.__setitem__) +def test_setitem_slice(xp): + rng = np.random.default_rng(146972845698875399755764481408308808739) + r1 = rotation_to_xp(Rotation.random(10, rng=rng), xp) + r2 = rotation_to_xp(Rotation.random(5, rng=rng), xp) + r1[1:6] = r2 + xp_assert_equal(r1[1:6].as_quat(), r2.as_quat()) + + # Multiple dimensions + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(3, 5, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 5, 4)))) + r1[1:3] = r2 + xp_assert_equal(r1[1:3].as_quat(), r2.as_quat()) + + +@make_xp_test_case(Rotation.__setitem__) +def test_setitem_integer(xp): + rng = np.random.default_rng(146972845698875399755764481408308808739) + r1 = rotation_to_xp(Rotation.random(10, rng=rng), xp) + r2 = rotation_to_xp(Rotation.random(rng=rng), xp) + r1[1] = r2 + xp_assert_equal(r1[1].as_quat(), r2.as_quat()) + + # Multiple dimensions + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(3, 5, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(5, 4)))) + r1[1] = r2 + xp_assert_equal(r1[1].as_quat(), r2.as_quat()) + + +@make_xp_test_case(Rotation.__setitem__) +def test_setitem_wrong_type(xp): + r = rotation_to_xp(Rotation.random(10, rng=0), xp) + with pytest.raises(TypeError, match='Rotation object'): + r[0] = 1 + + +@make_xp_test_case(Rotation.from_matrix) +def test_n_rotations(xp): + mat = np.empty((2, 3, 3)) + mat[0] = np.array([ + [0, -1, 0], + [1, 0, 0], + [0, 0, 1] + ]) + mat[1] = np.array([ + [1, 0, 0], + [0, 0, -1], + [0, 1, 0] + ]) + mat = xp.asarray(mat) + r = Rotation.from_matrix(mat) + + assert_equal(len(r), 2) + assert_equal(len(r[:-1]), 1) + + +def test_random_rotation(): + # No xp testing since random rotations are always using NumPy + rng = np.random.default_rng(0) + assert_equal(Rotation.random(rng=rng).as_quat().shape, (4,)) + assert_equal(Rotation.random(None, rng=rng).as_quat().shape, (4,)) + assert_equal(Rotation.random(1, rng=rng).as_quat().shape, (1, 4)) + assert_equal(Rotation.random(5, rng=rng).as_quat().shape, (5, 4)) + # Shape argument + assert_equal(Rotation.random(rng=rng, shape=()).as_quat().shape, (4,)) + assert_equal(Rotation.random(rng=rng, shape=(3,)).as_quat().shape, (3, 4)) + assert_equal(Rotation.random(rng=rng, shape=(2, 3)).as_quat().shape, (2, 3, 4)) + # Values should be the same for num=prod(shape) + rng1, rng2 = np.random.default_rng(42), np.random.default_rng(42) + r_num = Rotation.random(6, rng=rng1) + r_shape = Rotation.random(rng=rng2, shape=(2, 3)) + xp_assert_close(r_num.as_quat(), r_shape.as_quat().reshape(6, 4), atol=1e-12) + # Errors + with pytest.raises(ValueError, match="Only one of `num` or `shape` can be"): + Rotation.random(num=3,rng=rng, shape=(2, 2)) + with pytest.raises(ValueError, match="`shape` must be an int or a tuple of ints"): + Rotation.random(rng=rng, shape=2.5) + with pytest.raises(TypeError, match="takes from 0 to 2 positional arguments"): + Rotation.random(1, rng, None) # Shape should be kwarg only + + +@make_xp_test_case(Rotation.align_vectors, Rotation.as_matrix) +def test_align_vectors_no_rotation(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-5 + x = xp.asarray([[1, 2, 3], [4, 5, 6]], dtype=dtype) + y = xp.asarray(x, copy=True) + + r, rssd = Rotation.align_vectors(x, y) + xp_assert_close(r.as_matrix(), xp.eye(3), atol=atol) + xp_assert_close(rssd, xp.asarray(0.0)[()], check_shape=False, atol=1e-6) + + +@make_xp_test_case(Rotation.apply, Rotation.align_vectors) +def test_align_vectors_no_noise(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-7 if dtype == xp.float64 else 2e-3 + rng = np.random.default_rng(14697284569885399755764481408308808739) + c = rotation_to_xp(Rotation.random(rng=rng), xp) + b = xp.asarray(rng.normal(size=(5, 3)), dtype=dtype) + a = c.apply(b) + + est, rssd = Rotation.align_vectors(a, b) + xp_assert_close(c.as_quat(), est.as_quat()) + xp_assert_close(rssd, xp.asarray(0.0)[()], check_shape=False, atol=atol) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.apply) +def test_align_vectors_improper_rotation(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-7 if dtype == xp.float64 else 1e-3 + # Tests correct logic for issue #10444 + x = xp.asarray([[0.89299824, -0.44372674, 0.0752378], + [0.60221789, -0.47564102, -0.6411702]]) + y = xp.asarray([[0.02386536, -0.82176463, 0.5693271], + [-0.27654929, -0.95191427, -0.1318321]]) + + est, rssd = Rotation.align_vectors(x, y) + xp_assert_close(x, est.apply(y), atol=1e-6) + xp_assert_close(rssd, xp.asarray(0.0)[()], check_shape=False, atol=atol) + + +@make_xp_test_case(Rotation.align_vectors) +def test_align_vectors_rssd_sensitivity(xp): + rssd_expected = xp.asarray(0.141421356237308)[()] + sens_expected = xp.asarray([[0.2, 0. , 0.], + [0. , 1.5, 1.], + [0. , 1. , 1.]]) + atol = 1e-6 + a = xp.asarray([[0, 1, 0], [0, 1, 1], [0, 1, 1]]) + b = xp.asarray([[1, 0, 0], [1, 1.1, 0], [1, 0.9, 0]]) + rot, rssd, sens = Rotation.align_vectors(a, b, return_sensitivity=True) + xp_assert_close(rssd, rssd_expected, atol=atol) + xp_assert_close(sens, sens_expected, atol=atol) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.as_matrix) +def test_align_vectors_scaled_weights(xp): + n = 10 + a = xp.asarray(Rotation.random(n, rng=0).apply([1, 0, 0])) + b = xp.asarray(Rotation.random(n, rng=1).apply([1, 0, 0])) + scale = 2 + + est1, rssd1, cov1 = Rotation.align_vectors(a, b, xp.ones(n), True) + est2, rssd2, cov2 = Rotation.align_vectors(a, b, scale * xp.ones(n), True) + + xp_assert_close(est1.as_matrix(), est2.as_matrix()) + xp_assert_close(math.sqrt(scale) * rssd1, rssd2, atol=1e-6) + xp_assert_close(cov1, cov2) + + +@make_xp_test_case(Rotation.apply, Rotation.from_rotvec, Rotation.align_vectors) +def test_align_vectors_noise(xp): + dtype = xpx.default_dtype(xp) + rng = np.random.default_rng(146972845698875399755764481408308808739) + n_vectors = 100 + rot = rotation_to_xp(Rotation.random(rng=rng), xp) + vectors = xp.asarray(rng.normal(size=(n_vectors, 3)), dtype=dtype) + result = rot.apply(vectors) + + # The paper adds noise as independently distributed angular errors + sigma = np.deg2rad(1) + tolerance = 1.5 * sigma + noise = Rotation.from_rotvec( + xp.asarray(rng.normal(size=(n_vectors, 3), scale=sigma), dtype=dtype) + ) + + # Attitude errors must preserve norm. Hence apply individual random + # rotations to each vector. + noisy_result = noise.apply(result) + + est, rssd, cov = Rotation.align_vectors(noisy_result, vectors, + return_sensitivity=True) + + # Use rotation compositions to find out closeness + error_vector = (rot * est.inv()).as_rotvec() + xp_assert_close(error_vector[0], xp.asarray(0.0)[()], atol=tolerance) + xp_assert_close(error_vector[1], xp.asarray(0.0)[()], atol=tolerance) + xp_assert_close(error_vector[2], xp.asarray(0.0)[()], atol=tolerance) + + # Check error bounds using covariance matrix + cov *= xp.asarray(sigma) + xp_assert_close(cov[0, 0], xp.asarray(0.0)[()], atol=tolerance) + xp_assert_close(cov[1, 1], xp.asarray(0.0)[()], atol=tolerance) + xp_assert_close(cov[2, 2], xp.asarray(0.0)[()], atol=tolerance) + + rssd_check = xp.sum((noisy_result - est.apply(vectors)) ** 2) ** 0.5 + xp_assert_close(rssd, rssd_check, check_shape=False) + + +@make_xp_test_case(Rotation.align_vectors) +def test_align_vectors_invalid_input(xp): + with pytest.raises(ValueError, match="Expected input `a` to have shape"): + a, b = xp.asarray([1, 2, 3, 4]), xp.asarray([1, 2, 3]) + Rotation.align_vectors(a, b) + + with pytest.raises(ValueError, match="Expected input `b` to have shape"): + a, b = xp.asarray([1, 2, 3]), xp.asarray([1, 2, 3, 4]) + Rotation.align_vectors(a, b) + + with pytest.raises(ValueError, match="Expected inputs `a` and `b` " + "to have same shapes"): + a, b = xp.asarray([[1, 2, 3], [4, 5, 6]]), xp.asarray([[1, 2, 3]]) + Rotation.align_vectors(a, b) + + with pytest.raises(ValueError, + match="Expected `weights` to be 1 dimensional"): + a, b = xp.asarray([[1, 2, 3]]), xp.asarray([[1, 2, 3]]) + weights = xp.asarray([[1]]) + Rotation.align_vectors(a, b, weights) + + with pytest.raises(ValueError, + match="Expected `weights` to have number of values"): + a, b = xp.asarray([[1, 2, 3], [4, 5, 6]]), xp.asarray([[1, 2, 3], [4, 5, 6]]) + weights = xp.asarray([1, 2, 3]) + Rotation.align_vectors(a, b, weights) + + a, b = xp.asarray([[1, 2, 3]]), xp.asarray([[1, 2, 3]]) + weights = xp.asarray([-1]) + if is_lazy_array(weights): + r, rssd = Rotation.align_vectors(a, b, weights) + assert xp.all(xp.isnan(r.as_quat())), "Quaternion should be nan" + assert xp.isnan(rssd), "RSSD should be nan" + else: + with pytest.raises(ValueError, + match="`weights` may not contain negative values"): + Rotation.align_vectors(a, b, weights) + + a, b = xp.asarray([[1, 2, 3], [4, 5, 6]]), xp.asarray([[1, 2, 3], [4, 5, 6]]) + weights = xp.asarray([xp.inf, xp.inf]) + if is_lazy_array(weights): + r, rssd = Rotation.align_vectors(a, b, weights) + assert xp.all(xp.isnan(r.as_quat())), "Quaternion should be nan" + assert xp.isnan(rssd), "RSSD should be nan" + else: + with pytest.raises(ValueError, + match="Only one infinite weight is allowed"): + Rotation.align_vectors(a, b, weights) + + a, b = xp.asarray([[0, 0, 0]]), xp.asarray([[1, 2, 3]]) + if is_lazy_array(a): + r, rssd = Rotation.align_vectors(a, b) + assert xp.all(xp.isnan(r.as_quat())), "Quaternion should be nan" + assert xp.isnan(rssd), "RSSD should be nan" + else: + with pytest.raises(ValueError, + match="Cannot align zero length primary vectors"): + Rotation.align_vectors(a, b) + + a, b = xp.asarray([[1, 2, 3], [4, 5, 6]]), xp.asarray([[1, 2, 3], [4, 5, 6]]) + weights = xp.asarray([xp.inf, 1]) + if is_lazy_array(a): + r, rssd, sens = Rotation.align_vectors(a, b, weights, return_sensitivity=True) + assert xp.all(xp.isnan(sens)), "Sensitivity matrix should be nan" + else: + with pytest.raises(ValueError, + match="Cannot return sensitivity matrix"): + Rotation.align_vectors(a, b, weights, return_sensitivity=True) + + a, b = xp.asarray([[1, 2, 3]]), xp.asarray([[1, 2, 3]]) + if is_lazy_array(a): + r, rssd, sens = Rotation.align_vectors(a, b, return_sensitivity=True) + assert xp.all(xp.isnan(sens)), "Sensitivity matrix should be nan" + else: + with pytest.raises(ValueError, + match="Cannot return sensitivity matrix"): + Rotation.align_vectors(a, b, return_sensitivity=True) + + # No broadcast support for align_vectors + a, b = xp.asarray([[[1, 2, 3]]]), xp.asarray([[[1, 2, 3]]]) + with pytest.raises(ValueError, + match="Expected inputs `a` and `b` to have shape"): + Rotation.align_vectors(a, b) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.as_matrix, Rotation.apply) +def test_align_vectors_align_constrain(xp): + # Align the primary +X B axis with the primary +Y A axis, and rotate about + # it such that the +Y B axis (residual of the [1, 1, 0] secondary b vector) + # is aligned with the +Z A axis (residual of the [0, 1, 1] secondary a + # vector) + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + b = xp.asarray([[1, 0, 0], [1, 1, 0]]) + a = xp.asarray([[0.0, 1, 0], [0, 1, 1]]) + m_expected = xp.asarray([[0.0, 0, 1], + [1, 0, 0], + [0, 1, 0]]) + R, rssd = Rotation.align_vectors(a, b, weights=xp.asarray([xp.inf, 1])) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + xp_assert_close(R.apply(b), a, atol=atol) # Pri and sec align exactly + xp_assert_close(rssd, xp.asarray(0.0)[()], atol=atol) + + # Do the same but with an inexact secondary rotation + b = xp.asarray([[1, 0, 0], [1, 2, 0]]) + rssd_expected = 1.0 + R, rssd = Rotation.align_vectors(a, b, weights=xp.asarray([xp.inf, 1])) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + xp_assert_close(R.apply(b)[0, ...], a[0, ...], atol=atol) # Only pri aligns exactly + assert xpx.isclose(rssd, rssd_expected, atol=atol, xp=xp) + a_expected = xp.asarray([[0.0, 1, 0], [0, 1, 2]]) + xp_assert_close(R.apply(b), a_expected, atol=atol) + + # Check random vectors + b = xp.asarray([[1, 2, 3], [-2, 3, -1]]) + a = xp.asarray([[-1.0, 3, 2], [1, -1, 2]]) + rssd_expected = 1.3101595297515016 + R, rssd = Rotation.align_vectors(a, b, weights=xp.asarray([xp.inf, 1])) + xp_assert_close(R.apply(b)[0, ...], a[0, ...], atol=atol) # Only pri aligns exactly + assert xpx.isclose(rssd, rssd_expected, atol=atol, xp=xp) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.as_matrix) +def test_align_vectors_near_inf(xp): + # align_vectors should return near the same result for high weights as for + # infinite weights. rssd will be different with floating point error on the + # exactly aligned vector being multiplied by a large non-infinite weight + dtype = xpx.default_dtype(xp) + if dtype == xp.float32: + pytest.skip("Align vectors near inf is numerically unstable in float32") + n = 100 + mats = [] + for i in range(6): + mats.append(Rotation.random(n, rng=10 + i).as_matrix()) + + for i in range(n): + # Get random pairs of 3-element vectors + a = xp.asarray(np.array([1 * mats[0][i][0], 2 * mats[1][i][0]]), dtype=dtype) + b = xp.asarray(np.array([3 * mats[2][i][0], 4 * mats[3][i][0]]), dtype=dtype) + + R, _ = Rotation.align_vectors(a, b, weights=[1e10, 1]) + R2, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 1]) + xp_assert_close(R.as_matrix(), R2.as_matrix(), atol=1e-4) + + for i in range(n): + # Get random triplets of 3-element vectors + a = xp.asarray(np.array([1*mats[0][i][0], 2*mats[1][i][0], 3*mats[2][i][0]]), + dtype=dtype) + b = xp.asarray(np.array([4*mats[3][i][0], 5*mats[4][i][0], 6*mats[5][i][0]]), + dtype=dtype) + + R, _ = Rotation.align_vectors(a, b, weights=[1e10, 2, 1]) + R2, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 2, 1]) + xp_assert_close(R.as_matrix(), R2.as_matrix(), atol=1e-4) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.as_matrix) +def test_align_vectors_parallel(xp): + atol = 1e-12 + a = xp.asarray([[1.0, 0, 0], [0, 1, 0]]) + b = xp.asarray([[0.0, 1, 0], [0, 1, 0]]) + m_expected = xp.asarray([[0.0, 1, 0], + [-1, 0, 0], + [0, 0, 1]]) + R, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 1]) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + R, _ = Rotation.align_vectors(a[0, ...], b[0, ...]) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + xp_assert_close(R.apply(b[0, ...]), a[0, ...], atol=atol) + + b = xp.asarray([[1, 0, 0], [1, 0, 0]]) + m_expected = xp.asarray([[1.0, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + R, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 1]) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + R, _ = Rotation.align_vectors(a[0, ...], b[0, ...]) + xp_assert_close(R.as_matrix(), m_expected, atol=atol) + xp_assert_close(R.apply(b[0, ...]), a[0, ...], atol=atol) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.magnitude, Rotation.apply, + Rotation.from_rotvec, Rotation.as_rotvec, Rotation.as_matrix) +def test_align_vectors_antiparallel(xp): + dtype = xpx.default_dtype(xp) + # Test exact 180 deg rotation + atol = 1e-12 if dtype == xp.float64 else 1e-7 + + as_to_test = np.array([[[1.0, 0, 0], [0, 1, 0]], + [[0, 1, 0], [1, 0, 0]], + [[0, 0, 1], [0, 1, 0]]]) + + bs_to_test = np.array([[-a[0], a[1]] for a in as_to_test]) + for a, b in zip(as_to_test, bs_to_test): + a, b = xp.asarray(a, dtype=dtype), xp.asarray(b, dtype=dtype) + R, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 1]) + xp_assert_close(R.magnitude(), xp.asarray(xp.pi)[()], atol=atol) + xp_assert_close(R.apply(b[0, ...]), a[0, ...], atol=atol) + + # Test exact rotations near 180 deg + Rs = Rotation.random(100, rng=0) + dRs = Rotation.from_rotvec(Rs.as_rotvec()*1e-4) # scale down to small angle + a = [[ 1, 0, 0], [0, 1, 0]] + b = [[-1, 0, 0], [0, 1, 0]] + as_to_test = [] + for dR in dRs: + as_to_test.append(np.array([dR.apply(a[0]), a[1]])) + + # GPU computations may be less accurate. See e.g. + # https://github.com/jax-ml/jax/issues/18934 and + # https://docs.pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html + # We currently have no unified way to check which device is being used. Cupy will + # always run on the GPU regardless of SCIPY_DEVICE, hence the explicit check for + # cupy. Note that the current implementation lets other frameworks, e.g. numpy, run + # on the CPU regardless of SCIPY_DEVICE but with increased GPU tolerances. + if xp_device_type(xp.asarray(0)) == "cuda": + atol = 1e-7 + + for a in as_to_test: + a, b = xp.asarray(a), xp.asarray(b) + R, _ = Rotation.align_vectors(a, b, weights=[xp.inf, 1]) + R2, _ = Rotation.align_vectors(a, b, weights=[1e10, 1]) + xp_assert_close(R.as_matrix(), R2.as_matrix(), atol=atol) + + +@make_xp_test_case(Rotation.align_vectors, Rotation.apply) +def test_align_vectors_primary_only(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-5 + mats_a = Rotation.random(100, rng=0).as_matrix() + mats_b = Rotation.random(100, rng=1).as_matrix() + + for mat_a, mat_b in zip(mats_a, mats_b): + # Get random 3-element unit vectors + a = xp.asarray(mat_a[0], dtype=dtype) + b = xp.asarray(mat_b[0], dtype=dtype) + + # Compare to align_vectors with primary only + R, rssd = Rotation.align_vectors(a, b) + xp_assert_close(R.apply(b), a, atol=atol) + xp_assert_close(rssd, xp.asarray(0.0)[()], atol=atol) + + +def test_align_vectors_array_like(): + rng = np.random.default_rng(123) + c = Rotation.random(rng=rng) + b = rng.normal(size=(5, 3)) + a = c.apply(b) + + est_expected, rssd_expected = Rotation.align_vectors(a, b) + est, rssd = Rotation.align_vectors(a.tolist(), b.tolist()) + xp_assert_close(est_expected.as_quat(), est.as_quat()) + xp_assert_close(rssd, rssd_expected) + + +@make_xp_test_case(Rotation.apply, Rotation.align_vectors) +def test_align_vectors_mixed_dtypes(xp): + dtype = xpx.default_dtype(xp) + rng = np.random.default_rng(123) + c = rotation_to_xp(Rotation.random(rng=rng), xp) + b = xp.asarray(rng.normal(size=(5, 3)), dtype=dtype) + a = xp.asarray(c.apply(b), dtype=xp.float32) # Intentionally float32 + # Check that the dtype of the output is the result type of a and b + est, _ = Rotation.align_vectors(a, b) + xp_assert_close(est.as_quat(), c.as_quat()) + + +def test_repr_single_rotation(xp): + q = xp.asarray([0, 0, 0, 1]) + actual = repr(Rotation.from_quat(q)) + if is_numpy(xp): + expected = """\ +Rotation.from_matrix(array([[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]))""" + assert actual == expected + else: + assert actual.startswith("Rotation.from_matrix(") + + +def test_repr_rotation_sequence(xp): + q = xp.asarray([[0.0, 1, 0, 1], [0, 0, 1, 1]]) / math.sqrt(2) + actual = f"{Rotation.from_quat(q)!r}" + if is_numpy(xp): + expected = """\ +Rotation.from_matrix(array([[[ 0., 0., 1.], + [ 0., 1., 0.], + [-1., 0., 0.]], + + [[ 0., -1., 0.], + [ 1., 0., 0.], + [ 0., 0., 1.]]]))""" + def stripped(s: str) -> str: + # don't fail due to leading whitespace differences + return "\n".join(map(str.lstrip, s.splitlines())) + + assert stripped(actual) == stripped(expected) + else: + assert actual.startswith("Rotation.from_matrix(") + + +@make_xp_test_case(Slerp.__init__, Slerp.__call__) +def test_slerp(xp): + rnd = np.random.RandomState(0) + + key_rots = Rotation.from_quat(xp.asarray(rnd.uniform(size=(5, 4)))) + key_quats = key_rots.as_quat() + + key_times = [0, 1, 2, 3, 4] + interpolator = Slerp(key_times, key_rots) + assert isinstance(interpolator.times, type(xp.asarray(0))) + + times = [0, 0.5, 0.25, 1, 1.5, 2, 2.75, 3, 3.25, 3.60, 4] + interp_rots = interpolator(times) + interp_quats = interp_rots.as_quat() + + # Dot products are affected by sign of quaternions + mask = (interp_quats[:, -1] < 0)[:, None] + interp_quats = xp.where(mask, -interp_quats, interp_quats) + # Checking for quaternion equality, perform same operation + mask = (key_quats[:, -1] < 0)[:, None] + key_quats = xp.where(mask, -key_quats, key_quats) + + # Equality at keyframes, including both endpoints + xp_assert_close(interp_quats[0, ...], key_quats[0, ...]) + xp_assert_close(interp_quats[3, ...], key_quats[1, ...]) + xp_assert_close(interp_quats[5, ...], key_quats[2, ...]) + xp_assert_close(interp_quats[7, ...], key_quats[3, ...]) + xp_assert_close(interp_quats[10, ...], key_quats[4, ...]) + + # Constant angular velocity between keyframes. Check by equating + # cos(theta) between quaternion pairs with equal time difference. + cos_theta1 = xp.sum(interp_quats[0, ...] * interp_quats[2, ...]) + cos_theta2 = xp.sum(interp_quats[2, ...] * interp_quats[1, ...]) + xp_assert_close(cos_theta1, cos_theta2) + + cos_theta4 = xp.sum(interp_quats[3, ...] * interp_quats[4, ...]) + cos_theta5 = xp.sum(interp_quats[4, ...] * interp_quats[5, ...]) + xp_assert_close(cos_theta4, cos_theta5) + + # theta1: 0 -> 0.25, theta3 : 0.5 -> 1 + # Use double angle formula for double the time difference + cos_theta3 = xp.sum(interp_quats[1, ...] * interp_quats[3, ...]) + xp_assert_close(cos_theta3, 2 * (cos_theta1**2) - 1) + + # Miscellaneous checks + assert_equal(len(interp_rots), len(times)) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_rot_is_rotation(xp): + with pytest.raises(TypeError, match="must be a `Rotation` instance"): + r = xp.asarray([[1,2,3,4], + [0,0,0,1]]) + t = xp.asarray([0, 1]) + Slerp(t, r) + + +SLERP_EXCEPTION_MESSAGE = "must be a sequence of at least 2 rotations" + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_single_rot(xp): + r = Rotation.from_quat(xp.asarray([[1.0, 2, 3, 4]])) + with pytest.raises(ValueError, match=SLERP_EXCEPTION_MESSAGE): + Slerp([1], r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_rot_len0(xp): + r = Rotation.random() + r = Rotation.from_quat(xp.asarray(r.as_quat())) + with pytest.raises(ValueError, match=SLERP_EXCEPTION_MESSAGE): + Slerp([], r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_rot_len1(xp): + r = Rotation.random(1) + r = Rotation.from_quat(xp.asarray(r.as_quat())) + with pytest.raises(ValueError, match=SLERP_EXCEPTION_MESSAGE): + Slerp([1], r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_tensor_rot(xp): + r = Rotation.from_quat(xp.ones((2, 2, 4))) + with pytest.raises(ValueError, match="Rotations with more than 1 leading"): + Slerp([1, 2], r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_time_dim_mismatch(xp): + with pytest.raises(ValueError, + match="times to be specified in a 1 dimensional array"): + rnd = np.random.RandomState(0) + r = Rotation.from_quat(xp.asarray(rnd.uniform(size=(2, 4)))) + t = xp.asarray([[1], + [2]]) + Slerp(t, r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_num_rotations_mismatch(xp): + with pytest.raises(ValueError, match="number of rotations to be equal to " + "number of timestamps"): + rnd = np.random.RandomState(0) + r = Rotation.from_quat(xp.asarray(rnd.uniform(size=(5, 4)))) + t = xp.arange(7) + Slerp(t, r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_equal_times(xp): + rnd = np.random.RandomState(0) + q = xp.asarray(rnd.uniform(size=(5, 4))) + r = Rotation.from_quat(q) + t = [0, 1, 2, 2, 4] + if is_lazy_array(q): + s = Slerp(t, r) + assert xp.all(xp.isnan(s.times)) + else: + with pytest.raises(ValueError, match="strictly increasing order"): + Slerp(t, r) + + +@make_xp_test_case(Slerp.__init__) +def test_slerp_decreasing_times(xp): + rnd = np.random.RandomState(0) + q = xp.asarray(rnd.uniform(size=(5, 4))) + r = Rotation.from_quat(q) + t = [0, 1, 3, 2, 4] + if is_lazy_array(q): + s = Slerp(t, r) + assert xp.all(xp.isnan(s.times)) + else: + with pytest.raises(ValueError, match="strictly increasing order"): + Slerp(t, r) + + +@make_xp_test_case(Slerp.__init__, Slerp.__call__) +def test_slerp_call_time_dim_mismatch(xp): + rnd = np.random.RandomState(0) + r = Rotation.from_quat(xp.asarray(rnd.uniform(size=(5, 4)))) + t = xp.arange(5) + s = Slerp(t, r) + + with pytest.raises(ValueError, + match="`times` must be at most 1-dimensional."): + interp_times = xp.asarray([[3.5], + [4.2]]) + s(interp_times) + + +@make_xp_test_case(Slerp.__init__, Slerp.__call__) +def test_slerp_call_time_out_of_range(xp): + rnd = np.random.RandomState(0) + r = Rotation.from_quat(xp.asarray(rnd.uniform(size=(5, 4)))) + t = xp.arange(5) + 1 + s = Slerp(t, r) + + times_low = xp.asarray([0, 1, 2]) + times_high = xp.asarray([1, 2, 6]) + if is_lazy_array(times_low): + q = s(times_low).as_quat() + in_range = xp.logical_and(times_low >= xp.min(t), times_low <= xp.max(t)) + assert xp.all(xp.isnan(q[~in_range, ...])) + assert xp.all(~xp.isnan(q[in_range, ...])) + q = s(times_high).as_quat() + in_range = xp.logical_and(times_high >= xp.min(t), times_high <= xp.max(t)) + assert xp.all(xp.isnan(q[~in_range, ...])) + assert xp.all(~xp.isnan(q[in_range, ...])) + else: + with pytest.raises(ValueError, match="times must be within the range"): + s(times_low) + with pytest.raises(ValueError, match="times must be within the range"): + s(times_high) + + +@make_xp_test_case(Slerp.__init__, Slerp.__call__, Rotation.from_euler, Rotation.inv, + Rotation.magnitude) +def test_slerp_call_scalar_time(xp): + dtype = xpx.default_dtype(xp) + atol = 1e-16 if dtype == xp.float64 else 1e-7 + r = Rotation.from_euler('X', xp.asarray([[0], [80]]), degrees=True) + s = Slerp([0, 1], r) + + r_interpolated = s(0.25) + r_interpolated_expected = Rotation.from_euler('X', xp.asarray(20), degrees=True) + + delta = r_interpolated * r_interpolated_expected.inv() + + xp_assert_close(delta.magnitude(), xp.asarray(0.0)[()], atol=atol) + + +@make_xp_test_case(Rotation.__mul__) +def test_multiplication(xp): + r1 = Rotation.from_quat(xp.asarray([0, 0, 0, 1])) + r2 = Rotation.from_quat(xp.asarray([0, 0, 0, 1])) + r3 = r1 * r2 + xp_assert_close(r3.as_quat(), xp.asarray([0.0, 0, 0, 1])) + + # Check that multiplication with other types fails + with pytest.raises(TypeError, match="unsupported operand type"): + r1 * 2 + # Check that __mul__ returns NotImplemented so that other types can implement + # __rmul__. See https://github.com/scipy/scipy/issues/21541 + assert r1.__mul__(1) is NotImplemented + + +@make_xp_test_case(Rotation.__mul__) +def test_multiplication_nd(xp): + # multiple dimensions + rng = np.random.default_rng(0) + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 3, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 3, 4)))) + r3 = r1 * r2 + assert r3.as_quat().shape == (2, 3, 4) + + # same shape len, different dimensions + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(1, 3, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 1, 4)))) + r3 = r1 * r2 + assert r3.as_quat().shape == (2, 3, 4) + + # different shape len, different dimensions + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(3, 1, 4, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 4, 4)))) + r3 = r1 * r2 + assert r3.as_quat().shape == (3, 2, 4, 4) + + # transition between 2D and 3D with 2D rotation as first argument. This needs to + # choose the xp_backend even though r1's backend is cython + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 2, 4)))) + r3 = r1 * r2 + assert r3.as_quat().shape == (2, 2, 4) + + +@make_xp_test_case(Rotation.__mul__) +def test_multiplication_errors(xp): + rng = np.random.default_rng(0) + r1 = Rotation.from_quat(xp.asarray(rng.normal(size=(2, 4)))) + r2 = Rotation.from_quat(xp.asarray(rng.normal(size=(1, 4, 4)))) + with pytest.raises(ValueError, match="Cannot broadcast"): + r1 * r2 + + +@make_xp_test_case(Rotation.__mul__) +def test_multiplication_stability(xp): + qs = rotation_to_xp(Rotation.random(50, rng=0), xp) + rs = rotation_to_xp(Rotation.random(1000, rng=1), xp) + expected = xp.ones(len(rs)) + for r in qs: + rs = rs * r * rs + xp_assert_close(xp_vector_norm(rs.as_quat(), axis=1), expected) + + +@make_xp_test_case(Rotation.inv, Rotation.__pow__, Rotation.inv, Rotation.magnitude, + Rotation.from_rotvec, Rotation.as_rotvec) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_pow(xp, ndim: int): + dtype = xpx.default_dtype(xp) + atol = 1e-14 if dtype == xp.float64 else 1e-6 + rng = np.random.default_rng(0) + batch_shape = (ndim,) * (ndim - 1) + quat = rng.normal(size=batch_shape + (4,)) + p = Rotation.from_quat(xp.asarray(quat)) + p_inv = p.inv() + # Test the short-cuts and other integers + for n in [-5, -2, -1, 0, 1, 2, 5]: + # Test accuracy + q = p ** n + q_identity = xp.asarray([0., 0, 0, 1]) + # Regression test for gh-24436 + assert isinstance(q._quat, type(q_identity)) + r = Rotation.from_quat(xp.tile(q_identity, batch_shape + (1,))) + for _ in range(abs(n)): + if n > 0: + r = r * p + else: + r = r * p_inv + ang = (q * r.inv()).magnitude() + assert xp.all(ang < atol) + + # Test shape preservation + r = Rotation.from_quat(xp.tile(q_identity, batch_shape + (1,))) + assert (r**n).as_quat().shape == batch_shape + (4,) + + # Large angle fractional + for n in [-1.5, -0.5, -0.0, 0.0, 0.5, 1.5]: + q = p ** n + r = Rotation.from_rotvec(n * p.as_rotvec()) + xp_assert_close(q.as_quat(), r.as_quat(), atol=atol) + + # Array exponent + n = [-5, -2, -1.5, -1, -0.5, -0.0, 0, 0.0, 0.5, 1.0, 1.5, 2] + for exponent in n: + r = p ** exponent + r_array = p ** xp.asarray([exponent]) # Test with 1D array + xp_assert_close(r.as_quat(), r_array.as_quat()) + r_array = p ** xp.asarray(exponent) # Test with scalar + xp_assert_close(r.as_quat(), r_array.as_quat()) + + # Small angle + rotvec = xp.zeros(batch_shape + (3,)) + rotvec = xpx.at(rotvec)[..., 0].set(1e-12) + p = Rotation.from_rotvec(rotvec) + n = 3 + q = p ** n + r = Rotation.from_rotvec(n * p.as_rotvec()) + xp_assert_close(q.as_quat(), r.as_quat(), atol=atol) + + # Array exponent + q = p ** xp.asarray([n]) # Test with 1D array + r = Rotation.from_rotvec(n * p.as_rotvec()) + xp_assert_close(q.as_quat(), r.as_quat(), atol=atol) + q = p ** xp.asarray(n) # Test with scalar + r = Rotation.from_rotvec(n * p.as_rotvec()) + xp_assert_close(q.as_quat(), r.as_quat(), atol=atol) + + +@make_xp_test_case(Rotation.__pow__) +def test_pow_errors(xp): + p = rotation_to_xp(Rotation.random(rng=0), xp) + with pytest.raises(NotImplementedError, match='modulus not supported'): + pow(p, 1, 1) + with pytest.raises(ValueError, match="Array exponent must be a scalar"): + p ** xp.asarray([1, 2]) + with pytest.raises(ValueError, match="Array exponent must be a scalar"): + p ** xp.asarray([[1], [2]]) + + +def test_rotation_within_numpy_array(): + # TODO: Do we want to support this for all Array API frameworks? + single = Rotation.random(rng=0) + multiple = Rotation.random(2, rng=1) + + array = np.array(single) + assert_equal(array.shape, ()) + + array = np.array(multiple) + assert_equal(array.shape, (2,)) + xp_assert_close(array[0].as_matrix(), multiple[0].as_matrix()) + xp_assert_close(array[1].as_matrix(), multiple[1].as_matrix()) + + array = np.array([single]) + assert_equal(array.shape, (1,)) + assert_equal(array[0], single) + + array = np.array([multiple]) + assert_equal(array.shape, (1, 2)) + xp_assert_close(array[0, 0].as_matrix(), multiple[0].as_matrix()) + xp_assert_close(array[0, 1].as_matrix(), multiple[1].as_matrix()) + + array = np.array([single, multiple], dtype=object) + assert_equal(array.shape, (2,)) + assert_equal(array[0], single) + assert_equal(array[1], multiple) + + array = np.array([multiple, multiple, multiple]) + assert_equal(array.shape, (3, 2)) + + +@make_xp_test_case(Rotation.as_matrix) +@pytest.mark.skip_xp_backends("array_api_strict", + reason="array API doesn't support pickling") +def test_pickling(xp): + r = Rotation.from_quat(xp.asarray([0, 0, math.sin(np.pi/4), math.cos(np.pi/4)])) + pkl = pickle.dumps(r) + unpickled = pickle.loads(pkl) + xp_assert_close(r.as_matrix(), unpickled.as_matrix(), atol=1e-15) + + +@make_xp_test_case(Rotation.as_matrix) +@pytest.mark.skip_xp_backends("array_api_strict", + reason="array API doesn't support deepcopy") +def test_deepcopy(xp): + r = Rotation.from_quat(xp.asarray([0, 0, math.sin(np.pi/4), math.cos(np.pi/4)])) + r1 = copy.deepcopy(r) + xp_assert_close(r.as_matrix(), r1.as_matrix(), atol=1e-15) + + +def test_as_euler_contiguous(): + # The Array API does not specify contiguous arrays, so we can only check for NumPy + r = Rotation.from_quat([0, 0, 0, 1]) + e1 = r.as_euler('xyz') # extrinsic euler rotation + e2 = r.as_euler('XYZ') # intrinsic + assert e1.flags['C_CONTIGUOUS'] is True + assert e2.flags['C_CONTIGUOUS'] is True + assert all(i >= 0 for i in e1.strides) + assert all(i >= 0 for i in e2.strides) + + +@make_xp_test_case(Rotation.concatenate) +def test_concatenate(xp): + rotation = rotation_to_xp(Rotation.random(10, rng=0), xp) + sizes = [1, 2, 3, 1, 3] + starts = [0] + list(np.cumsum(sizes)) + split = [rotation[i:i + n] for i, n in zip(starts, sizes)] + result = Rotation.concatenate(split) + xp_assert_equal(rotation.as_quat(), result.as_quat()) + + # Test Rotation input for multiple rotations + result = Rotation.concatenate(rotation) + xp_assert_equal(rotation.as_quat(), result.as_quat()) + + # Test that a copy is returned + assert rotation is not result + + # Test Rotation input for single rotations + rng = np.random.default_rng(0) + quat = xp.asarray(rng.normal(size=(5, 2, 4))) + rotation = Rotation.from_quat(quat) + r1 = Rotation.from_quat(quat[:3, ...]) + r2 = Rotation.from_quat(quat[3:, ...]) + result = Rotation.concatenate([r1, r2]) + xp_assert_equal(rotation.as_quat(), result.as_quat()) + + +@make_xp_test_case(Rotation.concatenate) +def test_concatenate_wrong_type(xp): + with pytest.raises(TypeError, match='Rotation objects only'): + rot = Rotation(xp.asarray(Rotation.identity().as_quat())) + Rotation.concatenate([rot, 1, None]) + + +@make_xp_test_case(Rotation.concatenate) +def test_concatenate_wrong_shape(xp): + r1 = Rotation.from_quat(xp.ones((5, 2, 4))) + r2 = Rotation.from_quat(xp.ones((1, 4))) + # Frameworks throw inconsistent error types on concat failures + with pytest.raises((ValueError, RuntimeError, TypeError)): + Rotation.concatenate([r1, r2]) + + +# Regression test for gh-16663 +@make_xp_test_case() +def test_len_and_bool(xp): + rotation_multi_one = Rotation(xp.asarray([[0, 0, 0, 1]])) + rotation_multi = Rotation(xp.asarray([[0, 0, 0, 1], [0, 0, 0, 1]])) + rotation_single = Rotation(xp.asarray([0, 0, 0, 1])) + + assert len(rotation_multi_one) == 1 + assert len(rotation_multi) == 2 + with pytest.raises(TypeError, match="Single rotation has no len()."): + len(rotation_single) + + rotation_batched = Rotation.from_quat(xp.ones((3, 2, 4))) + assert len(rotation_batched) == 3 + + # Rotation should always be truthy. See gh-16663 + assert rotation_multi_one + assert rotation_multi + assert rotation_single + + +@make_xp_test_case(Rotation.from_davenport) +def test_from_davenport_single_rotation(xp): + axis = xp.asarray([0, 0, 1]) + quat = Rotation.from_davenport(axis, 'extrinsic', 90, + degrees=True).as_quat() + expected_quat = xp.asarray([0.0, 0, 1, 1]) / math.sqrt(2) + xp_assert_close(quat, expected_quat) + + +@make_xp_test_case(Rotation.from_rotvec, Rotation.from_davenport) +def test_from_davenport_one_or_two_axes(xp): + ez = xp.asarray([0.0, 0, 1]) + ey = xp.asarray([0.0, 1, 0]) + + # Single rotation, single axis, axes.shape == (3, ) + rot = Rotation.from_rotvec(ez * xp.pi/4) + rot_dav = Rotation.from_davenport(ez, 'e', xp.pi/4) + xp_assert_close(rot.as_quat(canonical=True), rot_dav.as_quat(canonical=True)) + + # Single rotation, single axis, axes.shape == (1, 3), angles.shape == (1, ) + # -> Still single rotation + axes = xp.reshape(ez, (1, 3)) # Torch can't create tensors from xp.asarray([ez]) + rot = Rotation.from_rotvec(ez * xp.pi/4) + rot_dav = Rotation.from_davenport(axes, 'e', [xp.pi/4]) + xp_assert_close(rot.as_quat(canonical=True), rot_dav.as_quat(canonical=True)) + + # Single rotation, two axes, axes.shape == (2, 3) + axes = xp.stack([ez, ey], axis=0) + rot = Rotation.from_rotvec(axes * xp.asarray([[xp.pi/4], [xp.pi/6]])) + rot = rot[0] * rot[1] + axes_dav = xp.stack([ey, ez], axis=0) + rot_dav = Rotation.from_davenport(axes_dav, 'e', [xp.pi/6, xp.pi/4]) + xp_assert_close(rot.as_quat(canonical=True), rot_dav.as_quat(canonical=True)) + + # Two rotations, single axis, axes.shape == (3, ) + axes = xp.stack([ez, ez], axis=0) + rot = Rotation.from_rotvec(axes * xp.asarray([[xp.pi/6], [xp.pi/4]])) + axes_dav = xp.reshape(ez, (1, 3)) + rot_dav = Rotation.from_davenport(axes_dav, 'e', [[xp.pi/6], [xp.pi/4]]) + xp_assert_close(rot.as_quat(canonical=True), rot_dav.as_quat(canonical=True)) + + +@make_xp_test_case(Rotation.from_davenport) +@pytest.mark.parametrize("ndim", range(1, 4)) +def test_from_davenport_shapes(xp, ndim: int): + # The shape rules for ND rotations are as follows: + # axes.shape[-2] must be angles.shape[-1] + # Resulting shape is np.broadcast_shapes(axes.shape[:-2], angles.shape[:-1]) + (4,) + rng = np.random.default_rng(0) + batch_shape = (ndim,) * (ndim - 1) + # Create random, orthogonal axes + r = Rotation.from_quat(xp.asarray(rng.normal(size=(4,)))) + axes = r.as_matrix() + # axes = (3,) + angles = xp.asarray(rng.normal(size=batch_shape + (1,))) + rot = Rotation.from_davenport(axes[0, ...], 'e', angles) + assert rot.as_quat().shape == batch_shape + (4,) + # axes = (1, 3) + angles = xp.asarray(rng.normal(size=batch_shape + (1,))) + rot = Rotation.from_davenport(axes[0, None, ...], 'e', angles) + assert rot.as_quat().shape == batch_shape + (4,) + # axes = (2, 3) + angles = xp.asarray(rng.normal(size=batch_shape + (2,))) + rot = Rotation.from_davenport(axes[:2, ...], 'e', angles) + assert rot.as_quat().shape == batch_shape + (4,) + + # axes = (...,3, 3) + r = Rotation.from_quat(xp.asarray(rng.normal(size=batch_shape + (4,)))) + axes = r.as_matrix() + angles = xp.asarray(rng.normal(size=batch_shape + (3,))) + rot = Rotation.from_davenport(axes, 'e', angles) + assert rot.as_quat().shape == batch_shape + (4,) + + +@make_xp_test_case(Rotation.from_davenport) +def test_from_davenport_broadcast(xp): + rng = np.random.default_rng(0) + # Create random, orthogonal axes + r = Rotation.from_quat(xp.asarray(rng.normal(size=(4, 9, 1, 4)))) + axes = r.as_matrix() + angles = xp.asarray(rng.normal(size=(1, 4, 3))) + rot = Rotation.from_davenport(axes, 'e', angles) + # (4, 9, 1, 3) + (3,) axes, (1, 4, 3) angles -> (4, 9, 4) + (4,) for quaternion + assert rot.as_quat().shape == (4, 9, 4, 4) + + +@make_xp_test_case(Rotation.from_davenport) +def test_from_davenport_invalid_input(xp): + ez = [0, 0, 1] + ey = [0, 1, 0] + ezy = [0, 1, 1] + # We can only raise in non-lazy frameworks. + axes = xp.asarray([ez, ezy]) + if is_lazy_array(axes): + q = Rotation.from_davenport(axes, 'e', [0, 0]).as_quat() + assert xp.all(xp.isnan(q)) + else: + with pytest.raises(ValueError, match="must be orthogonal"): + Rotation.from_davenport(axes, 'e', [0, 0]) + axes = xp.asarray([ez, ey, ezy]) + if is_lazy_array(axes): + q = Rotation.from_davenport(axes, 'e', [0, 0, 0]).as_quat() + assert xp.all(xp.isnan(q)) + else: + with pytest.raises(ValueError, match="must be orthogonal"): + Rotation.from_davenport(axes, 'e', [0, 0, 0]) + with pytest.raises(ValueError, match="order should be"): + Rotation.from_davenport(xp.asarray([ez]), 'xyz', [0]) + with pytest.raises(ValueError, match="Expected `angles`"): + Rotation.from_davenport(xp.asarray([ez, ey, ez]), 'e', [0, 1, 2, 3]) + with pytest.raises(ValueError, match="Expected `angles`"): # Too many angles + Rotation.from_davenport(xp.asarray(ez), 'e', [0, 1]) + with pytest.raises(ValueError, match="Expected `angles`"): # Too few angles + Rotation.from_davenport(xp.asarray([ez, ey, ez]), 'e', [0, 1]) + + +def test_from_davenport_array_like(): + rng = np.random.default_rng(123) + # Single rotation + e1 = np.array([1, 0, 0]) + e2 = np.array([0, 1, 0]) + e3 = np.array([0, 0, 1]) + r_expected = Rotation.random(rng=rng) + angles = r_expected.as_davenport([e1, e2, e3], 'e') + r = Rotation.from_davenport([e1, e2, e3], 'e', angles.tolist()) + assert r_expected.approx_equal(r, atol=1e-12) + + # Multiple rotations + r_expected = Rotation.random(2, rng=rng) + angles = r_expected.as_davenport([e1, e2, e3], 'e') + r = Rotation.from_davenport([e1, e2, e3], 'e', angles.tolist()) + assert np.all(r_expected.approx_equal(r, atol=1e-12)) + + +@make_xp_test_case(Rotation.from_davenport, Rotation.as_davenport) +def test_as_davenport(xp): + dtype = xpx.default_dtype(xp) + rnd = np.random.RandomState(0) + n = 100 + angles = np.empty((n, 3)) + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles_middle = rnd.uniform(low=0, high=np.pi, size=(n,)) + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + lambdas = rnd.uniform(low=0, high=np.pi, size=(20,)) + + e1 = xp.asarray([1.0, 0, 0]) + e2 = xp.asarray([0.0, 1, 0]) + + for lamb in lambdas: + e3 = xp.asarray(Rotation.from_rotvec(lamb*e2).apply(e1)) + ax_lamb = xp.stack([e1, e2, e3], axis=0) + angles[:, 1] = angles_middle - lamb + for order in ['extrinsic', 'intrinsic']: + ax = ax_lamb if order == "intrinsic" else xp.flip(ax_lamb, axis=0) + rot = Rotation.from_davenport(ax, order, xp.asarray(angles, dtype=dtype)) + angles_dav = rot.as_davenport(ax, order) + xp_assert_close(angles_dav, xp.asarray(angles, dtype=dtype)) + + +@make_xp_test_case(Rotation.from_davenport, Rotation.as_davenport) +def test_as_davenport_nd(xp): + rng = np.random.default_rng(0) + r = Rotation.from_quat(xp.asarray(rng.normal(size=(4, 9, 1, 4)))) + axes = r.as_matrix() # Get orthogonal axes + angles = xp.asarray(rng.uniform(low=-np.pi, high=np.pi, size=(4, 9, 1, 3))) + angles = xpx.at(angles)[..., 1].set(angles[..., 1] / 2) + + for order in ['extrinsic', 'intrinsic']: + if order == "intrinsic": + axes = xp.flip(axes, axis=-2) + rot = Rotation.from_davenport(axes, order, angles) + angles_dav = rot.as_davenport(axes, order) + xp_assert_close(angles_dav, angles) + + +@make_xp_test_case(Rotation.from_davenport, Rotation.as_davenport, Rotation.as_matrix) +@pytest.mark.parametrize("suppress_warnings", (False, True)) +def test_as_davenport_degenerate(xp, suppress_warnings): + dtype = xpx.default_dtype(xp) + atol = 1e-12 if dtype == xp.float64 else 1e-6 + # Since we cannot check for angle equality, we check for rotation matrix + # equality + rnd = np.random.RandomState(0) + n = 5 + angles = np.empty((n, 3)) + + # symmetric sequences + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles_middle = [rnd.choice([0, np.pi]) for i in range(n)] + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + lambdas = rnd.uniform(low=0, high=np.pi, size=(5,)) + + e1 = xp.asarray([1.0, 0, 0]) + e2 = xp.asarray([0.0, 1, 0]) + + for lamb in lambdas: + e3 = xp.asarray(Rotation.from_rotvec(lamb*e2).apply(e1)) + ax_lamb = xp.stack([e1, e2, e3], axis=0) + angles[:, 1] = angles_middle - lamb + for order in ['extrinsic', 'intrinsic']: + ax = ax_lamb if order == 'intrinsic' else xp.flip(ax_lamb, axis=0) + rot = Rotation.from_davenport(ax, order, xp.asarray(angles, dtype=dtype)) + with maybe_warn_gimbal_lock(not suppress_warnings, xp): + angles_dav = rot.as_davenport( + ax, + order, + suppress_warnings=suppress_warnings + ) + mat_expected = rot.as_matrix() + rot_estimated = Rotation.from_davenport(ax, order, angles_dav) + mat_estimated = rot_estimated.as_matrix() + xp_assert_close(mat_expected, mat_estimated, atol=atol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.from_davenport) +def test_compare_from_davenport_from_euler(xp): + dtype = xpx.default_dtype(xp) + rnd = np.random.RandomState(0) + n = 100 + angles = np.empty((n, 3)) + + # symmetric sequences + rtol = 1e-12 if dtype == xp.float64 else 1e-5 + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles[:, 1] = rnd.uniform(low=0, high=np.pi, size=(n,)) + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles = xp.asarray(angles, dtype=dtype) + for order in ['extrinsic', 'intrinsic']: + for seq_tuple in permutations('xyz'): + seq = ''.join([seq_tuple[0], seq_tuple[1], seq_tuple[0]]) + ax = xp.asarray([basis_vec(i) for i in seq], dtype=dtype) + if order == 'intrinsic': + seq = seq.upper() + eul = Rotation.from_euler(seq, angles) + dav = Rotation.from_davenport(ax, order, angles) + xp_assert_close(eul.as_quat(canonical=True), dav.as_quat(canonical=True), + rtol=rtol) + + # asymmetric sequences + angles = xpx.at(angles)[:, 1].subtract(np.pi / 2) + for order in ['extrinsic', 'intrinsic']: + for seq_tuple in permutations('xyz'): + seq = ''.join(seq_tuple) + ax = xp.asarray([basis_vec(i) for i in seq], dtype=dtype) + if order == 'intrinsic': + seq = seq.upper() + eul = Rotation.from_euler(seq, angles) + dav = Rotation.from_davenport(ax, order, angles) + xp_assert_close(eul.as_quat(), dav.as_quat(), rtol=rtol) + + +@make_xp_test_case(Rotation.from_euler, Rotation.as_euler, Rotation.as_davenport) +def test_compare_as_davenport_as_euler(xp): + rnd = np.random.RandomState(0) + n = 100 + angles = np.empty((n, 3)) + + # symmetric sequences + angles[:, 0] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + angles[:, 1] = rnd.uniform(low=0, high=np.pi, size=(n,)) + angles[:, 2] = rnd.uniform(low=-np.pi, high=np.pi, size=(n,)) + for order in ['extrinsic', 'intrinsic']: + for seq_tuple in permutations('xyz'): + seq = ''.join([seq_tuple[0], seq_tuple[1], seq_tuple[0]]) + ax = [basis_vec(i) for i in seq] + if order == 'intrinsic': + seq = seq.upper() + rot = Rotation.from_euler(seq, xp.asarray(angles)) + eul = rot.as_euler(seq) + dav = rot.as_davenport(xp.asarray(ax), order) + xp_assert_close(eul, dav, rtol=1e-12) + + # asymmetric sequences + angles[:, 1] -= np.pi / 2 + for order in ['extrinsic', 'intrinsic']: + for seq_tuple in permutations('xyz'): + seq = ''.join(seq_tuple) + ax = [basis_vec(i) for i in seq] + if order == 'intrinsic': + seq = seq.upper() + rot = Rotation.from_euler(seq, xp.asarray(angles)) + eul = rot.as_euler(seq) + dav = rot.as_davenport(xp.asarray(ax), order) + xp_assert_close(eul, dav, rtol=1e-12) + + +@make_xp_test_case(Rotation.from_matrix, Rotation.from_euler, Rotation.from_rotvec, + Rotation.from_davenport, Rotation.from_mrp) +def test_zero_rotation_construction(xp): + r = Rotation.random(num=0) + assert len(r) == 0 + + r_ide = Rotation.identity(num=0) + assert len(r_ide) == 0 + + r_get = Rotation.random(num=3)[[]] + assert len(r_get) == 0 + + r_quat = Rotation.from_quat(xp.zeros((0, 4))) + assert len(r_quat) == 0 + + r_matrix = Rotation.from_matrix(xp.zeros((0, 3, 3))) + assert len(r_matrix) == 0 + + r_euler = Rotation.from_euler("xyz", xp.zeros((0, 3))) + assert len(r_euler) == 0 + + r_vec = Rotation.from_rotvec(xp.zeros((0, 3))) + assert len(r_vec) == 0 + + r_dav = Rotation.from_davenport(xp.eye(3), "extrinsic", xp.zeros((0, 3))) + assert len(r_dav) == 0 + + r_mrp = Rotation.from_mrp(xp.zeros((0, 3))) + assert len(r_mrp) == 0 + + +@make_xp_test_case(Rotation.as_matrix, Rotation.as_euler, Rotation.as_rotvec, + Rotation.as_mrp, Rotation.as_davenport) +def test_zero_rotation_representation(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + assert r.as_quat().shape == (0, 4) + assert r.as_matrix().shape == (0, 3, 3) + assert r.as_euler("xyz").shape == (0, 3) + assert r.as_rotvec().shape == (0, 3) + assert r.as_mrp().shape == (0, 3) + assert r.as_davenport(xp.eye(3), "extrinsic").shape == (0, 3) + + +@make_xp_test_case(Rotation.apply) +def test_zero_rotation_array_rotation(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + + v = xp.asarray([1, 2, 3]) + v_rotated = r.apply(v) + assert v_rotated.shape == (0, 3) + + v0 = xp.zeros((0, 3)) + v0_rot = r.apply(v0) + assert v0_rot.shape == (0, 3) + + v2 = xp.ones((2, 3)) + with pytest.raises( + ValueError, match="Cannot broadcast"): + r.apply(v2) + + +@make_xp_test_case(Rotation.__mul__) +def test_zero_rotation_multiplication(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + + r_single = Rotation.from_quat(xp.asarray([0.0, 0, 0, 1])) + r_mult_left = r * r_single + assert len(r_mult_left) == 0 + + r_mult_right = r_single * r + assert len(r_mult_right) == 0 + + r0 = Rotation.from_quat(xp.zeros((0, 4))) + r_mult = r * r0 + assert len(r_mult) == 0 + + r2 = rotation_to_xp(Rotation.random(2), xp) + with pytest.raises(ValueError, match="Cannot broadcast"): + r0 * r2 + + with pytest.raises(ValueError, match="Cannot broadcast"): + r2 * r0 + + +@make_xp_test_case(Rotation.concatenate) +def test_zero_rotation_concatentation(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + + r0 = Rotation.concatenate([r, r]) + assert len(r0) == 0 + + r1 = Rotation.from_quat(xp.asarray([0.0, 0, 0, 1])) + r1 = r.concatenate([r1, r]) + assert len(r1) == 1 + + r3 = rotation_to_xp(Rotation.random(3), xp) + r3 = r.concatenate([r3, r]) + assert len(r3) == 3 + + r4 = rotation_to_xp(Rotation.random(4), xp) + r4 = r.concatenate([r, r4]) + r4 = r.concatenate([r, r4]) + assert len(r4) == 4 + + +@make_xp_test_case(Rotation.__pow__) +def test_zero_rotation_power(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + for pp in [-1.5, -1, 0, 1, 1.5]: + pow0 = r**pp + assert len(pow0) == 0 + + +@make_xp_test_case(Rotation.inv) +def test_zero_rotation_inverse(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + r_inv = r.inv() + assert len(r_inv) == 0 + + +@make_xp_test_case(Rotation.magnitude) +def test_zero_rotation_magnitude(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + magnitude = r.magnitude() + assert magnitude.shape == (0,) + + +@make_xp_test_case(Rotation.mean) +def test_zero_rotation_mean(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + with pytest.raises(ValueError, match="Mean of an empty rotation set is undefined."): + r.mean() + + +@make_xp_test_case(Rotation.approx_equal) +def test_zero_rotation_approx_equal(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + r0 = Rotation.from_quat(xp.zeros((0, 4))) + assert r.approx_equal(r0).shape == (0,) + r1 = Rotation.from_quat(xp.asarray([0.0, 0, 0, 1])) + assert r.approx_equal(r1).shape == (0,) + r2 = rotation_to_xp(Rotation.random(), xp) + assert r2.approx_equal(r).shape == (0,) + + approx_msg = "Expected broadcastable shapes in both rotations" + r3 = rotation_to_xp(Rotation.random(2), xp) + with pytest.raises(ValueError, match=approx_msg): + r.approx_equal(r3) + + with pytest.raises(ValueError, match=approx_msg): + r3.approx_equal(r) + + +@pytest.mark.skip_xp_backends("jax.numpy", + reason="JAX out-of-bounds indexing deviates from numpy") +@pytest.mark.skip_xp_backends("dask.array", reason="zero-length arrays have nan-shapes") +def test_zero_rotation_get_set(xp): + r = Rotation.from_quat(xp.zeros((0, 4))) + + r_get = r[xp.asarray([], dtype=xp.bool)] + assert len(r_get) == 0 + + r_slice = r[:0] + assert len(r_slice) == 0 + + with pytest.raises(IndexError): + r[xp.asarray([0])] + + with pytest.raises(IndexError): + r[xp.asarray([True])] + + with pytest.raises(IndexError): + r[0] = Rotation.from_quat(xp.asarray([0, 0, 0, 1])) + + +@make_xp_test_case(Rotation.__getitem__) +def test_boolean_indexes(xp): + r = rotation_to_xp(Rotation.random(3), xp) + + r0 = r[xp.asarray([False, False, False])] + assert len(r0) == 0 + + r1 = r[xp.asarray([False, True, False])] + assert len(r1) == 1 + + r3 = r[xp.asarray([True, True, True])] + assert len(r3) == 3 + + # Multiple dimensions + r = Rotation.from_quat(xp.ones((3, 2, 4))) + r4 = r[xp.asarray([True, False, False])] + assert len(r4) == 1 + assert r4.as_quat().shape == (1, 2, 4) + + with pytest.raises(IndexError): + r[xp.asarray([True, True])] + + +@make_xp_test_case(Rotation.__iter__) +def test_rotation_iter(xp): + r = rotation_to_xp(Rotation.random(3), xp) + for i, r_i in enumerate(r): + assert isinstance(r_i, Rotation) + xp_assert_equal(r_i.as_quat(), r[i].as_quat()) + if i > len(r): + raise RuntimeError("Iteration exceeded length of rotations") + + +@pytest.mark.parametrize("ndim", range(1, 5)) +def test_rotation_shape(xp, ndim: int): + shape = tuple(range(2, 2 + ndim)[:ndim - 1]) + quat = xp.ones(shape + (4,)) + r = Rotation.from_quat(quat) + assert r.shape == shape, f"Got {r.shape}, expected {shape}" + + +def test_non_writeable(): + q = np.array([0, 0, 0, 1.0]) + q.flags.writeable = False + Rotation.from_quat(q) # Regression test against gh-24354, should not raise diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14a458158a6680f0d3ba4da981bbeca93714c917 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/_version.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/_version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b831ddcd35069339e84d0cbe328e1ca172640e29 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/_version.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_model_pb2.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_model_pb2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd8e34a4e4bd023c733ab37881b64b93e3de51be Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_model_pb2.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_pb2.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_pb2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f57afe788a81f9b11d86b0051a311aa7288e798c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/sentencepiece/__pycache__/sentencepiece_pb2.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3-2.0.7.dist-info/licenses/LICENSE.txt b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3-2.0.7.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..e6183d0276b26c5b87aecccf8d0d5bcd7b1148d4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3-2.0.7.dist-info/licenses/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2008-2020 Andrey Petrov and contributors. + +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/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21769809799590fddd6d078295c0ddc8dfca6cd3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_base_connection.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_base_connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e13885c89ec8fc7d7ff8c3e4c86d82df32faa4b7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_base_connection.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_collections.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_collections.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de6224747f45383b974b3a82e2b6b86d27583914 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_collections.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_request_methods.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_request_methods.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..821790f0d35e41c43f433f64e15bb1f54c354c45 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_request_methods.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_version.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ed45e3e865680b955a7f2f2bf9ae2f04c52e657 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/_version.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connection.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c798f3e397822ead5210f40718dbedd08a518bc1 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connection.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connectionpool.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connectionpool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dedf9ab259196854d785aca5fdbbf830a34f6140 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/connectionpool.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/exceptions.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0f2c6b3368516a9eb8385401e95cad891ae86f0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/exceptions.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/fields.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/fields.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27ec69fb2cc1fbb91bbac403846013a46a773d59 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/fields.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/filepost.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/filepost.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e7f0086a2ae8741ff25a90c3e6ba7d8f983be7e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/filepost.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/poolmanager.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/poolmanager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5d641d51184d0ad41fb6959b2ed7cb16ac29bde Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/poolmanager.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/response.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/response.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..875e7c2aeabac928fc3f8878f3dbfa97cf70b073 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/__pycache__/response.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5f08c5a29cd5632aa8c216e173c974d77e40a0f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91fa06f816bc69cad587ac127aea69a675d88e0c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/securetransport.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/securetransport.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41ca609e0a2046ba50e9785b86412f2203a6657e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/securetransport.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/socks.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/socks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21b1746fe9b0667401765a85b4f8bfc4a161cf3d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/__pycache__/socks.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..005db4ed74a82af321c82b889ba82119b5cdea36 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48402ec2361d47eb4f41b587573b1ce3e566b057 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/bindings.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/low_level.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/low_level.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc937a7819aac1827c675b111908c130cfe048ce Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/__pycache__/low_level.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/bindings.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/bindings.py new file mode 100644 index 0000000000000000000000000000000000000000..3e4cd466eab6e551b3947819ddf271738b30e064 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/bindings.py @@ -0,0 +1,430 @@ +# type: ignore + +""" +This module uses ctypes to bind a whole bunch of functions and constants from +SecureTransport. The goal here is to provide the low-level API to +SecureTransport. These are essentially the C-level functions and constants, and +they're pretty gross to work with. + +This code is a bastardised version of the code found in Will Bond's oscrypto +library. An enormous debt is owed to him for blazing this trail for us. For +that reason, this code should be considered to be covered both by urllib3's +license and by oscrypto's: + + Copyright (c) 2015-2016 Will Bond + + 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. +""" + +from __future__ import annotations + +import platform +from ctypes import ( + CDLL, + CFUNCTYPE, + POINTER, + c_bool, + c_byte, + c_char_p, + c_int32, + c_long, + c_size_t, + c_uint32, + c_ulong, + c_void_p, +) +from ctypes.util import find_library + +if platform.system() != "Darwin": + raise ImportError("Only macOS is supported") + +version = platform.mac_ver()[0] +version_info = tuple(map(int, version.split("."))) +if version_info < (10, 8): + raise OSError( + f"Only OS X 10.8 and newer are supported, not {version_info[0]}.{version_info[1]}" + ) + + +def load_cdll(name: str, macos10_16_path: str) -> CDLL: + """Loads a CDLL by name, falling back to known path on 10.16+""" + try: + # Big Sur is technically 11 but we use 10.16 due to the Big Sur + # beta being labeled as 10.16. + path: str | None + if version_info >= (10, 16): + path = macos10_16_path + else: + path = find_library(name) + if not path: + raise OSError # Caught and reraised as 'ImportError' + return CDLL(path, use_errno=True) + except OSError: + raise ImportError(f"The library {name} failed to load") from None + + +Security = load_cdll( + "Security", "/System/Library/Frameworks/Security.framework/Security" +) +CoreFoundation = load_cdll( + "CoreFoundation", + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", +) + + +Boolean = c_bool +CFIndex = c_long +CFStringEncoding = c_uint32 +CFData = c_void_p +CFString = c_void_p +CFArray = c_void_p +CFMutableArray = c_void_p +CFDictionary = c_void_p +CFError = c_void_p +CFType = c_void_p +CFTypeID = c_ulong + +CFTypeRef = POINTER(CFType) +CFAllocatorRef = c_void_p + +OSStatus = c_int32 + +CFDataRef = POINTER(CFData) +CFStringRef = POINTER(CFString) +CFArrayRef = POINTER(CFArray) +CFMutableArrayRef = POINTER(CFMutableArray) +CFDictionaryRef = POINTER(CFDictionary) +CFArrayCallBacks = c_void_p +CFDictionaryKeyCallBacks = c_void_p +CFDictionaryValueCallBacks = c_void_p + +SecCertificateRef = POINTER(c_void_p) +SecExternalFormat = c_uint32 +SecExternalItemType = c_uint32 +SecIdentityRef = POINTER(c_void_p) +SecItemImportExportFlags = c_uint32 +SecItemImportExportKeyParameters = c_void_p +SecKeychainRef = POINTER(c_void_p) +SSLProtocol = c_uint32 +SSLCipherSuite = c_uint32 +SSLContextRef = POINTER(c_void_p) +SecTrustRef = POINTER(c_void_p) +SSLConnectionRef = c_uint32 +SecTrustResultType = c_uint32 +SecTrustOptionFlags = c_uint32 +SSLProtocolSide = c_uint32 +SSLConnectionType = c_uint32 +SSLSessionOption = c_uint32 + + +try: + Security.SecItemImport.argtypes = [ + CFDataRef, + CFStringRef, + POINTER(SecExternalFormat), + POINTER(SecExternalItemType), + SecItemImportExportFlags, + POINTER(SecItemImportExportKeyParameters), + SecKeychainRef, + POINTER(CFArrayRef), + ] + Security.SecItemImport.restype = OSStatus + + Security.SecCertificateGetTypeID.argtypes = [] + Security.SecCertificateGetTypeID.restype = CFTypeID + + Security.SecIdentityGetTypeID.argtypes = [] + Security.SecIdentityGetTypeID.restype = CFTypeID + + Security.SecKeyGetTypeID.argtypes = [] + Security.SecKeyGetTypeID.restype = CFTypeID + + Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef] + Security.SecCertificateCreateWithData.restype = SecCertificateRef + + Security.SecCertificateCopyData.argtypes = [SecCertificateRef] + Security.SecCertificateCopyData.restype = CFDataRef + + Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] + Security.SecCopyErrorMessageString.restype = CFStringRef + + Security.SecIdentityCreateWithCertificate.argtypes = [ + CFTypeRef, + SecCertificateRef, + POINTER(SecIdentityRef), + ] + Security.SecIdentityCreateWithCertificate.restype = OSStatus + + Security.SecKeychainCreate.argtypes = [ + c_char_p, + c_uint32, + c_void_p, + Boolean, + c_void_p, + POINTER(SecKeychainRef), + ] + Security.SecKeychainCreate.restype = OSStatus + + Security.SecKeychainDelete.argtypes = [SecKeychainRef] + Security.SecKeychainDelete.restype = OSStatus + + Security.SecPKCS12Import.argtypes = [ + CFDataRef, + CFDictionaryRef, + POINTER(CFArrayRef), + ] + Security.SecPKCS12Import.restype = OSStatus + + SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, c_void_p, POINTER(c_size_t)) + SSLWriteFunc = CFUNCTYPE( + OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t) + ) + + Security.SSLSetIOFuncs.argtypes = [SSLContextRef, SSLReadFunc, SSLWriteFunc] + Security.SSLSetIOFuncs.restype = OSStatus + + Security.SSLSetPeerID.argtypes = [SSLContextRef, c_char_p, c_size_t] + Security.SSLSetPeerID.restype = OSStatus + + Security.SSLSetCertificate.argtypes = [SSLContextRef, CFArrayRef] + Security.SSLSetCertificate.restype = OSStatus + + Security.SSLSetCertificateAuthorities.argtypes = [SSLContextRef, CFTypeRef, Boolean] + Security.SSLSetCertificateAuthorities.restype = OSStatus + + Security.SSLSetConnection.argtypes = [SSLContextRef, SSLConnectionRef] + Security.SSLSetConnection.restype = OSStatus + + Security.SSLSetPeerDomainName.argtypes = [SSLContextRef, c_char_p, c_size_t] + Security.SSLSetPeerDomainName.restype = OSStatus + + Security.SSLHandshake.argtypes = [SSLContextRef] + Security.SSLHandshake.restype = OSStatus + + Security.SSLRead.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] + Security.SSLRead.restype = OSStatus + + Security.SSLWrite.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] + Security.SSLWrite.restype = OSStatus + + Security.SSLClose.argtypes = [SSLContextRef] + Security.SSLClose.restype = OSStatus + + Security.SSLGetNumberSupportedCiphers.argtypes = [SSLContextRef, POINTER(c_size_t)] + Security.SSLGetNumberSupportedCiphers.restype = OSStatus + + Security.SSLGetSupportedCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + POINTER(c_size_t), + ] + Security.SSLGetSupportedCiphers.restype = OSStatus + + Security.SSLSetEnabledCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + c_size_t, + ] + Security.SSLSetEnabledCiphers.restype = OSStatus + + Security.SSLGetNumberEnabledCiphers.argtype = [SSLContextRef, POINTER(c_size_t)] + Security.SSLGetNumberEnabledCiphers.restype = OSStatus + + Security.SSLGetEnabledCiphers.argtypes = [ + SSLContextRef, + POINTER(SSLCipherSuite), + POINTER(c_size_t), + ] + Security.SSLGetEnabledCiphers.restype = OSStatus + + Security.SSLGetNegotiatedCipher.argtypes = [SSLContextRef, POINTER(SSLCipherSuite)] + Security.SSLGetNegotiatedCipher.restype = OSStatus + + Security.SSLGetNegotiatedProtocolVersion.argtypes = [ + SSLContextRef, + POINTER(SSLProtocol), + ] + Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus + + Security.SSLCopyPeerTrust.argtypes = [SSLContextRef, POINTER(SecTrustRef)] + Security.SSLCopyPeerTrust.restype = OSStatus + + Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef] + Security.SecTrustSetAnchorCertificates.restype = OSStatus + + Security.SecTrustSetAnchorCertificatesOnly.argstypes = [SecTrustRef, Boolean] + Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus + + Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)] + Security.SecTrustEvaluate.restype = OSStatus + + Security.SecTrustGetCertificateCount.argtypes = [SecTrustRef] + Security.SecTrustGetCertificateCount.restype = CFIndex + + Security.SecTrustGetCertificateAtIndex.argtypes = [SecTrustRef, CFIndex] + Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef + + Security.SSLCreateContext.argtypes = [ + CFAllocatorRef, + SSLProtocolSide, + SSLConnectionType, + ] + Security.SSLCreateContext.restype = SSLContextRef + + Security.SSLSetSessionOption.argtypes = [SSLContextRef, SSLSessionOption, Boolean] + Security.SSLSetSessionOption.restype = OSStatus + + Security.SSLSetProtocolVersionMin.argtypes = [SSLContextRef, SSLProtocol] + Security.SSLSetProtocolVersionMin.restype = OSStatus + + Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol] + Security.SSLSetProtocolVersionMax.restype = OSStatus + + try: + Security.SSLSetALPNProtocols.argtypes = [SSLContextRef, CFArrayRef] + Security.SSLSetALPNProtocols.restype = OSStatus + except AttributeError: + # Supported only in 10.12+ + pass + + Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] + Security.SecCopyErrorMessageString.restype = CFStringRef + + Security.SSLReadFunc = SSLReadFunc + Security.SSLWriteFunc = SSLWriteFunc + Security.SSLContextRef = SSLContextRef + Security.SSLProtocol = SSLProtocol + Security.SSLCipherSuite = SSLCipherSuite + Security.SecIdentityRef = SecIdentityRef + Security.SecKeychainRef = SecKeychainRef + Security.SecTrustRef = SecTrustRef + Security.SecTrustResultType = SecTrustResultType + Security.SecExternalFormat = SecExternalFormat + Security.OSStatus = OSStatus + + Security.kSecImportExportPassphrase = CFStringRef.in_dll( + Security, "kSecImportExportPassphrase" + ) + Security.kSecImportItemIdentity = CFStringRef.in_dll( + Security, "kSecImportItemIdentity" + ) + + # CoreFoundation time! + CoreFoundation.CFRetain.argtypes = [CFTypeRef] + CoreFoundation.CFRetain.restype = CFTypeRef + + CoreFoundation.CFRelease.argtypes = [CFTypeRef] + CoreFoundation.CFRelease.restype = None + + CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef] + CoreFoundation.CFGetTypeID.restype = CFTypeID + + CoreFoundation.CFStringCreateWithCString.argtypes = [ + CFAllocatorRef, + c_char_p, + CFStringEncoding, + ] + CoreFoundation.CFStringCreateWithCString.restype = CFStringRef + + CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding] + CoreFoundation.CFStringGetCStringPtr.restype = c_char_p + + CoreFoundation.CFStringGetCString.argtypes = [ + CFStringRef, + c_char_p, + CFIndex, + CFStringEncoding, + ] + CoreFoundation.CFStringGetCString.restype = c_bool + + CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex] + CoreFoundation.CFDataCreate.restype = CFDataRef + + CoreFoundation.CFDataGetLength.argtypes = [CFDataRef] + CoreFoundation.CFDataGetLength.restype = CFIndex + + CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef] + CoreFoundation.CFDataGetBytePtr.restype = c_void_p + + CoreFoundation.CFDictionaryCreate.argtypes = [ + CFAllocatorRef, + POINTER(CFTypeRef), + POINTER(CFTypeRef), + CFIndex, + CFDictionaryKeyCallBacks, + CFDictionaryValueCallBacks, + ] + CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef + + CoreFoundation.CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef] + CoreFoundation.CFDictionaryGetValue.restype = CFTypeRef + + CoreFoundation.CFArrayCreate.argtypes = [ + CFAllocatorRef, + POINTER(CFTypeRef), + CFIndex, + CFArrayCallBacks, + ] + CoreFoundation.CFArrayCreate.restype = CFArrayRef + + CoreFoundation.CFArrayCreateMutable.argtypes = [ + CFAllocatorRef, + CFIndex, + CFArrayCallBacks, + ] + CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef + + CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p] + CoreFoundation.CFArrayAppendValue.restype = None + + CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef] + CoreFoundation.CFArrayGetCount.restype = CFIndex + + CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex] + CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p + + CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll( + CoreFoundation, "kCFAllocatorDefault" + ) + CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll( + CoreFoundation, "kCFTypeArrayCallBacks" + ) + CoreFoundation.kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll( + CoreFoundation, "kCFTypeDictionaryKeyCallBacks" + ) + CoreFoundation.kCFTypeDictionaryValueCallBacks = c_void_p.in_dll( + CoreFoundation, "kCFTypeDictionaryValueCallBacks" + ) + + CoreFoundation.CFTypeRef = CFTypeRef + CoreFoundation.CFArrayRef = CFArrayRef + CoreFoundation.CFStringRef = CFStringRef + CoreFoundation.CFDictionaryRef = CFDictionaryRef + +except AttributeError: + raise ImportError("Error initializing ctypes") from None + + +class CFConst: + """ + A class object that acts as essentially a namespace for CoreFoundation + constants. + """ + + kCFStringEncodingUTF8 = CFStringEncoding(0x08000100) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/low_level.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/low_level.py new file mode 100644 index 0000000000000000000000000000000000000000..e23569972c7a541774366a01eea05e066b19c406 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/_securetransport/low_level.py @@ -0,0 +1,474 @@ +""" +Low-level helpers for the SecureTransport bindings. + +These are Python functions that are not directly related to the high-level APIs +but are necessary to get them to work. They include a whole bunch of low-level +CoreFoundation messing about and memory management. The concerns in this module +are almost entirely about trying to avoid memory leaks and providing +appropriate and useful assistance to the higher-level code. +""" +from __future__ import annotations + +import base64 +import ctypes +import itertools +import os +import re +import ssl +import struct +import tempfile +import typing + +from .bindings import ( # type: ignore[attr-defined] + CFArray, + CFConst, + CFData, + CFDictionary, + CFMutableArray, + CFString, + CFTypeRef, + CoreFoundation, + SecKeychainRef, + Security, +) + +# This regular expression is used to grab PEM data out of a PEM bundle. +_PEM_CERTS_RE = re.compile( + b"-----BEGIN CERTIFICATE-----\n(.*?)\n-----END CERTIFICATE-----", re.DOTALL +) + + +def _cf_data_from_bytes(bytestring: bytes) -> CFData: + """ + Given a bytestring, create a CFData object from it. This CFData object must + be CFReleased by the caller. + """ + return CoreFoundation.CFDataCreate( + CoreFoundation.kCFAllocatorDefault, bytestring, len(bytestring) + ) + + +def _cf_dictionary_from_tuples( + tuples: list[tuple[typing.Any, typing.Any]] +) -> CFDictionary: + """ + Given a list of Python tuples, create an associated CFDictionary. + """ + dictionary_size = len(tuples) + + # We need to get the dictionary keys and values out in the same order. + keys = (t[0] for t in tuples) + values = (t[1] for t in tuples) + cf_keys = (CoreFoundation.CFTypeRef * dictionary_size)(*keys) + cf_values = (CoreFoundation.CFTypeRef * dictionary_size)(*values) + + return CoreFoundation.CFDictionaryCreate( + CoreFoundation.kCFAllocatorDefault, + cf_keys, + cf_values, + dictionary_size, + CoreFoundation.kCFTypeDictionaryKeyCallBacks, + CoreFoundation.kCFTypeDictionaryValueCallBacks, + ) + + +def _cfstr(py_bstr: bytes) -> CFString: + """ + Given a Python binary data, create a CFString. + The string must be CFReleased by the caller. + """ + c_str = ctypes.c_char_p(py_bstr) + cf_str = CoreFoundation.CFStringCreateWithCString( + CoreFoundation.kCFAllocatorDefault, + c_str, + CFConst.kCFStringEncodingUTF8, + ) + return cf_str + + +def _create_cfstring_array(lst: list[bytes]) -> CFMutableArray: + """ + Given a list of Python binary data, create an associated CFMutableArray. + The array must be CFReleased by the caller. + + Raises an ssl.SSLError on failure. + """ + cf_arr = None + try: + cf_arr = CoreFoundation.CFArrayCreateMutable( + CoreFoundation.kCFAllocatorDefault, + 0, + ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), + ) + if not cf_arr: + raise MemoryError("Unable to allocate memory!") + for item in lst: + cf_str = _cfstr(item) + if not cf_str: + raise MemoryError("Unable to allocate memory!") + try: + CoreFoundation.CFArrayAppendValue(cf_arr, cf_str) + finally: + CoreFoundation.CFRelease(cf_str) + except BaseException as e: + if cf_arr: + CoreFoundation.CFRelease(cf_arr) + raise ssl.SSLError(f"Unable to allocate array: {e}") from None + return cf_arr + + +def _cf_string_to_unicode(value: CFString) -> str | None: + """ + Creates a Unicode string from a CFString object. Used entirely for error + reporting. + + Yes, it annoys me quite a lot that this function is this complex. + """ + value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p)) + + string = CoreFoundation.CFStringGetCStringPtr( + value_as_void_p, CFConst.kCFStringEncodingUTF8 + ) + if string is None: + buffer = ctypes.create_string_buffer(1024) + result = CoreFoundation.CFStringGetCString( + value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8 + ) + if not result: + raise OSError("Error copying C string from CFStringRef") + string = buffer.value + if string is not None: + string = string.decode("utf-8") + return string # type: ignore[no-any-return] + + +def _assert_no_error( + error: int, exception_class: type[BaseException] | None = None +) -> None: + """ + Checks the return code and throws an exception if there is an error to + report + """ + if error == 0: + return + + cf_error_string = Security.SecCopyErrorMessageString(error, None) + output = _cf_string_to_unicode(cf_error_string) + CoreFoundation.CFRelease(cf_error_string) + + if output is None or output == "": + output = f"OSStatus {error}" + + if exception_class is None: + exception_class = ssl.SSLError + + raise exception_class(output) + + +def _cert_array_from_pem(pem_bundle: bytes) -> CFArray: + """ + Given a bundle of certs in PEM format, turns them into a CFArray of certs + that can be used to validate a cert chain. + """ + # Normalize the PEM bundle's line endings. + pem_bundle = pem_bundle.replace(b"\r\n", b"\n") + + der_certs = [ + base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle) + ] + if not der_certs: + raise ssl.SSLError("No root certificates specified") + + cert_array = CoreFoundation.CFArrayCreateMutable( + CoreFoundation.kCFAllocatorDefault, + 0, + ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), + ) + if not cert_array: + raise ssl.SSLError("Unable to allocate memory!") + + try: + for der_bytes in der_certs: + certdata = _cf_data_from_bytes(der_bytes) + if not certdata: + raise ssl.SSLError("Unable to allocate memory!") + cert = Security.SecCertificateCreateWithData( + CoreFoundation.kCFAllocatorDefault, certdata + ) + CoreFoundation.CFRelease(certdata) + if not cert: + raise ssl.SSLError("Unable to build cert object!") + + CoreFoundation.CFArrayAppendValue(cert_array, cert) + CoreFoundation.CFRelease(cert) + except Exception: + # We need to free the array before the exception bubbles further. + # We only want to do that if an error occurs: otherwise, the caller + # should free. + CoreFoundation.CFRelease(cert_array) + raise + + return cert_array + + +def _is_cert(item: CFTypeRef) -> bool: + """ + Returns True if a given CFTypeRef is a certificate. + """ + expected = Security.SecCertificateGetTypeID() + return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return] + + +def _is_identity(item: CFTypeRef) -> bool: + """ + Returns True if a given CFTypeRef is an identity. + """ + expected = Security.SecIdentityGetTypeID() + return CoreFoundation.CFGetTypeID(item) == expected # type: ignore[no-any-return] + + +def _temporary_keychain() -> tuple[SecKeychainRef, str]: + """ + This function creates a temporary Mac keychain that we can use to work with + credentials. This keychain uses a one-time password and a temporary file to + store the data. We expect to have one keychain per socket. The returned + SecKeychainRef must be freed by the caller, including calling + SecKeychainDelete. + + Returns a tuple of the SecKeychainRef and the path to the temporary + directory that contains it. + """ + # Unfortunately, SecKeychainCreate requires a path to a keychain. This + # means we cannot use mkstemp to use a generic temporary file. Instead, + # we're going to create a temporary directory and a filename to use there. + # This filename will be 8 random bytes expanded into base64. We also need + # some random bytes to password-protect the keychain we're creating, so we + # ask for 40 random bytes. + random_bytes = os.urandom(40) + filename = base64.b16encode(random_bytes[:8]).decode("utf-8") + password = base64.b16encode(random_bytes[8:]) # Must be valid UTF-8 + tempdirectory = tempfile.mkdtemp() + + keychain_path = os.path.join(tempdirectory, filename).encode("utf-8") + + # We now want to create the keychain itself. + keychain = Security.SecKeychainRef() + status = Security.SecKeychainCreate( + keychain_path, len(password), password, False, None, ctypes.byref(keychain) + ) + _assert_no_error(status) + + # Having created the keychain, we want to pass it off to the caller. + return keychain, tempdirectory + + +def _load_items_from_file( + keychain: SecKeychainRef, path: str +) -> tuple[list[CFTypeRef], list[CFTypeRef]]: + """ + Given a single file, loads all the trust objects from it into arrays and + the keychain. + Returns a tuple of lists: the first list is a list of identities, the + second a list of certs. + """ + certificates = [] + identities = [] + result_array = None + + with open(path, "rb") as f: + raw_filedata = f.read() + + try: + filedata = CoreFoundation.CFDataCreate( + CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata) + ) + result_array = CoreFoundation.CFArrayRef() + result = Security.SecItemImport( + filedata, # cert data + None, # Filename, leaving it out for now + None, # What the type of the file is, we don't care + None, # what's in the file, we don't care + 0, # import flags + None, # key params, can include passphrase in the future + keychain, # The keychain to insert into + ctypes.byref(result_array), # Results + ) + _assert_no_error(result) + + # A CFArray is not very useful to us as an intermediary + # representation, so we are going to extract the objects we want + # and then free the array. We don't need to keep hold of keys: the + # keychain already has them! + result_count = CoreFoundation.CFArrayGetCount(result_array) + for index in range(result_count): + item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index) + item = ctypes.cast(item, CoreFoundation.CFTypeRef) + + if _is_cert(item): + CoreFoundation.CFRetain(item) + certificates.append(item) + elif _is_identity(item): + CoreFoundation.CFRetain(item) + identities.append(item) + finally: + if result_array: + CoreFoundation.CFRelease(result_array) + + CoreFoundation.CFRelease(filedata) + + return (identities, certificates) + + +def _load_client_cert_chain(keychain: SecKeychainRef, *paths: str | None) -> CFArray: + """ + Load certificates and maybe keys from a number of files. Has the end goal + of returning a CFArray containing one SecIdentityRef, and then zero or more + SecCertificateRef objects, suitable for use as a client certificate trust + chain. + """ + # Ok, the strategy. + # + # This relies on knowing that macOS will not give you a SecIdentityRef + # unless you have imported a key into a keychain. This is a somewhat + # artificial limitation of macOS (for example, it doesn't necessarily + # affect iOS), but there is nothing inside Security.framework that lets you + # get a SecIdentityRef without having a key in a keychain. + # + # So the policy here is we take all the files and iterate them in order. + # Each one will use SecItemImport to have one or more objects loaded from + # it. We will also point at a keychain that macOS can use to work with the + # private key. + # + # Once we have all the objects, we'll check what we actually have. If we + # already have a SecIdentityRef in hand, fab: we'll use that. Otherwise, + # we'll take the first certificate (which we assume to be our leaf) and + # ask the keychain to give us a SecIdentityRef with that cert's associated + # key. + # + # We'll then return a CFArray containing the trust chain: one + # SecIdentityRef and then zero-or-more SecCertificateRef objects. The + # responsibility for freeing this CFArray will be with the caller. This + # CFArray must remain alive for the entire connection, so in practice it + # will be stored with a single SSLSocket, along with the reference to the + # keychain. + certificates = [] + identities = [] + + # Filter out bad paths. + filtered_paths = (path for path in paths if path) + + try: + for file_path in filtered_paths: + new_identities, new_certs = _load_items_from_file(keychain, file_path) + identities.extend(new_identities) + certificates.extend(new_certs) + + # Ok, we have everything. The question is: do we have an identity? If + # not, we want to grab one from the first cert we have. + if not identities: + new_identity = Security.SecIdentityRef() + status = Security.SecIdentityCreateWithCertificate( + keychain, certificates[0], ctypes.byref(new_identity) + ) + _assert_no_error(status) + identities.append(new_identity) + + # We now want to release the original certificate, as we no longer + # need it. + CoreFoundation.CFRelease(certificates.pop(0)) + + # We now need to build a new CFArray that holds the trust chain. + trust_chain = CoreFoundation.CFArrayCreateMutable( + CoreFoundation.kCFAllocatorDefault, + 0, + ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), + ) + for item in itertools.chain(identities, certificates): + # ArrayAppendValue does a CFRetain on the item. That's fine, + # because the finally block will release our other refs to them. + CoreFoundation.CFArrayAppendValue(trust_chain, item) + + return trust_chain + finally: + for obj in itertools.chain(identities, certificates): + CoreFoundation.CFRelease(obj) + + +TLS_PROTOCOL_VERSIONS = { + "SSLv2": (0, 2), + "SSLv3": (3, 0), + "TLSv1": (3, 1), + "TLSv1.1": (3, 2), + "TLSv1.2": (3, 3), +} + + +def _build_tls_unknown_ca_alert(version: str) -> bytes: + """ + Builds a TLS alert record for an unknown CA. + """ + ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version] + severity_fatal = 0x02 + description_unknown_ca = 0x30 + msg = struct.pack(">BB", severity_fatal, description_unknown_ca) + msg_len = len(msg) + record_type_alert = 0x15 + record = struct.pack(">BBBH", record_type_alert, ver_maj, ver_min, msg_len) + msg + return record + + +class SecurityConst: + """ + A class object that acts as essentially a namespace for Security constants. + """ + + kSSLSessionOptionBreakOnServerAuth = 0 + + kSSLProtocol2 = 1 + kSSLProtocol3 = 2 + kTLSProtocol1 = 4 + kTLSProtocol11 = 7 + kTLSProtocol12 = 8 + # SecureTransport does not support TLS 1.3 even if there's a constant for it + kTLSProtocol13 = 10 + kTLSProtocolMaxSupported = 999 + + kSSLClientSide = 1 + kSSLStreamType = 0 + + kSecFormatPEMSequence = 10 + + kSecTrustResultInvalid = 0 + kSecTrustResultProceed = 1 + # This gap is present on purpose: this was kSecTrustResultConfirm, which + # is deprecated. + kSecTrustResultDeny = 3 + kSecTrustResultUnspecified = 4 + kSecTrustResultRecoverableTrustFailure = 5 + kSecTrustResultFatalTrustFailure = 6 + kSecTrustResultOtherError = 7 + + errSSLProtocol = -9800 + errSSLWouldBlock = -9803 + errSSLClosedGraceful = -9805 + errSSLClosedNoNotify = -9816 + errSSLClosedAbort = -9806 + + errSSLXCertChainInvalid = -9807 + errSSLCrypto = -9809 + errSSLInternal = -9810 + errSSLCertExpired = -9814 + errSSLCertNotYetValid = -9815 + errSSLUnknownRootCert = -9812 + errSSLNoRootCert = -9813 + errSSLHostNameMismatch = -9843 + errSSLPeerHandshakeFail = -9824 + errSSLPeerUserCancelled = -9839 + errSSLWeakPeerEphemeralDHKey = -9850 + errSSLServerAuthCompleted = -9841 + errSSLRecordOverflow = -9847 + + errSecVerifyFailed = -67808 + errSecNoTrustSettings = -25263 + errSecItemNotFound = -25300 + errSecInvalidTrustSettings = -25262 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/pyopenssl.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/pyopenssl.py new file mode 100644 index 0000000000000000000000000000000000000000..74b35883bfdd214cb784215a0b83ff2c0f5f23c3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/pyopenssl.py @@ -0,0 +1,548 @@ +""" +Module for using pyOpenSSL as a TLS backend. This module was relevant before +the standard library ``ssl`` module supported SNI, but now that we've dropped +support for Python 2.7 all relevant Python versions support SNI so +**this module is no longer recommended**. + +This needs the following packages installed: + +* `pyOpenSSL`_ (tested with 16.0.0) +* `cryptography`_ (minimum 1.3.4, from pyopenssl) +* `idna`_ (minimum 2.0, from cryptography) + +However, pyOpenSSL depends on cryptography, which depends on idna, so while we +use all three directly here we end up having relatively few packages required. + +You can install them with the following command: + +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna + +To activate certificate checking, call +:func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code +before you begin making HTTP requests. This can be done in a ``sitecustomize`` +module, or at any other time before your application begins using ``urllib3``, +like this: + +.. code-block:: python + + try: + import urllib3.contrib.pyopenssl + urllib3.contrib.pyopenssl.inject_into_urllib3() + except ImportError: + pass + +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna +""" + +from __future__ import annotations + +import OpenSSL.SSL # type: ignore[import] +from cryptography import x509 + +try: + from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined] +except ImportError: + # UnsupportedExtension is gone in cryptography >= 2.1.0 + class UnsupportedExtension(Exception): # type: ignore[no-redef] + pass + + +import logging +import ssl +import typing +from io import BytesIO +from socket import socket as socket_cls +from socket import timeout + +from .. import util + +if typing.TYPE_CHECKING: + from OpenSSL.crypto import X509 # type: ignore[import] + + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +# Map from urllib3 to PyOpenSSL compatible parameter-values. +_openssl_versions = { + util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined] + ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, +} + +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD + +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): + _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD + + +_stdlib_to_openssl_verify = { + ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, + ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, +} +_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()} + +# The SSLvX values are the most likely to be missing in the future +# but we check them all just to be sure. +_OP_NO_SSLv2_OR_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0) | getattr( + OpenSSL.SSL, "OP_NO_SSLv3", 0 +) +_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0) +_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0) +_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0) +_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0) + +_openssl_to_ssl_minimum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1, + ssl.TLSVersion.TLSv1_3: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), + ssl.TLSVersion.MAXIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 + ), +} +_openssl_to_ssl_maximum_version: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: ( + _OP_NO_SSLv2_OR_SSLv3 + | _OP_NO_TLSv1 + | _OP_NO_TLSv1_1 + | _OP_NO_TLSv1_2 + | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1: ( + _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3 + ), + ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2_OR_SSLv3 | _OP_NO_TLSv1_3, + ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2_OR_SSLv3, + ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2_OR_SSLv3, +} + +# OpenSSL will only write 16K at a time +SSL_WRITE_BLOCKSIZE = 16384 + +orig_util_SSLContext = util.ssl_.SSLContext + + +log = logging.getLogger(__name__) + + +def inject_into_urllib3() -> None: + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." + + _validate_dependencies_met() + + util.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.ssl_.SSLContext = PyOpenSSLContext # type: ignore[assignment] + util.IS_PYOPENSSL = True + util.ssl_.IS_PYOPENSSL = True + + +def extract_from_urllib3() -> None: + "Undo monkey-patching by :func:`inject_into_urllib3`." + + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_PYOPENSSL = False + util.ssl_.IS_PYOPENSSL = False + + +def _validate_dependencies_met() -> None: + """ + Verifies that PyOpenSSL's package-level dependencies have been met. + Throws `ImportError` if they are not met. + """ + # Method added in `cryptography==1.1`; not available in older versions + from cryptography.x509.extensions import Extensions + + if getattr(Extensions, "get_extension_for_class", None) is None: + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) + + # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 + # attribute is only present on those versions. + from OpenSSL.crypto import X509 + + x509 = X509() + if getattr(x509, "_x509", None) is None: + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) + + +def _dnsname_to_stdlib(name: str) -> str | None: + """ + Converts a dNSName SubjectAlternativeName field to the form used by the + standard library on the given Python version. + + Cryptography produces a dNSName as a unicode string that was idna-decoded + from ASCII bytes. We need to idna-encode that string to get it back, and + then on Python 3 we also need to convert to unicode via UTF-8 (the stdlib + uses PyUnicode_FromStringAndSize on it, which decodes via UTF-8). + + If the name cannot be idna-encoded then we return None signalling that + the name given should be skipped. + """ + + def idna_encode(name: str) -> bytes | None: + """ + Borrowed wholesale from the Python Cryptography Project. It turns out + that we can't just safely call `idna.encode`: it can explode for + wildcard names. This avoids that problem. + """ + import idna + + try: + for prefix in ["*.", "."]: + if name.startswith(prefix): + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) + return idna.encode(name) + except idna.core.IDNAError: + return None + + # Don't send IPv6 addresses through the IDNA encoder. + if ":" in name: + return name + + encoded_name = idna_encode(name) + if encoded_name is None: + return None + return encoded_name.decode("utf-8") + + +def get_subj_alt_name(peer_cert: X509) -> list[tuple[str, str]]: + """ + Given an PyOpenSSL certificate, provides all the subject alternative names. + """ + cert = peer_cert.to_cryptography() + + # We want to find the SAN extension. Ask Cryptography to locate it (it's + # faster than looping in Python) + try: + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + except x509.ExtensionNotFound: + # No such extension, return the empty list. + return [] + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: + # A problem has been found with the quality of the certificate. Assume + # no SAN field is present. + log.warning( + "A problem was encountered with the certificate that prevented " + "urllib3 from finding the SubjectAlternativeName field. This can " + "affect certificate validation. The error was %s", + e, + ) + return [] + + # We want to return dNSName and iPAddress fields. We need to cast the IPs + # back to strings because the match_hostname function wants them as + # strings. + # Sadly the DNS names need to be idna encoded and then, on Python 3, UTF-8 + # decoded. This is pretty frustrating, but that's what the standard library + # does with certificates, and so we need to attempt to do the same. + # We also want to skip over names which cannot be idna encoded. + names = [ + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + if name is not None + ] + names.extend( + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) + ) + + return names + + +class WrappedSocket: + """API-compatibility wrapper for Python OpenSSL's Connection-class.""" + + def __init__( + self, + connection: OpenSSL.SSL.Connection, + socket: socket_cls, + suppress_ragged_eofs: bool = True, + ) -> None: + self.connection = connection + self.socket = socket + self.suppress_ragged_eofs = suppress_ragged_eofs + self._io_refs = 0 + self._closed = False + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, *args: typing.Any, **kwargs: typing.Any) -> bytes: + try: + data = self.connection.recv(*args, **kwargs) + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return b"" + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise timeout("The read operation timed out") from e + else: + return self.recv(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + else: + return data # type: ignore[no-any-return] + + def recv_into(self, *args: typing.Any, **kwargs: typing.Any) -> int: + try: + return self.connection.recv_into(*args, **kwargs) # type: ignore[no-any-return] + except OpenSSL.SSL.SysCallError as e: + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return 0 + else: + raise OSError(e.args[0], str(e)) from e + except OpenSSL.SSL.ZeroReturnError: + if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: + return 0 + else: + raise + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(self.socket, self.socket.gettimeout()): + raise timeout("The read operation timed out") from e + else: + return self.recv_into(*args, **kwargs) + + # TLS 1.3 post-handshake authentication + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"read error: {e!r}") from e + + def settimeout(self, timeout: float) -> None: + return self.socket.settimeout(timeout) + + def _send_until_done(self, data: bytes) -> int: + while True: + try: + return self.connection.send(data) # type: ignore[no-any-return] + except OpenSSL.SSL.WantWriteError as e: + if not util.wait_for_write(self.socket, self.socket.gettimeout()): + raise timeout() from e + continue + except OpenSSL.SSL.SysCallError as e: + raise OSError(e.args[0], str(e)) from e + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) + total_sent += sent + + def shutdown(self) -> None: + # FIXME rethrow compatible exceptions should we ever use this + self.connection.shutdown() + + def close(self) -> None: + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + try: + return self.connection.close() # type: ignore[no-any-return] + except OpenSSL.SSL.Error: + return + + def getpeercert( + self, binary_form: bool = False + ) -> dict[str, list[typing.Any]] | None: + x509 = self.connection.get_peer_certificate() + + if not x509: + return x509 # type: ignore[no-any-return] + + if binary_form: + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return] + + return { + "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item] + "subjectAltName": get_subj_alt_name(x509), + } + + def version(self) -> str: + return self.connection.get_protocol_version_name() # type: ignore[no-any-return] + + +WrappedSocket.makefile = socket_cls.makefile # type: ignore[attr-defined] + + +class PyOpenSSLContext: + """ + I am a wrapper class for the PyOpenSSL ``Context`` object. I am responsible + for translating the interface of the standard library ``SSLContext`` object + to calls into PyOpenSSL. + """ + + def __init__(self, protocol: int) -> None: + self.protocol = _openssl_versions[protocol] + self._ctx = OpenSSL.SSL.Context(self.protocol) + self._options = 0 + self.check_hostname = False + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + + @property + def options(self) -> int: + return self._options + + @options.setter + def options(self, value: int) -> None: + self._options = value + self._set_ctx_options() + + @property + def verify_mode(self) -> int: + return _openssl_to_stdlib_verify[self._ctx.get_verify_mode()] + + @verify_mode.setter + def verify_mode(self, value: ssl.VerifyMode) -> None: + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) + + def set_default_verify_paths(self) -> None: + self._ctx.set_default_verify_paths() + + def set_ciphers(self, ciphers: bytes | str) -> None: + if isinstance(ciphers, str): + ciphers = ciphers.encode("utf-8") + self._ctx.set_cipher_list(ciphers) + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + if cafile is not None: + cafile = cafile.encode("utf-8") # type: ignore[assignment] + if capath is not None: + capath = capath.encode("utf-8") # type: ignore[assignment] + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"unable to load trusted certificates: {e!r}") from e + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + try: + self._ctx.use_certificate_chain_file(certfile) + if password is not None: + if not isinstance(password, bytes): + password = password.encode("utf-8") # type: ignore[assignment] + self._ctx.set_passwd_cb(lambda *_: password) + self._ctx.use_privatekey_file(keyfile or certfile) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e + + def set_alpn_protocols(self, protocols: list[bytes | str]) -> None: + protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + return self._ctx.set_alpn_protos(protocols) # type: ignore[no-any-return] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + cnx = OpenSSL.SSL.Connection(self._ctx, sock) + + # If server_hostname is an IP, don't use it for SNI, per RFC6066 Section 3 + if server_hostname and not util.ssl_.is_ipaddress(server_hostname): + if isinstance(server_hostname, str): + server_hostname = server_hostname.encode("utf-8") + cnx.set_tlsext_host_name(server_hostname) + + cnx.set_connect_state() + + while True: + try: + cnx.do_handshake() + except OpenSSL.SSL.WantReadError as e: + if not util.wait_for_read(sock, sock.gettimeout()): + raise timeout("select timed out") from e + continue + except OpenSSL.SSL.Error as e: + raise ssl.SSLError(f"bad handshake: {e!r}") from e + break + + return WrappedSocket(cnx, sock) + + def _set_ctx_options(self) -> None: + self._ctx.set_options( + self._options + | _openssl_to_ssl_minimum_version[self._minimum_version] + | _openssl_to_ssl_maximum_version[self._maximum_version] + ) + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + self._set_ctx_options() + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version + self._set_ctx_options() + + +def _verify_callback( + cnx: OpenSSL.SSL.Connection, + x509: X509, + err_no: int, + err_depth: int, + return_code: int, +) -> bool: + return err_no == 0 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/securetransport.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/securetransport.py new file mode 100644 index 0000000000000000000000000000000000000000..11beb3dfefb7e92c2e37440bd2a29809657d5f47 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/securetransport.py @@ -0,0 +1,913 @@ +""" +SecureTranport support for urllib3 via ctypes. + +This makes platform-native TLS available to urllib3 users on macOS without the +use of a compiler. This is an important feature because the Python Package +Index is moving to become a TLSv1.2-or-higher server, and the default OpenSSL +that ships with macOS is not capable of doing TLSv1.2. The only way to resolve +this is to give macOS users an alternative solution to the problem, and that +solution is to use SecureTransport. + +We use ctypes here because this solution must not require a compiler. That's +because pip is not allowed to require a compiler either. + +This is not intended to be a seriously long-term solution to this problem. +The hope is that PEP 543 will eventually solve this issue for us, at which +point we can retire this contrib module. But in the short term, we need to +solve the impending tire fire that is Python on Mac without this kind of +contrib module. So...here we are. + +To use this module, simply import and inject it:: + + import urllib3.contrib.securetransport + urllib3.contrib.securetransport.inject_into_urllib3() + +Happy TLSing! + +This code is a bastardised version of the code found in Will Bond's oscrypto +library. An enormous debt is owed to him for blazing this trail for us. For +that reason, this code should be considered to be covered both by urllib3's +license and by oscrypto's: + +.. code-block:: + + Copyright (c) 2015-2016 Will Bond + + 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. +""" + +from __future__ import annotations + +import contextlib +import ctypes +import errno +import os.path +import shutil +import socket +import ssl +import struct +import threading +import typing +import warnings +import weakref +from socket import socket as socket_cls + +from .. import util +from ._securetransport.bindings import ( # type: ignore[attr-defined] + CoreFoundation, + Security, +) +from ._securetransport.low_level import ( + SecurityConst, + _assert_no_error, + _build_tls_unknown_ca_alert, + _cert_array_from_pem, + _create_cfstring_array, + _load_client_cert_chain, + _temporary_keychain, +) + +warnings.warn( + "'urllib3.contrib.securetransport' module is deprecated and will be removed " + "in urllib3 v2.1.0. Read more in this issue: " + "https://github.com/urllib3/urllib3/issues/2681", + category=DeprecationWarning, + stacklevel=2, +) + +if typing.TYPE_CHECKING: + from typing_extensions import Literal + +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] + +orig_util_SSLContext = util.ssl_.SSLContext + +# This dictionary is used by the read callback to obtain a handle to the +# calling wrapped socket. This is a pretty silly approach, but for now it'll +# do. I feel like I should be able to smuggle a handle to the wrapped socket +# directly in the SSLConnectionRef, but for now this approach will work I +# guess. +# +# We need to lock around this structure for inserts, but we don't do it for +# reads/writes in the callbacks. The reasoning here goes as follows: +# +# 1. It is not possible to call into the callbacks before the dictionary is +# populated, so once in the callback the id must be in the dictionary. +# 2. The callbacks don't mutate the dictionary, they only read from it, and +# so cannot conflict with any of the insertions. +# +# This is good: if we had to lock in the callbacks we'd drastically slow down +# the performance of this code. +_connection_refs: weakref.WeakValueDictionary[ + int, WrappedSocket +] = weakref.WeakValueDictionary() +_connection_ref_lock = threading.Lock() + +# Limit writes to 16kB. This is OpenSSL's limit, but we'll cargo-cult it over +# for no better reason than we need *a* limit, and this one is right there. +SSL_WRITE_BLOCKSIZE = 16384 + +# Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of +# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version. +# TLSv1 to 1.2 are supported on macOS 10.8+ +_protocol_to_min_max = { + util.ssl_.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12), # type: ignore[attr-defined] + util.ssl_.PROTOCOL_TLS_CLIENT: ( # type: ignore[attr-defined] + SecurityConst.kTLSProtocol1, + SecurityConst.kTLSProtocol12, + ), +} + +if hasattr(ssl, "PROTOCOL_SSLv2"): + _protocol_to_min_max[ssl.PROTOCOL_SSLv2] = ( + SecurityConst.kSSLProtocol2, + SecurityConst.kSSLProtocol2, + ) +if hasattr(ssl, "PROTOCOL_SSLv3"): + _protocol_to_min_max[ssl.PROTOCOL_SSLv3] = ( + SecurityConst.kSSLProtocol3, + SecurityConst.kSSLProtocol3, + ) +if hasattr(ssl, "PROTOCOL_TLSv1"): + _protocol_to_min_max[ssl.PROTOCOL_TLSv1] = ( + SecurityConst.kTLSProtocol1, + SecurityConst.kTLSProtocol1, + ) +if hasattr(ssl, "PROTOCOL_TLSv1_1"): + _protocol_to_min_max[ssl.PROTOCOL_TLSv1_1] = ( + SecurityConst.kTLSProtocol11, + SecurityConst.kTLSProtocol11, + ) +if hasattr(ssl, "PROTOCOL_TLSv1_2"): + _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = ( + SecurityConst.kTLSProtocol12, + SecurityConst.kTLSProtocol12, + ) + + +_tls_version_to_st: dict[int, int] = { + ssl.TLSVersion.MINIMUM_SUPPORTED: SecurityConst.kTLSProtocol1, + ssl.TLSVersion.TLSv1: SecurityConst.kTLSProtocol1, + ssl.TLSVersion.TLSv1_1: SecurityConst.kTLSProtocol11, + ssl.TLSVersion.TLSv1_2: SecurityConst.kTLSProtocol12, + ssl.TLSVersion.MAXIMUM_SUPPORTED: SecurityConst.kTLSProtocol12, +} + + +def inject_into_urllib3() -> None: + """ + Monkey-patch urllib3 with SecureTransport-backed SSL-support. + """ + util.SSLContext = SecureTransportContext # type: ignore[assignment] + util.ssl_.SSLContext = SecureTransportContext # type: ignore[assignment] + util.IS_SECURETRANSPORT = True + util.ssl_.IS_SECURETRANSPORT = True + + +def extract_from_urllib3() -> None: + """ + Undo monkey-patching by :func:`inject_into_urllib3`. + """ + util.SSLContext = orig_util_SSLContext + util.ssl_.SSLContext = orig_util_SSLContext + util.IS_SECURETRANSPORT = False + util.ssl_.IS_SECURETRANSPORT = False + + +def _read_callback( + connection_id: int, data_buffer: int, data_length_pointer: bytearray +) -> int: + """ + SecureTransport read callback. This is called by ST to request that data + be returned from the socket. + """ + wrapped_socket = None + try: + wrapped_socket = _connection_refs.get(connection_id) + if wrapped_socket is None: + return SecurityConst.errSSLInternal + base_socket = wrapped_socket.socket + + requested_length = data_length_pointer[0] + + timeout = wrapped_socket.gettimeout() + error = None + read_count = 0 + + try: + while read_count < requested_length: + if timeout is None or timeout >= 0: + if not util.wait_for_read(base_socket, timeout): + raise OSError(errno.EAGAIN, "timed out") + + remaining = requested_length - read_count + buffer = (ctypes.c_char * remaining).from_address( + data_buffer + read_count + ) + chunk_size = base_socket.recv_into(buffer, remaining) + read_count += chunk_size + if not chunk_size: + if not read_count: + return SecurityConst.errSSLClosedGraceful + break + except OSError as e: + error = e.errno + + if error is not None and error != errno.EAGAIN: + data_length_pointer[0] = read_count + if error == errno.ECONNRESET or error == errno.EPIPE: + return SecurityConst.errSSLClosedAbort + raise + + data_length_pointer[0] = read_count + + if read_count != requested_length: + return SecurityConst.errSSLWouldBlock + + return 0 + except Exception as e: + if wrapped_socket is not None: + wrapped_socket._exception = e + return SecurityConst.errSSLInternal + + +def _write_callback( + connection_id: int, data_buffer: int, data_length_pointer: bytearray +) -> int: + """ + SecureTransport write callback. This is called by ST to request that data + actually be sent on the network. + """ + wrapped_socket = None + try: + wrapped_socket = _connection_refs.get(connection_id) + if wrapped_socket is None: + return SecurityConst.errSSLInternal + base_socket = wrapped_socket.socket + + bytes_to_write = data_length_pointer[0] + data = ctypes.string_at(data_buffer, bytes_to_write) + + timeout = wrapped_socket.gettimeout() + error = None + sent = 0 + + try: + while sent < bytes_to_write: + if timeout is None or timeout >= 0: + if not util.wait_for_write(base_socket, timeout): + raise OSError(errno.EAGAIN, "timed out") + chunk_sent = base_socket.send(data) + sent += chunk_sent + + # This has some needless copying here, but I'm not sure there's + # much value in optimising this data path. + data = data[chunk_sent:] + except OSError as e: + error = e.errno + + if error is not None and error != errno.EAGAIN: + data_length_pointer[0] = sent + if error == errno.ECONNRESET or error == errno.EPIPE: + return SecurityConst.errSSLClosedAbort + raise + + data_length_pointer[0] = sent + + if sent != bytes_to_write: + return SecurityConst.errSSLWouldBlock + + return 0 + except Exception as e: + if wrapped_socket is not None: + wrapped_socket._exception = e + return SecurityConst.errSSLInternal + + +# We need to keep these two objects references alive: if they get GC'd while +# in use then SecureTransport could attempt to call a function that is in freed +# memory. That would be...uh...bad. Yeah, that's the word. Bad. +_read_callback_pointer = Security.SSLReadFunc(_read_callback) +_write_callback_pointer = Security.SSLWriteFunc(_write_callback) + + +class WrappedSocket: + """ + API-compatibility wrapper for Python's OpenSSL wrapped socket object. + """ + + def __init__(self, socket: socket_cls) -> None: + self.socket = socket + self.context = None + self._io_refs = 0 + self._closed = False + self._real_closed = False + self._exception: Exception | None = None + self._keychain = None + self._keychain_dir: str | None = None + self._client_cert_chain = None + + # We save off the previously-configured timeout and then set it to + # zero. This is done because we use select and friends to handle the + # timeouts, but if we leave the timeout set on the lower socket then + # Python will "kindly" call select on that socket again for us. Avoid + # that by forcing the timeout to zero. + self._timeout = self.socket.gettimeout() + self.socket.settimeout(0) + + @contextlib.contextmanager + def _raise_on_error(self) -> typing.Generator[None, None, None]: + """ + A context manager that can be used to wrap calls that do I/O from + SecureTransport. If any of the I/O callbacks hit an exception, this + context manager will correctly propagate the exception after the fact. + This avoids silently swallowing those exceptions. + + It also correctly forces the socket closed. + """ + self._exception = None + + # We explicitly don't catch around this yield because in the unlikely + # event that an exception was hit in the block we don't want to swallow + # it. + yield + if self._exception is not None: + exception, self._exception = self._exception, None + self._real_close() + raise exception + + def _set_alpn_protocols(self, protocols: list[bytes] | None) -> None: + """ + Sets up the ALPN protocols on the context. + """ + if not protocols: + return + protocols_arr = _create_cfstring_array(protocols) + try: + result = Security.SSLSetALPNProtocols(self.context, protocols_arr) + _assert_no_error(result) + finally: + CoreFoundation.CFRelease(protocols_arr) + + def _custom_validate(self, verify: bool, trust_bundle: bytes | None) -> None: + """ + Called when we have set custom validation. We do this in two cases: + first, when cert validation is entirely disabled; and second, when + using a custom trust DB. + Raises an SSLError if the connection is not trusted. + """ + # If we disabled cert validation, just say: cool. + if not verify or trust_bundle is None: + return + + successes = ( + SecurityConst.kSecTrustResultUnspecified, + SecurityConst.kSecTrustResultProceed, + ) + try: + trust_result = self._evaluate_trust(trust_bundle) + if trust_result in successes: + return + reason = f"error code: {int(trust_result)}" + exc = None + except Exception as e: + # Do not trust on error + reason = f"exception: {e!r}" + exc = e + + # SecureTransport does not send an alert nor shuts down the connection. + rec = _build_tls_unknown_ca_alert(self.version()) + self.socket.sendall(rec) + # close the connection immediately + # l_onoff = 1, activate linger + # l_linger = 0, linger for 0 seoncds + opts = struct.pack("ii", 1, 0) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts) + self._real_close() + raise ssl.SSLError(f"certificate verify failed, {reason}") from exc + + def _evaluate_trust(self, trust_bundle: bytes) -> int: + # We want data in memory, so load it up. + if os.path.isfile(trust_bundle): + with open(trust_bundle, "rb") as f: + trust_bundle = f.read() + + cert_array = None + trust = Security.SecTrustRef() + + try: + # Get a CFArray that contains the certs we want. + cert_array = _cert_array_from_pem(trust_bundle) + + # Ok, now the hard part. We want to get the SecTrustRef that ST has + # created for this connection, shove our CAs into it, tell ST to + # ignore everything else it knows, and then ask if it can build a + # chain. This is a buuuunch of code. + result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) + _assert_no_error(result) + if not trust: + raise ssl.SSLError("Failed to copy trust reference") + + result = Security.SecTrustSetAnchorCertificates(trust, cert_array) + _assert_no_error(result) + + result = Security.SecTrustSetAnchorCertificatesOnly(trust, True) + _assert_no_error(result) + + trust_result = Security.SecTrustResultType() + result = Security.SecTrustEvaluate(trust, ctypes.byref(trust_result)) + _assert_no_error(result) + finally: + if trust: + CoreFoundation.CFRelease(trust) + + if cert_array is not None: + CoreFoundation.CFRelease(cert_array) + + return trust_result.value # type: ignore[no-any-return] + + def handshake( + self, + server_hostname: bytes | str | None, + verify: bool, + trust_bundle: bytes | None, + min_version: int, + max_version: int, + client_cert: str | None, + client_key: str | None, + client_key_passphrase: typing.Any, + alpn_protocols: list[bytes] | None, + ) -> None: + """ + Actually performs the TLS handshake. This is run automatically by + wrapped socket, and shouldn't be needed in user code. + """ + # First, we do the initial bits of connection setup. We need to create + # a context, set its I/O funcs, and set the connection reference. + self.context = Security.SSLCreateContext( + None, SecurityConst.kSSLClientSide, SecurityConst.kSSLStreamType + ) + result = Security.SSLSetIOFuncs( + self.context, _read_callback_pointer, _write_callback_pointer + ) + _assert_no_error(result) + + # Here we need to compute the handle to use. We do this by taking the + # id of self modulo 2**31 - 1. If this is already in the dictionary, we + # just keep incrementing by one until we find a free space. + with _connection_ref_lock: + handle = id(self) % 2147483647 + while handle in _connection_refs: + handle = (handle + 1) % 2147483647 + _connection_refs[handle] = self + + result = Security.SSLSetConnection(self.context, handle) + _assert_no_error(result) + + # If we have a server hostname, we should set that too. + # RFC6066 Section 3 tells us not to use SNI when the host is an IP, but we have + # to do it anyway to match server_hostname against the server certificate + if server_hostname: + if not isinstance(server_hostname, bytes): + server_hostname = server_hostname.encode("utf-8") + + result = Security.SSLSetPeerDomainName( + self.context, server_hostname, len(server_hostname) + ) + _assert_no_error(result) + + # Setup the ALPN protocols. + self._set_alpn_protocols(alpn_protocols) + + # Set the minimum and maximum TLS versions. + result = Security.SSLSetProtocolVersionMin(self.context, min_version) + _assert_no_error(result) + + result = Security.SSLSetProtocolVersionMax(self.context, max_version) + _assert_no_error(result) + + # If there's a trust DB, we need to use it. We do that by telling + # SecureTransport to break on server auth. We also do that if we don't + # want to validate the certs at all: we just won't actually do any + # authing in that case. + if not verify or trust_bundle is not None: + result = Security.SSLSetSessionOption( + self.context, SecurityConst.kSSLSessionOptionBreakOnServerAuth, True + ) + _assert_no_error(result) + + # If there's a client cert, we need to use it. + if client_cert: + self._keychain, self._keychain_dir = _temporary_keychain() + self._client_cert_chain = _load_client_cert_chain( + self._keychain, client_cert, client_key + ) + result = Security.SSLSetCertificate(self.context, self._client_cert_chain) + _assert_no_error(result) + + while True: + with self._raise_on_error(): + result = Security.SSLHandshake(self.context) + + if result == SecurityConst.errSSLWouldBlock: + raise socket.timeout("handshake timed out") + elif result == SecurityConst.errSSLServerAuthCompleted: + self._custom_validate(verify, trust_bundle) + continue + else: + _assert_no_error(result) + break + + def fileno(self) -> int: + return self.socket.fileno() + + # Copy-pasted from Python 3.5 source code + def _decref_socketios(self) -> None: + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def recv(self, bufsiz: int) -> bytes: + buffer = ctypes.create_string_buffer(bufsiz) + bytes_read = self.recv_into(buffer, bufsiz) + data = buffer[:bytes_read] + return typing.cast(bytes, data) + + def recv_into( + self, buffer: ctypes.Array[ctypes.c_char], nbytes: int | None = None + ) -> int: + # Read short on EOF. + if self._real_closed: + return 0 + + if nbytes is None: + nbytes = len(buffer) + + buffer = (ctypes.c_char * nbytes).from_buffer(buffer) + processed_bytes = ctypes.c_size_t(0) + + with self._raise_on_error(): + result = Security.SSLRead( + self.context, buffer, nbytes, ctypes.byref(processed_bytes) + ) + + # There are some result codes that we want to treat as "not always + # errors". Specifically, those are errSSLWouldBlock, + # errSSLClosedGraceful, and errSSLClosedNoNotify. + if result == SecurityConst.errSSLWouldBlock: + # If we didn't process any bytes, then this was just a time out. + # However, we can get errSSLWouldBlock in situations when we *did* + # read some data, and in those cases we should just read "short" + # and return. + if processed_bytes.value == 0: + # Timed out, no data read. + raise socket.timeout("recv timed out") + elif result in ( + SecurityConst.errSSLClosedGraceful, + SecurityConst.errSSLClosedNoNotify, + ): + # The remote peer has closed this connection. We should do so as + # well. Note that we don't actually return here because in + # principle this could actually be fired along with return data. + # It's unlikely though. + self._real_close() + else: + _assert_no_error(result) + + # Ok, we read and probably succeeded. We should return whatever data + # was actually read. + return processed_bytes.value + + def settimeout(self, timeout: float) -> None: + self._timeout = timeout + + def gettimeout(self) -> float | None: + return self._timeout + + def send(self, data: bytes) -> int: + processed_bytes = ctypes.c_size_t(0) + + with self._raise_on_error(): + result = Security.SSLWrite( + self.context, data, len(data), ctypes.byref(processed_bytes) + ) + + if result == SecurityConst.errSSLWouldBlock and processed_bytes.value == 0: + # Timed out + raise socket.timeout("send timed out") + else: + _assert_no_error(result) + + # We sent, and probably succeeded. Tell them how much we sent. + return processed_bytes.value + + def sendall(self, data: bytes) -> None: + total_sent = 0 + while total_sent < len(data): + sent = self.send(data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]) + total_sent += sent + + def shutdown(self) -> None: + with self._raise_on_error(): + Security.SSLClose(self.context) + + def close(self) -> None: + self._closed = True + # TODO: should I do clean shutdown here? Do I have to? + if self._io_refs <= 0: + self._real_close() + + def _real_close(self) -> None: + self._real_closed = True + if self.context: + CoreFoundation.CFRelease(self.context) + self.context = None + if self._client_cert_chain: + CoreFoundation.CFRelease(self._client_cert_chain) + self._client_cert_chain = None + if self._keychain: + Security.SecKeychainDelete(self._keychain) + CoreFoundation.CFRelease(self._keychain) + shutil.rmtree(self._keychain_dir) + self._keychain = self._keychain_dir = None + return self.socket.close() + + def getpeercert(self, binary_form: bool = False) -> bytes | None: + # Urgh, annoying. + # + # Here's how we do this: + # + # 1. Call SSLCopyPeerTrust to get hold of the trust object for this + # connection. + # 2. Call SecTrustGetCertificateAtIndex for index 0 to get the leaf. + # 3. To get the CN, call SecCertificateCopyCommonName and process that + # string so that it's of the appropriate type. + # 4. To get the SAN, we need to do something a bit more complex: + # a. Call SecCertificateCopyValues to get the data, requesting + # kSecOIDSubjectAltName. + # b. Mess about with this dictionary to try to get the SANs out. + # + # This is gross. Really gross. It's going to be a few hundred LoC extra + # just to repeat something that SecureTransport can *already do*. So my + # operating assumption at this time is that what we want to do is + # instead to just flag to urllib3 that it shouldn't do its own hostname + # validation when using SecureTransport. + if not binary_form: + raise ValueError("SecureTransport only supports dumping binary certs") + trust = Security.SecTrustRef() + certdata = None + der_bytes = None + + try: + # Grab the trust store. + result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) + _assert_no_error(result) + if not trust: + # Probably we haven't done the handshake yet. No biggie. + return None + + cert_count = Security.SecTrustGetCertificateCount(trust) + if not cert_count: + # Also a case that might happen if we haven't handshaked. + # Handshook? Handshaken? + return None + + leaf = Security.SecTrustGetCertificateAtIndex(trust, 0) + assert leaf + + # Ok, now we want the DER bytes. + certdata = Security.SecCertificateCopyData(leaf) + assert certdata + + data_length = CoreFoundation.CFDataGetLength(certdata) + data_buffer = CoreFoundation.CFDataGetBytePtr(certdata) + der_bytes = ctypes.string_at(data_buffer, data_length) + finally: + if certdata: + CoreFoundation.CFRelease(certdata) + if trust: + CoreFoundation.CFRelease(trust) + + return der_bytes + + def version(self) -> str: + protocol = Security.SSLProtocol() + result = Security.SSLGetNegotiatedProtocolVersion( + self.context, ctypes.byref(protocol) + ) + _assert_no_error(result) + if protocol.value == SecurityConst.kTLSProtocol13: + raise ssl.SSLError("SecureTransport does not support TLS 1.3") + elif protocol.value == SecurityConst.kTLSProtocol12: + return "TLSv1.2" + elif protocol.value == SecurityConst.kTLSProtocol11: + return "TLSv1.1" + elif protocol.value == SecurityConst.kTLSProtocol1: + return "TLSv1" + elif protocol.value == SecurityConst.kSSLProtocol3: + return "SSLv3" + elif protocol.value == SecurityConst.kSSLProtocol2: + return "SSLv2" + else: + raise ssl.SSLError(f"Unknown TLS version: {protocol!r}") + + +def makefile( + self: socket_cls, + mode: ( + Literal["r"] | Literal["w"] | Literal["rw"] | Literal["wr"] | Literal[""] + ) = "r", + buffering: int | None = None, + *args: typing.Any, + **kwargs: typing.Any, +) -> typing.BinaryIO | typing.TextIO: + # We disable buffering with SecureTransport because it conflicts with + # the buffering that ST does internally (see issue #1153 for more). + buffering = 0 + return socket_cls.makefile(self, mode, buffering, *args, **kwargs) + + +WrappedSocket.makefile = makefile # type: ignore[attr-defined] + + +class SecureTransportContext: + """ + I am a wrapper class for the SecureTransport library, to translate the + interface of the standard library ``SSLContext`` object to calls into + SecureTransport. + """ + + def __init__(self, protocol: int) -> None: + self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED + self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED + if protocol not in (None, ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_CLIENT): + self._min_version, self._max_version = _protocol_to_min_max[protocol] + + self._options = 0 + self._verify = False + self._trust_bundle: bytes | None = None + self._client_cert: str | None = None + self._client_key: str | None = None + self._client_key_passphrase = None + self._alpn_protocols: list[bytes] | None = None + + @property + def check_hostname(self) -> Literal[True]: + """ + SecureTransport cannot have its hostname checking disabled. For more, + see the comment on getpeercert() in this file. + """ + return True + + @check_hostname.setter + def check_hostname(self, value: typing.Any) -> None: + """ + SecureTransport cannot have its hostname checking disabled. For more, + see the comment on getpeercert() in this file. + """ + + @property + def options(self) -> int: + # TODO: Well, crap. + # + # So this is the bit of the code that is the most likely to cause us + # trouble. Essentially we need to enumerate all of the SSL options that + # users might want to use and try to see if we can sensibly translate + # them, or whether we should just ignore them. + return self._options + + @options.setter + def options(self, value: int) -> None: + # TODO: Update in line with above. + self._options = value + + @property + def verify_mode(self) -> int: + return ssl.CERT_REQUIRED if self._verify else ssl.CERT_NONE + + @verify_mode.setter + def verify_mode(self, value: int) -> None: + self._verify = value == ssl.CERT_REQUIRED + + def set_default_verify_paths(self) -> None: + # So, this has to do something a bit weird. Specifically, what it does + # is nothing. + # + # This means that, if we had previously had load_verify_locations + # called, this does not undo that. We need to do that because it turns + # out that the rest of the urllib3 code will attempt to load the + # default verify paths if it hasn't been told about any paths, even if + # the context itself was sometime earlier. We resolve that by just + # ignoring it. + pass + + def load_default_certs(self) -> None: + return self.set_default_verify_paths() + + def set_ciphers(self, ciphers: typing.Any) -> None: + raise ValueError("SecureTransport doesn't support custom cipher strings") + + def load_verify_locations( + self, + cafile: str | None = None, + capath: str | None = None, + cadata: bytes | None = None, + ) -> None: + # OK, we only really support cadata and cafile. + if capath is not None: + raise ValueError("SecureTransport does not support cert directories") + + # Raise if cafile does not exist. + if cafile is not None: + with open(cafile): + pass + + self._trust_bundle = cafile or cadata # type: ignore[assignment] + + def load_cert_chain( + self, + certfile: str, + keyfile: str | None = None, + password: str | None = None, + ) -> None: + self._client_cert = certfile + self._client_key = keyfile + self._client_cert_passphrase = password + + def set_alpn_protocols(self, protocols: list[str | bytes]) -> None: + """ + Sets the ALPN protocols that will later be set on the context. + + Raises a NotImplementedError if ALPN is not supported. + """ + if not hasattr(Security, "SSLSetALPNProtocols"): + raise NotImplementedError( + "SecureTransport supports ALPN only in macOS 10.12+" + ) + self._alpn_protocols = [util.util.to_bytes(p, "ascii") for p in protocols] + + def wrap_socket( + self, + sock: socket_cls, + server_side: bool = False, + do_handshake_on_connect: bool = True, + suppress_ragged_eofs: bool = True, + server_hostname: bytes | str | None = None, + ) -> WrappedSocket: + # So, what do we do here? Firstly, we assert some properties. This is a + # stripped down shim, so there is some functionality we don't support. + # See PEP 543 for the real deal. + assert not server_side + assert do_handshake_on_connect + assert suppress_ragged_eofs + + # Ok, we're good to go. Now we want to create the wrapped socket object + # and store it in the appropriate place. + wrapped_socket = WrappedSocket(sock) + + # Now we can handshake + wrapped_socket.handshake( + server_hostname, + self._verify, + self._trust_bundle, + _tls_version_to_st[self._minimum_version], + _tls_version_to_st[self._maximum_version], + self._client_cert, + self._client_key, + self._client_key_passphrase, + self._alpn_protocols, + ) + return wrapped_socket + + @property + def minimum_version(self) -> int: + return self._minimum_version + + @minimum_version.setter + def minimum_version(self, minimum_version: int) -> None: + self._minimum_version = minimum_version + + @property + def maximum_version(self) -> int: + return self._maximum_version + + @maximum_version.setter + def maximum_version(self, maximum_version: int) -> None: + self._maximum_version = maximum_version diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/socks.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/socks.py new file mode 100644 index 0000000000000000000000000000000000000000..5e552ddaed36d698bee9c086a590af3807ba1972 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/contrib/socks.py @@ -0,0 +1,233 @@ +""" +This module contains provisional support for SOCKS proxies from within +urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and +SOCKS5. To enable its functionality, either install PySocks or install this +module with the ``socks`` extra. + +The SOCKS implementation supports the full range of urllib3 features. It also +supports the following SOCKS features: + +- SOCKS4A (``proxy_url='socks4a://...``) +- SOCKS4 (``proxy_url='socks4://...``) +- SOCKS5 with remote DNS (``proxy_url='socks5h://...``) +- SOCKS5 with local DNS (``proxy_url='socks5://...``) +- Usernames and passwords for the SOCKS proxy + +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. + +SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 +supports IPv4, IPv6, and domain names. + +When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python + + proxy_url="socks4a://@proxy-host" + +When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion +of the ``proxy_url`` will be sent as the username/password to authenticate +with the proxy: + +.. code-block:: python + + proxy_url="socks5h://:@proxy-host" + +""" + +from __future__ import annotations + +try: + import socks # type: ignore[import] +except ImportError: + import warnings + + from ..exceptions import DependencyWarning + + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies" + ), + DependencyWarning, + ) + raise + +import typing +from socket import timeout as SocketTimeout + +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool +from ..exceptions import ConnectTimeoutError, NewConnectionError +from ..poolmanager import PoolManager +from ..util.url import parse_url + +try: + import ssl +except ImportError: + ssl = None # type: ignore[assignment] + +try: + from typing import TypedDict + + class _TYPE_SOCKS_OPTIONS(TypedDict): + socks_version: int + proxy_host: str | None + proxy_port: str | None + username: str | None + password: str | None + rdns: bool + +except ImportError: # Python 3.7 + _TYPE_SOCKS_OPTIONS = typing.Dict[str, typing.Any] # type: ignore[misc, assignment] + + +class SOCKSConnection(HTTPConnection): + """ + A plain-text HTTP connection that connects via a SOCKS proxy. + """ + + def __init__( + self, + _socks_options: _TYPE_SOCKS_OPTIONS, + *args: typing.Any, + **kwargs: typing.Any, + ) -> None: + self._socks_options = _socks_options + super().__init__(*args, **kwargs) + + def _new_conn(self) -> socks.socksocket: + """ + Establish a new connection via the SOCKS proxy. + """ + extra_kw: dict[str, typing.Any] = {} + if self.source_address: + extra_kw["source_address"] = self.source_address + + if self.socket_options: + extra_kw["socket_options"] = self.socket_options + + try: + conn = socks.create_connection( + (self.host, self.port), + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], + timeout=self.timeout, + **extra_kw, + ) + + except SocketTimeout as e: + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + + except socks.ProxyError as e: + # This is fragile as hell, but it seems to be the only way to raise + # useful errors here. + if e.socket_err: + error = e.socket_err + if isinstance(error, SocketTimeout): + raise ConnectTimeoutError( + self, + f"Connection to {self.host} timed out. (connect timeout={self.timeout})", + ) from e + else: + # Adding `from e` messes with coverage somehow, so it's omitted. + # See #2386. + raise NewConnectionError( + self, f"Failed to establish a new connection: {error}" + ) + else: + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + except OSError as e: # Defensive: PySocks should catch all these. + raise NewConnectionError( + self, f"Failed to establish a new connection: {e}" + ) from e + + return conn + + +# We don't need to duplicate the Verified/Unverified distinction from +# urllib3/connection.py here because the HTTPSConnection will already have been +# correctly set to either the Verified or Unverified form by that module. This +# means the SOCKSHTTPSConnection will automatically be the correct type. +class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): + pass + + +class SOCKSHTTPConnectionPool(HTTPConnectionPool): + ConnectionCls = SOCKSConnection + + +class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): + ConnectionCls = SOCKSHTTPSConnection + + +class SOCKSProxyManager(PoolManager): + """ + A version of the urllib3 ProxyManager that routes connections via the + defined SOCKS proxy. + """ + + pool_classes_by_scheme = { + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, + } + + def __init__( + self, + proxy_url: str, + username: str | None = None, + password: str | None = None, + num_pools: int = 10, + headers: typing.Mapping[str, str] | None = None, + **connection_pool_kw: typing.Any, + ): + parsed = parse_url(proxy_url) + + if username is None and password is None and parsed.auth is not None: + split = parsed.auth.split(":") + if len(split) == 2: + username, password = split + if parsed.scheme == "socks5": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = False + elif parsed.scheme == "socks5h": + socks_version = socks.PROXY_TYPE_SOCKS5 + rdns = True + elif parsed.scheme == "socks4": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = False + elif parsed.scheme == "socks4a": + socks_version = socks.PROXY_TYPE_SOCKS4 + rdns = True + else: + raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") + + self.proxy_url = proxy_url + + socks_options = { + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, + } + connection_pool_kw["_socks_options"] = socks_options + + super().__init__(num_pools, headers, **connection_pool_kw) + + self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ff56c55bae3059b2b4578b3f0220a1fcd80984d4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__init__.py @@ -0,0 +1,44 @@ +# For backwards compatibility, provide imports that used to be here. +from __future__ import annotations + +from .connection import is_connection_dropped +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers +from .response import is_fp_closed +from .retry import Retry +from .ssl_ import ( + ALPN_PROTOCOLS, + IS_PYOPENSSL, + IS_SECURETRANSPORT, + SSLContext, + assert_fingerprint, + create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, + ssl_wrap_socket, +) +from .timeout import Timeout +from .url import Url, parse_url +from .wait import wait_for_read, wait_for_write + +__all__ = ( + "IS_PYOPENSSL", + "IS_SECURETRANSPORT", + "SSLContext", + "ALPN_PROTOCOLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "create_urllib3_context", + "is_connection_dropped", + "is_fp_closed", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", +) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f72074d820475f02bb5769da2cf0a597ca867215 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/connection.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/connection.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f252ba26dd75092c17f7056f8c30d4d7762e8f8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/connection.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/proxy.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/proxy.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..43f1cedb0f0eaa2df96f58cfb6587d3bd583a1ca Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/proxy.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/request.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/request.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d188004d838aaca80e8d6033cb0a92b2a6915545 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/request.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/response.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/response.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f1af44c921ad84ad58ceaafaa7e880095c5f9db Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/response.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/retry.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/retry.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdd4d90142106c1c195d3a1542ccbf56f2b71f9c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/retry.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7e1dc66dd833a330468359733d83fa10da9b81b Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_match_hostname.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_match_hostname.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..077f181d3067b6cec5fa13098962d6beb3d924a4 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssl_match_hostname.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssltransport.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssltransport.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f4dc746b1fa1ec35e052270391f07b07cf7feea Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/ssltransport.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/timeout.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/timeout.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54adef6faef67575cfe8152710e4396fc3eaa8f0 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/timeout.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/url.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/url.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..699b29986aa2f1cc74372f0d76eb5cfe2709d602 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/url.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/util.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a43cfaa33a90b86f075789a53e6762874857656a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/util.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/wait.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/wait.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae251284e7e23e66c0e4e2778907292c3db93297 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/__pycache__/wait.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/connection.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/connection.py new file mode 100644 index 0000000000000000000000000000000000000000..5c7da73f4e0e57cfe9074c50c2628300130fc94d --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/connection.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import socket +import typing + +from ..exceptions import LocationParseError +from .timeout import _DEFAULT_TIMEOUT, _TYPE_TIMEOUT + +_TYPE_SOCKET_OPTIONS = typing.Sequence[typing.Tuple[int, int, typing.Union[int, bytes]]] + +if typing.TYPE_CHECKING: + from .._base_connection import BaseHTTPConnection + + +def is_connection_dropped(conn: BaseHTTPConnection) -> bool: # Platform-specific + """ + Returns True if the connection is dropped and should be closed. + :param conn: :class:`urllib3.connection.HTTPConnection` object. + """ + return not conn.is_connected + + +# This function is copied from socket.py in the Python 2.7 standard +# library test suite. Added to its signature is only `socket_options`. +# One additional modification is that we avoid binding to IPv6 servers +# discovered in DNS if the system doesn't have IPv6 functionality. +def create_connection( + address: tuple[str, int], + timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + source_address: tuple[str, int] | None = None, + socket_options: _TYPE_SOCKET_OPTIONS | None = None, +) -> socket.socket: + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`socket.getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + if host.startswith("["): + host = host.strip("[]") + err = None + + # Using the value from allowed_gai_family() in the context of getaddrinfo lets + # us select whether to work with IPv4 DNS records, IPv6 records, or both. + # The original create_connection function always returns all records. + family = allowed_gai_family() + + try: + host.encode("idna") + except UnicodeError: + raise LocationParseError(f"'{host}', label empty or too long") from None + + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + + # If provided, set socket level options before connecting. + _set_socket_options(sock, socket_options) + + if timeout is not _DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + # Break explicitly a reference cycle + err = None + return sock + + except OSError as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + try: + raise err + finally: + # Break explicitly a reference cycle + err = None + else: + raise OSError("getaddrinfo returns an empty list") + + +def _set_socket_options( + sock: socket.socket, options: _TYPE_SOCKET_OPTIONS | None +) -> None: + if options is None: + return + + for opt in options: + sock.setsockopt(*opt) + + +def allowed_gai_family() -> socket.AddressFamily: + """This function is designed to work in the context of + getaddrinfo, where family=socket.AF_UNSPEC is the default and + will perform a DNS search for both IPv6 and IPv4 records.""" + + family = socket.AF_INET + if HAS_IPV6: + family = socket.AF_UNSPEC + return family + + +def _has_ipv6(host: str) -> bool: + """Returns True if the system can bind an IPv6 address.""" + sock = None + has_ipv6 = False + + if socket.has_ipv6: + # has_ipv6 returns true if cPython was compiled with IPv6 support. + # It does not tell us if the system has IPv6 support enabled. To + # determine that we must bind to an IPv6 address. + # https://github.com/urllib3/urllib3/pull/611 + # https://bugs.python.org/issue658327 + try: + sock = socket.socket(socket.AF_INET6) + sock.bind((host, 0)) + has_ipv6 = True + except Exception: + pass + + if sock: + sock.close() + return has_ipv6 + + +HAS_IPV6 = _has_ipv6("::1") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/proxy.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/proxy.py new file mode 100644 index 0000000000000000000000000000000000000000..908fc6621d0afbed16bde2c1957a5cf28d3a84d8 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/proxy.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import typing + +from .url import Url + +if typing.TYPE_CHECKING: + from ..connection import ProxyConfig + + +def connection_requires_http_tunnel( + proxy_url: Url | None = None, + proxy_config: ProxyConfig | None = None, + destination_scheme: str | None = None, +) -> bool: + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/request.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/request.py new file mode 100644 index 0000000000000000000000000000000000000000..7d6866f3adbcf765068ce845657b40917b9c5b48 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/request.py @@ -0,0 +1,256 @@ +from __future__ import annotations + +import io +import typing +from base64 import b64encode +from enum import Enum + +from ..exceptions import UnrewindableBodyError +from .util import to_bytes + +if typing.TYPE_CHECKING: + from typing_extensions import Final + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) + +ACCEPT_ENCODING = "gzip,deflate" +try: + try: + import brotlicffi as _unused_module_brotli # type: ignore[import] # noqa: F401 + except ImportError: + import brotli as _unused_module_brotli # type: ignore[import] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",br" +try: + import zstandard as _unused_module_zstd # type: ignore[import] # noqa: F401 +except ImportError: + pass +else: + ACCEPT_ENCODING += ",zstd" + + +class _TYPE_FAILEDTELL(Enum): + token = 0 + + +_FAILEDTELL: Final[_TYPE_FAILEDTELL] = _TYPE_FAILEDTELL.token + +_TYPE_BODY_POSITION = typing.Union[int, _TYPE_FAILEDTELL] + +# When sending a request with these methods we aren't expecting +# a body so don't need to set an explicit 'Content-Length: 0' +# The reason we do this in the negative instead of tracking methods +# which 'should' have a body is because unknown methods should be +# treated as if they were 'POST' which *does* expect a body. +_METHODS_NOT_EXPECTING_BODY = {"GET", "HEAD", "DELETE", "TRACE", "OPTIONS", "CONNECT"} + + +def make_headers( + keep_alive: bool | None = None, + accept_encoding: bool | list[str] | str | None = None, + user_agent: str | None = None, + basic_auth: str | None = None, + proxy_basic_auth: str | None = None, + disable_cache: bool | None = None, +) -> dict[str, str]: + """ + Shortcuts for generating request headers. + + :param keep_alive: + If ``True``, adds 'connection: keep-alive' header. + + :param accept_encoding: + Can be a boolean, list, or string. + ``True`` translates to 'gzip,deflate'. If either the ``brotli`` or + ``brotlicffi`` package is installed 'gzip,deflate,br' is used instead. + List will get joined by comma. + String will be used as provided. + + :param user_agent: + String representing the user-agent you want, such as + "python-urllib3/0.6" + + :param basic_auth: + Colon-separated username:password string for 'authorization: basic ...' + auth header. + + :param proxy_basic_auth: + Colon-separated username:password string for 'proxy-authorization: basic ...' + auth header. + + :param disable_cache: + If ``True``, adds 'cache-control: no-cache' header. + + Example: + + .. code-block:: python + + import urllib3 + + print(urllib3.util.make_headers(keep_alive=True, user_agent="Batman/1.0")) + # {'connection': 'keep-alive', 'user-agent': 'Batman/1.0'} + print(urllib3.util.make_headers(accept_encoding=True)) + # {'accept-encoding': 'gzip,deflate'} + """ + headers: dict[str, str] = {} + if accept_encoding: + if isinstance(accept_encoding, str): + pass + elif isinstance(accept_encoding, list): + accept_encoding = ",".join(accept_encoding) + else: + accept_encoding = ACCEPT_ENCODING + headers["accept-encoding"] = accept_encoding + + if user_agent: + headers["user-agent"] = user_agent + + if keep_alive: + headers["connection"] = "keep-alive" + + if basic_auth: + headers[ + "authorization" + ] = f"Basic {b64encode(basic_auth.encode('latin-1')).decode()}" + + if proxy_basic_auth: + headers[ + "proxy-authorization" + ] = f"Basic {b64encode(proxy_basic_auth.encode('latin-1')).decode()}" + + if disable_cache: + headers["cache-control"] = "no-cache" + + return headers + + +def set_file_position( + body: typing.Any, pos: _TYPE_BODY_POSITION | None +) -> _TYPE_BODY_POSITION | None: + """ + If a position is provided, move file to that point. + Otherwise, we'll attempt to record a position for future use. + """ + if pos is not None: + rewind_body(body, pos) + elif getattr(body, "tell", None) is not None: + try: + pos = body.tell() + except OSError: + # This differentiates from None, allowing us to catch + # a failed `tell()` later when trying to rewind the body. + pos = _FAILEDTELL + + return pos + + +def rewind_body(body: typing.IO[typing.AnyStr], body_pos: _TYPE_BODY_POSITION) -> None: + """ + Attempt to rewind body to a certain position. + Primarily used for request redirects and retries. + + :param body: + File-like object that supports seek. + + :param int pos: + Position to seek to in file. + """ + body_seek = getattr(body, "seek", None) + if body_seek is not None and isinstance(body_pos, int): + try: + body_seek(body_pos) + except OSError as e: + raise UnrewindableBodyError( + "An error occurred when rewinding request body for redirect/retry." + ) from e + elif body_pos is _FAILEDTELL: + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) + else: + raise ValueError( + f"body_pos must be of type integer, instead it was {type(body_pos)}." + ) + + +class ChunksAndContentLength(typing.NamedTuple): + chunks: typing.Iterable[bytes] | None + content_length: int | None + + +def body_to_chunks( + body: typing.Any | None, method: str, blocksize: int +) -> ChunksAndContentLength: + """Takes the HTTP request method, body, and blocksize and + transforms them into an iterable of chunks to pass to + socket.sendall() and an optional 'Content-Length' header. + + A 'Content-Length' of 'None' indicates the length of the body + can't be determined so should use 'Transfer-Encoding: chunked' + for framing instead. + """ + + chunks: typing.Iterable[bytes] | None + content_length: int | None + + # No body, we need to make a recommendation on 'Content-Length' + # based on whether that request method is expected to have + # a body or not. + if body is None: + chunks = None + if method.upper() not in _METHODS_NOT_EXPECTING_BODY: + content_length = 0 + else: + content_length = None + + # Bytes or strings become bytes + elif isinstance(body, (str, bytes)): + chunks = (to_bytes(body),) + content_length = len(chunks[0]) + + # File-like object, TODO: use seek() and tell() for length? + elif hasattr(body, "read"): + + def chunk_readable() -> typing.Iterable[bytes]: + nonlocal body, blocksize + encode = isinstance(body, io.TextIOBase) + while True: + datablock = body.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("iso-8859-1") + yield datablock + + chunks = chunk_readable() + content_length = None + + # Otherwise we need to start checking via duck-typing. + else: + try: + # Check if the body implements the buffer API. + mv = memoryview(body) + except TypeError: + try: + # Check if the body is an iterable + chunks = iter(body) + content_length = None + except TypeError: + raise TypeError( + f"'body' must be a bytes-like object, file-like " + f"object, or iterable. Instead was {body!r}" + ) from None + else: + # Since it implements the buffer API can be passed directly to socket.sendall() + chunks = (body,) + content_length = mv.nbytes + + return ChunksAndContentLength(chunks=chunks, content_length=content_length) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/response.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/response.py new file mode 100644 index 0000000000000000000000000000000000000000..0f4578696fa2e17a900c6890ec26d65e860b0b72 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/response.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import http.client as httplib +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect + +from ..exceptions import HeaderParsingError + + +def is_fp_closed(obj: object) -> bool: + """ + Checks whether a given file-like object is closed. + + :param obj: + The file-like object to check. + """ + + try: + # Check `isclosed()` first, in case Python3 doesn't set `closed`. + # GH Issue #928 + return obj.isclosed() # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check via the official file-like-object way. + return obj.closed # type: ignore[no-any-return, attr-defined] + except AttributeError: + pass + + try: + # Check if the object is a container for another file-like object that + # gets released on exhaustion (e.g. HTTPResponse). + return obj.fp is None # type: ignore[attr-defined] + except AttributeError: + pass + + raise ValueError("Unable to determine whether fp is closed.") + + +def assert_header_parsing(headers: httplib.HTTPMessage) -> None: + """ + Asserts whether all headers have been successfully parsed. + Extracts encountered errors from the result of parsing headers. + + Only works on Python 3. + + :param http.client.HTTPMessage headers: Headers to verify. + + :raises urllib3.exceptions.HeaderParsingError: + If parsing errors are found. + """ + + # This will fail silently if we pass in the wrong kind of parameter. + # To make debugging easier add an explicit check. + if not isinstance(headers, httplib.HTTPMessage): + raise TypeError(f"expected httplib.Message, got {type(headers)}.") + + unparsed_data = None + + # get_payload is actually email.message.Message.get_payload; + # we're only interested in the result if it's not a multipart message + if not headers.is_multipart(): + payload = headers.get_payload() + + if isinstance(payload, (bytes, str)): + unparsed_data = payload + + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in headers.defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] + + if defects or unparsed_data: + raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) + + +def is_response_to_head(response: httplib.HTTPResponse) -> bool: + """ + Checks whether the request of a response has been a HEAD-request. + + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. + """ + # FIXME: Can we do this somehow without accessing private httplib _method? + method_str = response._method # type: str # type: ignore[attr-defined] + return method_str.upper() == "HEAD" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/retry.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/retry.py new file mode 100644 index 0000000000000000000000000000000000000000..7572bfd26ad87711d67c3418a6a0ac9921fed08c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/retry.py @@ -0,0 +1,529 @@ +from __future__ import annotations + +import email +import logging +import random +import re +import time +import typing +from itertools import takewhile +from types import TracebackType + +from ..exceptions import ( + ConnectTimeoutError, + InvalidHeader, + MaxRetryError, + ProtocolError, + ProxyError, + ReadTimeoutError, + ResponseError, +) +from .util import reraise + +if typing.TYPE_CHECKING: + from ..connectionpool import ConnectionPool + from ..response import BaseHTTPResponse + +log = logging.getLogger(__name__) + + +# Data structure for representing the metadata of requests that result in a retry. +class RequestHistory(typing.NamedTuple): + method: str | None + url: str | None + error: Exception | None + status: int | None + redirect_location: str | None + + +class Retry: + """Retry configuration. + + Each retry attempt will create a new Retry object with updated values, so + they can be safely reused. + + Retries can be defined as a default for a pool: + + .. code-block:: python + + retries = Retry(connect=5, read=2, redirect=5) + http = PoolManager(retries=retries) + response = http.request("GET", "https://example.com/") + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=Retry(10)) + + Retries can be disabled by passing ``False``: + + .. code-block:: python + + response = http.request("GET", "https://example.com/", retries=False) + + Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless + retries are disabled, in which case the causing exception will be raised. + + :param int total: + Total number of retries to allow. Takes precedence over other counts. + + Set to ``None`` to remove this constraint and fall back on other + counts. + + Set to ``0`` to fail on the first retry. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int connect: + How many connection-related errors to retry on. + + These are errors raised before the request is sent to the remote server, + which we assume has not triggered the server to process the request. + + Set to ``0`` to fail on the first retry of this type. + + :param int read: + How many times to retry on read errors. + + These errors are raised after the request was sent to the server, so the + request may have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + :param int redirect: + How many redirects to perform. Limit this to avoid infinite redirect + loops. + + A redirect is a HTTP response with a status code 301, 302, 303, 307 or + 308. + + Set to ``0`` to fail on the first retry of this type. + + Set to ``False`` to disable and imply ``raise_on_redirect=False``. + + :param int status: + How many times to retry on bad status codes. + + These are retries made on responses, where status code matches + ``status_forcelist``. + + Set to ``0`` to fail on the first retry of this type. + + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param Collection allowed_methods: + Set of uppercased HTTP method verbs that we should retry on. + + By default, we only retry on methods which are considered to be + idempotent (multiple requests with the same parameters end with the + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. + + Set to a ``None`` value to retry on any verb. + + :param Collection status_forcelist: + A set of integer HTTP status codes that we should force a retry on. + A retry is initiated if the request method is in ``allowed_methods`` + and the response status code is in ``status_forcelist``. + + By default, this is disabled with ``None``. + + :param float backoff_factor: + A backoff factor to apply between attempts after the second try + (most errors are resolved immediately by a second try without a + delay). urllib3 will sleep for:: + + {backoff factor} * (2 ** ({number of previous retries})) + + seconds. If `backoff_jitter` is non-zero, this sleep is extended by:: + + random.uniform(0, {backoff jitter}) + + seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will + sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever + be longer than `backoff_max`. + + By default, backoff is disabled (factor set to 0). + + :param bool raise_on_redirect: Whether, if the number of redirects is + exhausted, to raise a MaxRetryError, or to return a response with a + response code in the 3xx range. + + :param bool raise_on_status: Similar meaning to ``raise_on_redirect``: + whether we should raise an exception, or return a response, + if status falls in ``status_forcelist`` range and retries have + been exhausted. + + :param tuple history: The history of the request encountered during + each call to :meth:`~Retry.increment`. The list is in the order + the requests occurred. Each list item is of class :class:`RequestHistory`. + + :param bool respect_retry_after_header: + Whether to respect Retry-After header on status codes defined as + :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not. + + :param Collection remove_headers_on_redirect: + Sequence of headers to remove from the request when a response + indicating a redirect is returned before firing off the redirected + request. + """ + + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) + + #: Default status codes to be used for ``status_forcelist`` + RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) + + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Cookie", "Authorization"]) + + #: Default maximum backoff time. + DEFAULT_BACKOFF_MAX = 120 + + # Backward compatibility; assigned outside of the class. + DEFAULT: typing.ClassVar[Retry] + + def __init__( + self, + total: bool | int | None = 10, + connect: int | None = None, + read: int | None = None, + redirect: bool | int | None = None, + status: int | None = None, + other: int | None = None, + allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS, + status_forcelist: typing.Collection[int] | None = None, + backoff_factor: float = 0, + backoff_max: float = DEFAULT_BACKOFF_MAX, + raise_on_redirect: bool = True, + raise_on_status: bool = True, + history: tuple[RequestHistory, ...] | None = None, + respect_retry_after_header: bool = True, + remove_headers_on_redirect: typing.Collection[ + str + ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT, + backoff_jitter: float = 0.0, + ) -> None: + self.total = total + self.connect = connect + self.read = read + self.status = status + self.other = other + + if redirect is False or total is False: + redirect = 0 + raise_on_redirect = False + + self.redirect = redirect + self.status_forcelist = status_forcelist or set() + self.allowed_methods = allowed_methods + self.backoff_factor = backoff_factor + self.backoff_max = backoff_max + self.raise_on_redirect = raise_on_redirect + self.raise_on_status = raise_on_status + self.history = history or () + self.respect_retry_after_header = respect_retry_after_header + self.remove_headers_on_redirect = frozenset( + h.lower() for h in remove_headers_on_redirect + ) + self.backoff_jitter = backoff_jitter + + def new(self, **kw: typing.Any) -> Retry: + params = dict( + total=self.total, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, + other=self.other, + allowed_methods=self.allowed_methods, + status_forcelist=self.status_forcelist, + backoff_factor=self.backoff_factor, + backoff_max=self.backoff_max, + raise_on_redirect=self.raise_on_redirect, + raise_on_status=self.raise_on_status, + history=self.history, + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, + backoff_jitter=self.backoff_jitter, + ) + + params.update(kw) + return type(self)(**params) # type: ignore[arg-type] + + @classmethod + def from_int( + cls, + retries: Retry | bool | int | None, + redirect: bool | int | None = True, + default: Retry | bool | int | None = None, + ) -> Retry: + """Backwards-compatibility for the old retries format.""" + if retries is None: + retries = default if default is not None else cls.DEFAULT + + if isinstance(retries, Retry): + return retries + + redirect = bool(redirect) and None + new_retries = cls(retries, redirect=redirect) + log.debug("Converted retries value: %r -> %r", retries, new_retries) + return new_retries + + def get_backoff_time(self) -> float: + """Formula for computing the current backoff + + :rtype: float + """ + # We want to consider only the last consecutive errors sequence (Ignore redirects). + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) + if consecutive_errors_len <= 1: + return 0 + + backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1)) + if self.backoff_jitter != 0.0: + backoff_value += random.random() * self.backoff_jitter + return float(max(0, min(self.backoff_max, backoff_value))) + + def parse_retry_after(self, retry_after: str) -> float: + seconds: float + # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4 + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = int(retry_after) + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + raise InvalidHeader(f"Invalid Retry-After header: {retry_after}") + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + seconds = max(seconds, 0) + + return seconds + + def get_retry_after(self, response: BaseHTTPResponse) -> float | None: + """Get the value of Retry-After in seconds.""" + + retry_after = response.headers.get("Retry-After") + + if retry_after is None: + return None + + return self.parse_retry_after(retry_after) + + def sleep_for_retry(self, response: BaseHTTPResponse) -> bool: + retry_after = self.get_retry_after(response) + if retry_after: + time.sleep(retry_after) + return True + + return False + + def _sleep_backoff(self) -> None: + backoff = self.get_backoff_time() + if backoff <= 0: + return + time.sleep(backoff) + + def sleep(self, response: BaseHTTPResponse | None = None) -> None: + """Sleep between retry attempts. + + This method will respect a server's ``Retry-After`` response header + and sleep the duration of the time requested. If that is not present, it + will use an exponential backoff. By default, the backoff factor is 0 and + this method will return immediately. + """ + + if self.respect_retry_after_header and response: + slept = self.sleep_for_retry(response) + if slept: + return + + self._sleep_backoff() + + def _is_connection_error(self, err: Exception) -> bool: + """Errors when we're fairly sure that the server did not receive the + request, so it should be safe to retry. + """ + if isinstance(err, ProxyError): + err = err.original_error + return isinstance(err, ConnectTimeoutError) + + def _is_read_error(self, err: Exception) -> bool: + """Errors that occur after the request has been started, so we should + assume that the server began processing it. + """ + return isinstance(err, (ReadTimeoutError, ProtocolError)) + + def _is_method_retryable(self, method: str) -> bool: + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods + """ + if self.allowed_methods and method.upper() not in self.allowed_methods: + return False + return True + + def is_retry( + self, method: str, status_code: int, has_retry_after: bool = False + ) -> bool: + """Is this method/status code retryable? (Based on allowlists and control + variables such as the number of total retries to allow, whether to + respect the Retry-After header, whether this header is present, and + whether the returned status code is on the list of status codes to + be retried upon on the presence of the aforementioned header) + """ + if not self._is_method_retryable(method): + return False + + if self.status_forcelist and status_code in self.status_forcelist: + return True + + return bool( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) + + def is_exhausted(self) -> bool: + """Are we out of retries?""" + retry_counts = [ + x + for x in ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) + if x + ] + if not retry_counts: + return False + + return min(retry_counts) < 0 + + def increment( + self, + method: str | None = None, + url: str | None = None, + response: BaseHTTPResponse | None = None, + error: Exception | None = None, + _pool: ConnectionPool | None = None, + _stacktrace: TracebackType | None = None, + ) -> Retry: + """Return a new Retry object with incremented retry counters. + + :param response: A response object, or None, if the server did not + return a response. + :type response: :class:`~urllib3.response.BaseHTTPResponse` + :param Exception error: An error encountered during the request, or + None if the response was received successfully. + + :return: A new ``Retry`` object. + """ + if self.total is False and error: + # Disabled, indicate to re-raise the error. + raise reraise(type(error), error, _stacktrace) + + total = self.total + if total is not None: + total -= 1 + + connect = self.connect + read = self.read + redirect = self.redirect + status_count = self.status + other = self.other + cause = "unknown" + status = None + redirect_location = None + + if error and self._is_connection_error(error): + # Connect retry? + if connect is False: + raise reraise(type(error), error, _stacktrace) + elif connect is not None: + connect -= 1 + + elif error and self._is_read_error(error): + # Read retry? + if read is False or method is None or not self._is_method_retryable(method): + raise reraise(type(error), error, _stacktrace) + elif read is not None: + read -= 1 + + elif error: + # Other retry? + if other is not None: + other -= 1 + + elif response and response.get_redirect_location(): + # Redirect retry? + if redirect is not None: + redirect -= 1 + cause = "too many redirects" + response_redirect_location = response.get_redirect_location() + if response_redirect_location: + redirect_location = response_redirect_location + status = response.status + + else: + # Incrementing because of a server error like a 500 in + # status_forcelist and the given method is in the allowed_methods + cause = ResponseError.GENERIC_ERROR + if response and response.status: + if status_count is not None: + status_count -= 1 + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) + status = response.status + + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) + + new_retry = self.new( + total=total, + connect=connect, + read=read, + redirect=redirect, + status=status_count, + other=other, + history=history, + ) + + if new_retry.is_exhausted(): + reason = error or ResponseError(cause) + raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type] + + log.debug("Incremented Retry for (url='%s'): %r", url, new_retry) + + return new_retry + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(total={self.total}, connect={self.connect}, " + f"read={self.read}, redirect={self.redirect}, status={self.status})" + ) + + +# For backwards compatibility (equivalent to pre-v1.9): +Retry.DEFAULT = Retry(3) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_.py new file mode 100644 index 0000000000000000000000000000000000000000..e35e3940301c3f92c3dadc086b5bb6b6cb1f31ec --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_.py @@ -0,0 +1,515 @@ +from __future__ import annotations + +import hmac +import os +import socket +import sys +import typing +import warnings +from binascii import unhexlify +from hashlib import md5, sha1, sha256 + +from ..exceptions import ProxySchemeUnsupported, SSLError +from .url import _BRACELESS_IPV6_ADDRZ_RE, _IPV4_RE + +SSLContext = None +SSLTransport = None +HAS_NEVER_CHECK_COMMON_NAME = False +IS_PYOPENSSL = False +IS_SECURETRANSPORT = False +ALPN_PROTOCOLS = ["http/1.1"] + +_TYPE_VERSION_INFO = typing.Tuple[int, int, int, str, int] + +# Maps the length of a digest to a possible hash function producing this digest +HASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256} + + +def _is_bpo_43522_fixed( + implementation_name: str, + version_info: _TYPE_VERSION_INFO, + pypy_version_info: _TYPE_VERSION_INFO | None, +) -> bool: + """Return True for CPython 3.8.9+, 3.9.3+ or 3.10+ and PyPy 7.3.8+ where + setting SSLContext.hostname_checks_common_name to False works. + + Outside of CPython and PyPy we don't know which implementations work + or not so we conservatively use our hostname matching as we know that works + on all implementations. + + https://github.com/urllib3/urllib3/issues/2192#issuecomment-821832963 + https://foss.heptapod.net/pypy/pypy/-/issues/3539 + """ + if implementation_name == "pypy": + # https://foss.heptapod.net/pypy/pypy/-/issues/3129 + return pypy_version_info >= (7, 3, 8) and version_info >= (3, 8) # type: ignore[operator] + elif implementation_name == "cpython": + major_minor = version_info[:2] + micro = version_info[2] + return ( + (major_minor == (3, 8) and micro >= 9) + or (major_minor == (3, 9) and micro >= 3) + or major_minor >= (3, 10) + ) + else: # Defensive: + return False + + +def _is_has_never_check_common_name_reliable( + openssl_version: str, + openssl_version_number: int, + implementation_name: str, + version_info: _TYPE_VERSION_INFO, + pypy_version_info: _TYPE_VERSION_INFO | None, +) -> bool: + # As of May 2023, all released versions of LibreSSL fail to reject certificates with + # only common names, see https://github.com/urllib3/urllib3/pull/3024 + is_openssl = openssl_version.startswith("OpenSSL ") + # Before fixing OpenSSL issue #14579, the SSL_new() API was not copying hostflags + # like X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, which tripped up CPython. + # https://github.com/openssl/openssl/issues/14579 + # This was released in OpenSSL 1.1.1l+ (>=0x101010cf) + is_openssl_issue_14579_fixed = openssl_version_number >= 0x101010CF + + return is_openssl and ( + is_openssl_issue_14579_fixed + or _is_bpo_43522_fixed(implementation_name, version_info, pypy_version_info) + ) + + +if typing.TYPE_CHECKING: + from ssl import VerifyMode + + from typing_extensions import Literal, TypedDict + + from .ssltransport import SSLTransport as SSLTransportType + + class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False): + subjectAltName: tuple[tuple[str, str], ...] + subject: tuple[tuple[tuple[str, str], ...], ...] + serialNumber: str + + +# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X' +_SSL_VERSION_TO_TLS_VERSION: dict[int, int] = {} + +try: # Do we have ssl at all? + import ssl + from ssl import ( # type: ignore[assignment] + CERT_REQUIRED, + HAS_NEVER_CHECK_COMMON_NAME, + OP_NO_COMPRESSION, + OP_NO_TICKET, + OPENSSL_VERSION, + OPENSSL_VERSION_NUMBER, + PROTOCOL_TLS, + PROTOCOL_TLS_CLIENT, + OP_NO_SSLv2, + OP_NO_SSLv3, + SSLContext, + TLSVersion, + ) + + PROTOCOL_SSLv23 = PROTOCOL_TLS + + # Setting SSLContext.hostname_checks_common_name = False didn't work before CPython + # 3.8.9, 3.9.3, and 3.10 (but OK on PyPy) or OpenSSL 1.1.1l+ + if HAS_NEVER_CHECK_COMMON_NAME and not _is_has_never_check_common_name_reliable( + OPENSSL_VERSION, + OPENSSL_VERSION_NUMBER, + sys.implementation.name, + sys.version_info, + sys.pypy_version_info if sys.implementation.name == "pypy" else None, # type: ignore[attr-defined] + ): + HAS_NEVER_CHECK_COMMON_NAME = False + + # Need to be careful here in case old TLS versions get + # removed in future 'ssl' module implementations. + for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"): + try: + _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr( + TLSVersion, attr + ) + except AttributeError: # Defensive: + continue + + from .ssltransport import SSLTransport # type: ignore[assignment] +except ImportError: + OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment] + OP_NO_TICKET = 0x4000 # type: ignore[assignment] + OP_NO_SSLv2 = 0x1000000 # type: ignore[assignment] + OP_NO_SSLv3 = 0x2000000 # type: ignore[assignment] + PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 # type: ignore[assignment] + PROTOCOL_TLS_CLIENT = 16 # type: ignore[assignment] + + +_TYPE_PEER_CERT_RET = typing.Union["_TYPE_PEER_CERT_RET_DICT", bytes, None] + + +def assert_fingerprint(cert: bytes | None, fingerprint: str) -> None: + """ + Checks if given fingerprint matches the supplied certificate. + + :param cert: + Certificate as bytes object. + :param fingerprint: + Fingerprint as string of hexdigits, can be interspersed by colons. + """ + + if cert is None: + raise SSLError("No certificate for the peer.") + + fingerprint = fingerprint.replace(":", "").lower() + digest_length = len(fingerprint) + hashfunc = HASHFUNC_MAP.get(digest_length) + if not hashfunc: + raise SSLError(f"Fingerprint of invalid length: {fingerprint}") + + # We need encode() here for py32; works on py2 and p33. + fingerprint_bytes = unhexlify(fingerprint.encode()) + + cert_digest = hashfunc(cert).digest() + + if not hmac.compare_digest(cert_digest, fingerprint_bytes): + raise SSLError( + f'Fingerprints did not match. Expected "{fingerprint}", got "{cert_digest.hex()}"' + ) + + +def resolve_cert_reqs(candidate: None | int | str) -> VerifyMode: + """ + Resolves the argument to a numeric constant, which can be passed to + the wrap_socket function/method from the ssl module. + Defaults to :data:`ssl.CERT_REQUIRED`. + If given a string it is assumed to be the name of the constant in the + :mod:`ssl` module or its abbreviation. + (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. + If it's neither `None` nor a string we assume it is already the numeric + constant which can directly be passed to wrap_socket. + """ + if candidate is None: + return CERT_REQUIRED + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "CERT_" + candidate) + return res # type: ignore[no-any-return] + + return candidate # type: ignore[return-value] + + +def resolve_ssl_version(candidate: None | int | str) -> int: + """ + like resolve_cert_reqs + """ + if candidate is None: + return PROTOCOL_TLS + + if isinstance(candidate, str): + res = getattr(ssl, candidate, None) + if res is None: + res = getattr(ssl, "PROTOCOL_" + candidate) + return typing.cast(int, res) + + return candidate + + +def create_urllib3_context( + ssl_version: int | None = None, + cert_reqs: int | None = None, + options: int | None = None, + ciphers: str | None = None, + ssl_minimum_version: int | None = None, + ssl_maximum_version: int | None = None, +) -> ssl.SSLContext: + """Creates and configures an :class:`ssl.SSLContext` instance for use with urllib3. + + :param ssl_version: + The desired protocol version to use. This will default to + PROTOCOL_SSLv23 which will negotiate the highest protocol that both + the server and your installation of OpenSSL support. + + This parameter is deprecated instead use 'ssl_minimum_version'. + :param ssl_minimum_version: + The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + :param ssl_maximum_version: + The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value. + Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the + default value. + :param cert_reqs: + Whether to require the certificate verification. This defaults to + ``ssl.CERT_REQUIRED``. + :param options: + Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. + :param ciphers: + Which cipher suites to allow the server to select. Defaults to either system configured + ciphers if OpenSSL 1.1.1+, otherwise uses a secure default set of ciphers. + :returns: + Constructed SSLContext object with specified options + :rtype: SSLContext + """ + if SSLContext is None: + raise TypeError("Can't create an SSLContext object without an ssl module") + + # This means 'ssl_version' was specified as an exact value. + if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT): + # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version' + # to avoid conflicts. + if ssl_minimum_version is not None or ssl_maximum_version is not None: + raise ValueError( + "Can't specify both 'ssl_version' and either " + "'ssl_minimum_version' or 'ssl_maximum_version'" + ) + + # 'ssl_version' is deprecated and will be removed in the future. + else: + # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead. + ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MINIMUM_SUPPORTED + ) + ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get( + ssl_version, TLSVersion.MAXIMUM_SUPPORTED + ) + + # This warning message is pushing users to use 'ssl_minimum_version' + # instead of both min/max. Best practice is to only set the minimum version and + # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED' + warnings.warn( + "'ssl_version' option is deprecated and will be " + "removed in urllib3 v2.1.0. Instead use 'ssl_minimum_version'", + category=DeprecationWarning, + stacklevel=2, + ) + + # PROTOCOL_TLS is deprecated in Python 3.10 so we always use PROTOCOL_TLS_CLIENT + context = SSLContext(PROTOCOL_TLS_CLIENT) + + if ssl_minimum_version is not None: + context.minimum_version = ssl_minimum_version + else: # Python <3.10 defaults to 'MINIMUM_SUPPORTED' so explicitly set TLSv1.2 here + context.minimum_version = TLSVersion.TLSv1_2 + + if ssl_maximum_version is not None: + context.maximum_version = ssl_maximum_version + + # Unless we're given ciphers defer to either system ciphers in + # the case of OpenSSL 1.1.1+ or use our own secure default ciphers. + if ciphers: + context.set_ciphers(ciphers) + + # Setting the default here, as we may have no ssl module on import + cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs + + if options is None: + options = 0 + # SSLv2 is easily broken and is considered harmful and dangerous + options |= OP_NO_SSLv2 + # SSLv3 has several problems and is now dangerous + options |= OP_NO_SSLv3 + # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (issue #309) + options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET + + context.options |= options + + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older + # versions of Python. We only enable on Python 3.7.4+ or if certificate + # verification is enabled to work around Python issue #37428 + # See: https://bugs.python.org/issue37428 + if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr( + context, "post_handshake_auth", None + ) is not None: + context.post_handshake_auth = True + + # The order of the below lines setting verify_mode and check_hostname + # matter due to safe-guards SSLContext has to prevent an SSLContext with + # check_hostname=True, verify_mode=NONE/OPTIONAL. + # We always set 'check_hostname=False' for pyOpenSSL so we rely on our own + # 'ssl.match_hostname()' implementation. + if cert_reqs == ssl.CERT_REQUIRED and not IS_PYOPENSSL: + context.verify_mode = cert_reqs + context.check_hostname = True + else: + context.check_hostname = False + context.verify_mode = cert_reqs + + try: + context.hostname_checks_common_name = False + except AttributeError: # Defensive: for CPython < 3.8.9 and 3.9.3; for PyPy < 7.3.8 + pass + + # Enable logging of TLS session keys via defacto standard environment variable + # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values. + if hasattr(context, "keylog_filename"): + sslkeylogfile = os.environ.get("SSLKEYLOGFILE") + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + + return context + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: Literal[False] = ..., +) -> ssl.SSLSocket: + ... + + +@typing.overload +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = ..., + certfile: str | None = ..., + cert_reqs: int | None = ..., + ca_certs: str | None = ..., + server_hostname: str | None = ..., + ssl_version: int | None = ..., + ciphers: str | None = ..., + ssl_context: ssl.SSLContext | None = ..., + ca_cert_dir: str | None = ..., + key_password: str | None = ..., + ca_cert_data: None | str | bytes = ..., + tls_in_tls: bool = ..., +) -> ssl.SSLSocket | SSLTransportType: + ... + + +def ssl_wrap_socket( + sock: socket.socket, + keyfile: str | None = None, + certfile: str | None = None, + cert_reqs: int | None = None, + ca_certs: str | None = None, + server_hostname: str | None = None, + ssl_version: int | None = None, + ciphers: str | None = None, + ssl_context: ssl.SSLContext | None = None, + ca_cert_dir: str | None = None, + key_password: str | None = None, + ca_cert_data: None | str | bytes = None, + tls_in_tls: bool = False, +) -> ssl.SSLSocket | SSLTransportType: + """ + All arguments except for server_hostname, ssl_context, tls_in_tls, ca_cert_data and + ca_cert_dir have the same meaning as they do when using + :func:`ssl.create_default_context`, :meth:`ssl.SSLContext.load_cert_chain`, + :meth:`ssl.SSLContext.set_ciphers` and :meth:`ssl.SSLContext.wrap_socket`. + + :param server_hostname: + When SNI is supported, the expected hostname of the certificate + :param ssl_context: + A pre-made :class:`SSLContext` object. If none is provided, one will + be created using :func:`create_urllib3_context`. + :param ciphers: + A string of ciphers we wish the client to support. + :param ca_cert_dir: + A directory containing CA certificates in multiple separate files, as + supported by OpenSSL's -CApath flag or the capath argument to + SSLContext.load_verify_locations(). + :param key_password: + Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. + """ + context = ssl_context + if context is None: + # Note: This branch of code and all the variables in it are only used in tests. + # We should consider deprecating and removing this code. + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) + + if ca_certs or ca_cert_dir or ca_cert_data: + try: + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) + except OSError as e: + raise SSLError(e) from e + + elif ssl_context is None and hasattr(context, "load_default_certs"): + # try to load OS default certs; works well on Windows. + context.load_default_certs() + + # Attempt to detect if we get the goofy behavior of the + # keyfile being encrypted and OpenSSL asking for the + # passphrase via the terminal and instead error out. + if keyfile and key_password is None and _is_key_file_encrypted(keyfile): + raise SSLError("Client private key is encrypted, password is required") + + if certfile: + if key_password is None: + context.load_cert_chain(certfile, keyfile) + else: + context.load_cert_chain(certfile, keyfile, key_password) + + try: + context.set_alpn_protocols(ALPN_PROTOCOLS) + except NotImplementedError: # Defensive: in CI, we always have set_alpn_protocols + pass + + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname) + return ssl_sock + + +def is_ipaddress(hostname: str | bytes) -> bool: + """Detects whether the hostname given is an IPv4 or IPv6 address. + Also detects IPv6 addresses with Zone IDs. + + :param str hostname: Hostname to examine. + :return: True if the hostname is an IP address, False otherwise. + """ + if isinstance(hostname, bytes): + # IDN A-label bytes are ASCII compatible. + hostname = hostname.decode("ascii") + return bool(_IPV4_RE.match(hostname) or _BRACELESS_IPV6_ADDRZ_RE.match(hostname)) + + +def _is_key_file_encrypted(key_file: str) -> bool: + """Detects if a key file is encrypted or not.""" + with open(key_file) as f: + for line in f: + # Look for Proc-Type: 4,ENCRYPTED + if "ENCRYPTED" in line: + return True + + return False + + +def _ssl_wrap_socket_impl( + sock: socket.socket, + ssl_context: ssl.SSLContext, + tls_in_tls: bool, + server_hostname: str | None = None, +) -> ssl.SSLSocket | SSLTransportType: + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_match_hostname.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_match_hostname.py new file mode 100644 index 0000000000000000000000000000000000000000..453cfd420d835be58b5af581c3065e7b37079ecf --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssl_match_hostname.py @@ -0,0 +1,159 @@ +"""The match_hostname() function from Python 3.5, essential when using SSL.""" + +# Note: This file is under the PSF license as the code comes from the python +# stdlib. http://docs.python.org/3/license.html +# It is modified to remove commonName support. + +from __future__ import annotations + +import ipaddress +import re +import typing +from ipaddress import IPv4Address, IPv6Address + +if typing.TYPE_CHECKING: + from .ssl_ import _TYPE_PEER_CERT_RET_DICT + +__version__ = "3.5.0.1" + + +class CertificateError(ValueError): + pass + + +def _dnsname_match( + dn: typing.Any, hostname: str, max_wildcards: int = 1 +) -> typing.Match[str] | None | bool: + """Matching according to RFC 6125, section 6.4.3 + + http://tools.ietf.org/html/rfc6125#section-6.4.3 + """ + pats = [] + if not dn: + return False + + # Ported from python3-syntax: + # leftmost, *remainder = dn.split(r'.') + parts = dn.split(r".") + leftmost = parts[0] + remainder = parts[1:] + + wildcards = leftmost.count("*") + if wildcards > max_wildcards: + # Issue #17980: avoid denials of service by refusing more + # than one wildcard per fragment. A survey of established + # policy among SSL implementations showed it to be a + # reasonable choice. + raise CertificateError( + "too many wildcards in certificate DNS name: " + repr(dn) + ) + + # speed up common case w/o wildcards + if not wildcards: + return bool(dn.lower() == hostname.lower()) + + # RFC 6125, section 6.4.3, subitem 1. + # The client SHOULD NOT attempt to match a presented identifier in which + # the wildcard character comprises a label other than the left-most label. + if leftmost == "*": + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): + # RFC 6125, section 6.4.3, subitem 3. + # The client SHOULD NOT attempt to match a presented identifier + # where the wildcard character is embedded within an A-label or + # U-label of an internationalized domain name. + pats.append(re.escape(leftmost)) + else: + # Otherwise, '*' matches any dotless string, e.g. www* + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) + + # add the remaining fragments, ignore any wildcards + for frag in remainder: + pats.append(re.escape(frag)) + + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) + return pat.match(hostname) + + +def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: + """Exact matching of IP addresses. + + RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded + bytes of the IP address. An IP version 4 address is 4 octets, and an IP + version 6 address is 16 octets. [...] A reference identity of type IP-ID + matches if the address is identical to an iPAddress value of the + subjectAltName extension of the certificate." + """ + # OpenSSL may add a trailing newline to a subjectAltName's IP address + # Divergence from upstream: ipaddress can't handle byte str + ip = ipaddress.ip_address(ipname.rstrip()) + return bool(ip.packed == host_ip.packed) + + +def match_hostname( + cert: _TYPE_PEER_CERT_RET_DICT | None, + hostname: str, + hostname_checks_common_name: bool = False, +) -> None: + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 + rules are followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) + try: + # Divergence from upstream: ipaddress can't handle byte str + # + # The ipaddress module shipped with Python < 3.9 does not support + # scoped IPv6 addresses so we unconditionally strip the Zone IDs for + # now. Once we drop support for Python 3.9 we can remove this branch. + if "%" in hostname: + host_ip = ipaddress.ip_address(hostname[: hostname.rfind("%")]) + else: + host_ip = ipaddress.ip_address(hostname) + + except ValueError: + # Not an IP address (common case) + host_ip = None + dnsnames = [] + san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) + key: str + value: str + for key, value in san: + if key == "DNS": + if host_ip is None and _dnsname_match(value, hostname): + return + dnsnames.append(value) + elif key == "IP Address": + if host_ip is not None and _ipaddress_match(value, host_ip): + return + dnsnames.append(value) + + # We only check 'commonName' if it's enabled and we're not verifying + # an IP address. IP addresses aren't valid within 'commonName'. + if hostname_checks_common_name and host_ip is None and not dnsnames: + for sub in cert.get("subject", ()): + for key, value in sub: + if key == "commonName": + if _dnsname_match(value, hostname): + return + dnsnames.append(value) + + if len(dnsnames) > 1: + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) + elif len(dnsnames) == 1: + raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") + else: + raise CertificateError("no appropriate subjectAltName fields were found") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssltransport.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssltransport.py new file mode 100644 index 0000000000000000000000000000000000000000..5ec86473b47b0d07ee6dc9eab4e02abaf6dc7ee1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/ssltransport.py @@ -0,0 +1,280 @@ +from __future__ import annotations + +import io +import socket +import ssl +import typing + +from ..exceptions import ProxySchemeUnsupported + +if typing.TYPE_CHECKING: + from typing_extensions import Literal + + from .ssl_ import _TYPE_PEER_CERT_RET, _TYPE_PEER_CERT_RET_DICT + + +_SelfT = typing.TypeVar("_SelfT", bound="SSLTransport") +_WriteBuffer = typing.Union[bytearray, memoryview] +_ReturnValue = typing.TypeVar("_ReturnValue") + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context: ssl.SSLContext) -> None: + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, + socket: socket.socket, + ssl_context: ssl.SSLContext, + server_hostname: str | None = None, + suppress_ragged_eofs: bool = True, + ) -> None: + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self: _SelfT) -> _SelfT: + return self + + def __exit__(self, *_: typing.Any) -> None: + self.close() + + def fileno(self) -> int: + return self.socket.fileno() + + def read(self, len: int = 1024, buffer: typing.Any | None = None) -> int | bytes: + return self._wrap_ssl_read(len, buffer) + + def recv(self, buflen: int = 1024, flags: int = 0) -> int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(buflen) + + def recv_into( + self, + buffer: _WriteBuffer, + nbytes: int | None = None, + flags: int = 0, + ) -> None | int | bytes: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if nbytes is None: + nbytes = len(buffer) + return self.read(nbytes, buffer) + + def sendall(self, data: bytes, flags: int = 0) -> None: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data: bytes, flags: int = 0) -> int: + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + return self._ssl_io_loop(self.sslobj.write, data) + + def makefile( + self, + mode: str, + buffering: int | None = None, + *, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> typing.BinaryIO | typing.TextIO | socket.SocketIO: + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError(f"invalid mode {mode!r} (only r, w, b allowed)") + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) # type: ignore[arg-type] + self.socket._io_refs += 1 # type: ignore[attr-defined] + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + buffer: typing.BinaryIO + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) # type: ignore[assignment] + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode # type: ignore[misc] + return text + + def unwrap(self) -> None: + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self) -> None: + self.socket.close() + + @typing.overload + def getpeercert( + self, binary_form: Literal[False] = ... + ) -> _TYPE_PEER_CERT_RET_DICT | None: + ... + + @typing.overload + def getpeercert(self, binary_form: Literal[True]) -> bytes | None: + ... + + def getpeercert(self, binary_form: bool = False) -> _TYPE_PEER_CERT_RET: + return self.sslobj.getpeercert(binary_form) # type: ignore[return-value] + + def version(self) -> str | None: + return self.sslobj.version() + + def cipher(self) -> tuple[str, str, int] | None: + return self.sslobj.cipher() + + def selected_alpn_protocol(self) -> str | None: + return self.sslobj.selected_alpn_protocol() + + def selected_npn_protocol(self) -> str | None: + return self.sslobj.selected_npn_protocol() + + def shared_ciphers(self) -> list[tuple[str, str, int]] | None: + return self.sslobj.shared_ciphers() + + def compression(self) -> str | None: + return self.sslobj.compression() + + def settimeout(self, value: float | None) -> None: + self.socket.settimeout(value) + + def gettimeout(self) -> float | None: + return self.socket.gettimeout() + + def _decref_socketios(self) -> None: + self.socket._decref_socketios() # type: ignore[attr-defined] + + def _wrap_ssl_read(self, len: int, buffer: bytearray | None = None) -> int | bytes: + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + # func is sslobj.do_handshake or sslobj.unwrap + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[], None]) -> None: + ... + + # func is sslobj.write, arg1 is data + @typing.overload + def _ssl_io_loop(self, func: typing.Callable[[bytes], int], arg1: bytes) -> int: + ... + + # func is sslobj.read, arg1 is len, arg2 is buffer + @typing.overload + def _ssl_io_loop( + self, + func: typing.Callable[[int, bytearray | None], bytes], + arg1: int, + arg2: bytearray | None, + ) -> bytes: + ... + + def _ssl_io_loop( + self, + func: typing.Callable[..., _ReturnValue], + arg1: None | bytes | int = None, + arg2: bytearray | None = None, + ) -> _ReturnValue: + """Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + if arg1 is None and arg2 is None: + ret = func() + elif arg2 is None: + ret = func(arg1) + else: + ret = func(arg1, arg2) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return typing.cast(_ReturnValue, ret) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/timeout.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/timeout.py new file mode 100644 index 0000000000000000000000000000000000000000..ec090f69cc96810c35e6c50cdc179a4a83c982b7 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/timeout.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import time +import typing +from enum import Enum +from socket import getdefaulttimeout + +from ..exceptions import TimeoutStateError + +if typing.TYPE_CHECKING: + from typing_extensions import Final + + +class _TYPE_DEFAULT(Enum): + # This value should never be passed to socket.settimeout() so for safety we use a -1. + # socket.settimout() raises a ValueError for negative values. + token = -1 + + +_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token + +_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]] + + +class Timeout: + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + import urllib3 + + timeout = urllib3.util.Timeout(connect=2.0, read=7.0) + + http = urllib3.PoolManager(timeout=timeout) + + resp = http.request("GET", "https://example.com/") + + print(resp.status) + + Or per-request (which overrides the default for the pool): + + .. code-block:: python + + response = http.request("GET", "https://example.com/", timeout=Timeout(10)) + + Timeouts can be disabled by setting all the parameters to ``None``: + + .. code-block:: python + + no_timeout = Timeout(connect=None, read=None) + response = http.request("GET", "https://example.com/", timeout=no_timeout) + + + :param total: + This combines the connect and read timeouts into one; the read timeout + will be set to the time leftover from the connect attempt. In the + event that both a connect timeout and a total are specified, or a read + timeout and a total are specified, the shorter timeout will be applied. + + Defaults to None. + + :type total: int, float, or None + + :param connect: + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py + `_. + None will set an infinite timeout for connection attempts. + + :type connect: int, float, or None + + :param read: + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py + `_. + None will set an infinite timeout. + + :type read: int, float, or None + + .. note:: + + Many factors can affect the total amount of time for urllib3 to return + an HTTP response. + + For example, Python's DNS resolver does not obey the timeout specified + on the socket. Other factors that can affect total request time include + high CPU load, high swap, the program running at a low priority level, + or other behaviors. + + In addition, the read and total timeouts only measure the time between + read operations on the socket connecting the client and the server, + not the total amount of time for the request to return a complete + response. For most requests, the timeout is raised because the server + has not sent the first byte in the specified time. This is not always + the case; if a server streams one byte every fifteen seconds, a timeout + of 20 seconds will not trigger, even though the request will take + several minutes to complete. + + If your goal is to cut off any request after a set amount of wall clock + time, consider having a second "watcher" thread to cut off a slow + request. + """ + + #: A sentinel object representing the default timeout value + DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT + + def __init__( + self, + total: _TYPE_TIMEOUT = None, + connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT, + ) -> None: + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") + self._start_connect: float | None = None + + def __repr__(self) -> str: + return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})" + + # __str__ provided for backwards compatibility + __str__ = __repr__ + + @staticmethod + def resolve_default_timeout(timeout: _TYPE_TIMEOUT) -> float | None: + return getdefaulttimeout() if timeout is _DEFAULT_TIMEOUT else timeout + + @classmethod + def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT: + """Check that a timeout attribute is valid. + + :param value: The timeout value to validate + :param name: The name of the timeout attribute to validate. This is + used to specify in error messages. + :return: The validated and casted version of the given value. + :raises ValueError: If it is a numeric value less than or equal to + zero, or the type is not an integer, float, or None. + """ + if value is None or value is _DEFAULT_TIMEOUT: + return value + + if isinstance(value, bool): + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) + try: + float(value) + except (TypeError, ValueError): + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + try: + if value <= 0: + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) + except TypeError: + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) from None + + return value + + @classmethod + def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout: + """Create a new Timeout from a legacy timeout value. + + The timeout value used by httplib.py sets the same timeout on the + connect(), and recv() socket requests. This creates a :class:`Timeout` + object that sets the individual timeouts to the ``timeout`` value + passed to this function. + + :param timeout: The legacy timeout value. + :type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None + :return: Timeout object + :rtype: :class:`Timeout` + """ + return Timeout(read=timeout, connect=timeout) + + def clone(self) -> Timeout: + """Create a copy of the timeout object + + Timeout properties are stored per-pool but each request needs a fresh + Timeout object to ensure each one has its own start/stop configured. + + :return: a copy of the timeout object + :rtype: :class:`Timeout` + """ + # We can't use copy.deepcopy because that will also create a new object + # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to + # detect the user default. + return Timeout(connect=self._connect, read=self._read, total=self.total) + + def start_connect(self) -> float: + """Start the timeout clock, used during a connect() attempt + + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to start a timer that has been started already. + """ + if self._start_connect is not None: + raise TimeoutStateError("Timeout timer has already been started.") + self._start_connect = time.monotonic() + return self._start_connect + + def get_connect_duration(self) -> float: + """Gets the time elapsed since the call to :meth:`start_connect`. + + :return: Elapsed time in seconds. + :rtype: float + :raises urllib3.exceptions.TimeoutStateError: if you attempt + to get duration for a timer that hasn't been started. + """ + if self._start_connect is None: + raise TimeoutStateError( + "Can't get connect duration for timer that has not started." + ) + return time.monotonic() - self._start_connect + + @property + def connect_timeout(self) -> _TYPE_TIMEOUT: + """Get the value to use when setting a connection timeout. + + This will be a positive float or integer, the value None + (never timeout), or the default system timeout. + + :return: Connect timeout. + :rtype: int, float, :attr:`Timeout.DEFAULT_TIMEOUT` or None + """ + if self.total is None: + return self._connect + + if self._connect is None or self._connect is _DEFAULT_TIMEOUT: + return self.total + + return min(self._connect, self.total) # type: ignore[type-var] + + @property + def read_timeout(self) -> float | None: + """Get the value for the read timeout. + + This assumes some time has elapsed in the connection timeout and + computes the read timeout appropriately. + + If self.total is set, the read timeout is dependent on the amount of + time taken by the connect timeout. If the connection time has not been + established, a :exc:`~urllib3.exceptions.TimeoutStateError` will be + raised. + + :return: Value to use for the read timeout. + :rtype: int, float or None + :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` + has not yet been called on this object. + """ + if ( + self.total is not None + and self.total is not _DEFAULT_TIMEOUT + and self._read is not None + and self._read is not _DEFAULT_TIMEOUT + ): + # In case the connect timeout has not yet been established. + if self._start_connect is None: + return self._read + return max(0, min(self.total - self.get_connect_duration(), self._read)) + elif self.total is not None and self.total is not _DEFAULT_TIMEOUT: + return max(0, self.total - self.get_connect_duration()) + else: + return self.resolve_default_timeout(self._read) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/url.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/url.py new file mode 100644 index 0000000000000000000000000000000000000000..d53ea932a0309181a4e07596c773f3765eb36977 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/url.py @@ -0,0 +1,471 @@ +from __future__ import annotations + +import re +import typing + +from ..exceptions import LocationParseError +from .util import to_str + +# We only want to normalize urls with an HTTP(S) scheme. +# urllib3 infers URLs without a scheme (None) to be http. +_NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +_PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +_SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +_URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^\\/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +_IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +_HEX_PAT = "[0-9A-Fa-f]{1,4}" +_LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=_HEX_PAT, ipv4=_IPV4_PAT) +_subs = {"hex": _HEX_PAT, "ls32": _LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +_UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._\-~" +_IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +_ZONE_ID_PAT = "(?:%25|%)(?:[" + _UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +_IPV6_ADDRZ_PAT = r"\[" + _IPV6_PAT + r"(?:" + _ZONE_ID_PAT + r")?\]" +_REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +_TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") + +_IPV4_RE = re.compile("^" + _IPV4_PAT + "$") +_IPV6_RE = re.compile("^" + _IPV6_PAT + "$") +_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT + "$") +_BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + _IPV6_ADDRZ_PAT[2:-2] + "$") +_ZONE_ID_RE = re.compile("(" + _ZONE_ID_PAT + r")\]$") + +_HOST_PORT_PAT = ("^(%s|%s|%s)(?::0*?(|0|[1-9][0-9]{0,4}))?$") % ( + _REG_NAME_PAT, + _IPV4_PAT, + _IPV6_ADDRZ_PAT, +) +_HOST_PORT_RE = re.compile(_HOST_PORT_PAT, re.UNICODE | re.DOTALL) + +_UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +_SUB_DELIM_CHARS = set("!$&'()*+,;=") +_USERINFO_CHARS = _UNRESERVED_CHARS | _SUB_DELIM_CHARS | {":"} +_PATH_CHARS = _USERINFO_CHARS | {"@", "/"} +_QUERY_CHARS = _FRAGMENT_CHARS = _PATH_CHARS | {"?"} + + +class Url( + typing.NamedTuple( + "Url", + [ + ("scheme", typing.Optional[str]), + ("auth", typing.Optional[str]), + ("host", typing.Optional[str]), + ("port", typing.Optional[int]), + ("path", typing.Optional[str]), + ("query", typing.Optional[str]), + ("fragment", typing.Optional[str]), + ], + ) +): + """ + Data structure for representing an HTTP URL. Used as a return value for + :func:`parse_url`. Both the scheme and host are normalized as they are + both case-insensitive according to RFC 3986. + """ + + def __new__( # type: ignore[no-untyped-def] + cls, + scheme: str | None = None, + auth: str | None = None, + host: str | None = None, + port: int | None = None, + path: str | None = None, + query: str | None = None, + fragment: str | None = None, + ): + if path and not path.startswith("/"): + path = "/" + path + if scheme is not None: + scheme = scheme.lower() + return super().__new__(cls, scheme, auth, host, port, path, query, fragment) + + @property + def hostname(self) -> str | None: + """For backwards-compatibility with urlparse. We're nice like that.""" + return self.host + + @property + def request_uri(self) -> str: + """Absolute path including the query string.""" + uri = self.path or "/" + + if self.query is not None: + uri += "?" + self.query + + return uri + + @property + def authority(self) -> str | None: + """ + Authority component as defined in RFC 3986 3.2. + This includes userinfo (auth), host and port. + + i.e. + userinfo@host:port + """ + userinfo = self.auth + netloc = self.netloc + if netloc is None or userinfo is None: + return netloc + else: + return f"{userinfo}@{netloc}" + + @property + def netloc(self) -> str | None: + """ + Network location including host and port. + + If you need the equivalent of urllib.parse's ``netloc``, + use the ``authority`` property instead. + """ + if self.host is None: + return None + if self.port: + return f"{self.host}:{self.port}" + return self.host + + @property + def url(self) -> str: + """ + Convert self into a url + + This function should more or less round-trip with :func:`.parse_url`. The + returned url may not be exactly the same as the url inputted to + :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls + with a blank port will have : removed). + + Example: + + .. code-block:: python + + import urllib3 + + U = urllib3.util.parse_url("https://google.com/mail/") + + print(U.url) + # "https://google.com/mail/" + + print( urllib3.util.Url("https", "username:password", + "host.com", 80, "/path", "query", "fragment" + ).url + ) + # "https://username:password@host.com:80/path?query#fragment" + """ + scheme, auth, host, port, path, query, fragment = self + url = "" + + # We use "is not None" we want things to happen with empty strings (or 0 port) + if scheme is not None: + url += scheme + "://" + if auth is not None: + url += auth + "@" + if host is not None: + url += host + if port is not None: + url += ":" + str(port) + if path is not None: + url += path + if query is not None: + url += "?" + query + if fragment is not None: + url += "#" + fragment + + return url + + def __str__(self) -> str: + return self.url + + +@typing.overload +def _encode_invalid_chars( + component: str, allowed_chars: typing.Container[str] +) -> str: # Abstract + ... + + +@typing.overload +def _encode_invalid_chars( + component: None, allowed_chars: typing.Container[str] +) -> None: # Abstract + ... + + +def _encode_invalid_chars( + component: str | None, allowed_chars: typing.Container[str] +) -> str | None: + """Percent-encodes a URI component without reapplying + onto an already percent-encoded component. + """ + if component is None: + return component + + component = to_str(component) + + # Normalize existing percent-encoded bytes. + # Try to see if the component we're encoding is already percent-encoded + # so we can skip all '%' characters but still encode all others. + component, percent_encodings = _PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") + encoded_component = bytearray() + + for i in range(0, len(uri_bytes)): + # Will return a single character bytestring + byte = uri_bytes[i : i + 1] + byte_ord = ord(byte) + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): + encoded_component += byte + continue + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) + + return encoded_component.decode() + + +def _remove_path_dot_segments(path: str) -> str: + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + if segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +@typing.overload +def _normalize_host(host: None, scheme: str | None) -> None: + ... + + +@typing.overload +def _normalize_host(host: str, scheme: str | None) -> str: + ... + + +def _normalize_host(host: str | None, scheme: str | None) -> str | None: + if host: + if scheme in _NORMALIZABLE_SCHEMES: + is_ipv6 = _IPV6_ADDRZ_RE.match(host) + if is_ipv6: + # IPv6 hosts of the form 'a::b%zone' are encoded in a URL as + # such per RFC 6874: 'a::b%25zone'. Unquote the ZoneID + # separator as necessary to return a valid RFC 4007 scoped IP. + match = _ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = _encode_invalid_chars(zone_id, _UNRESERVED_CHARS) + return f"{host[:start].lower()}%{zone_id}{host[end:]}" + else: + return host.lower() + elif not _IPV4_RE.match(host): + return to_str( + b".".join([_idna_encode(label) for label in host.split(".")]), + "ascii", + ) + return host + + +def _idna_encode(name: str) -> bytes: + if not name.isascii(): + try: + import idna + except ImportError: + raise LocationParseError( + "Unable to parse URL without the 'idna' module" + ) from None + + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + raise LocationParseError( + f"Name '{name}' is not a valid IDNA label" + ) from None + + return name.lower().encode("ascii") + + +def _encode_target(target: str) -> str: + """Percent-encodes a request target so that there are no invalid characters + + Pre-condition for this function is that 'target' must start with '/'. + If that is the case then _TARGET_RE will always produce a match. + """ + match = _TARGET_RE.match(target) + if not match: # Defensive: + raise LocationParseError(f"{target!r} is not a valid request URI") + + path, query = match.groups() + encoded_target = _encode_invalid_chars(path, _PATH_CHARS) + if query is not None: + query = _encode_invalid_chars(query, _QUERY_CHARS) + encoded_target += "?" + query + return encoded_target + + +def parse_url(url: str) -> Url: + """ + Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is + performed to parse incomplete urls. Fields not provided will be None. + This parser is RFC 3986 and RFC 6874 compliant. + + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + + :param str url: URL to parse into a :class:`.Url` namedtuple. + + Partly backwards-compatible with :mod:`urllib.parse`. + + Example: + + .. code-block:: python + + import urllib3 + + print( urllib3.util.parse_url('http://google.com/mail/')) + # Url(scheme='http', host='google.com', port=None, path='/mail/', ...) + + print( urllib3.util.parse_url('google.com:80')) + # Url(scheme=None, host='google.com', port=80, path=None, ...) + + print( urllib3.util.parse_url('/foo?bar')) + # Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...) + """ + if not url: + # Empty + return Url() + + source_url = url + if not _SCHEME_RE.search(url): + url = "//" + url + + scheme: str | None + authority: str | None + auth: str | None + host: str | None + port: str | None + port_int: int | None + path: str | None + query: str | None + fragment: str | None + + try: + scheme, authority, path, query, fragment = _URI_RE.match(url).groups() # type: ignore[union-attr] + normalize_uri = scheme is None or scheme.lower() in _NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, _, host_port = authority.rpartition("@") + auth = auth or None + host, port = _HOST_PORT_RE.match(host_port).groups() # type: ignore[union-attr] + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, _USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port_int = int(port) + if not (0 <= port_int <= 65535): + raise LocationParseError(url) + else: + port_int = None + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, _PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, _QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, _FRAGMENT_CHARS) + + except (ValueError, AttributeError) as e: + raise LocationParseError(source_url) from e + + # For the sake of backwards compatibility we put empty + # string values for path if there are any defined values + # beyond the path in the URL. + # TODO: Remove this when we break backwards compatibility. + if not path: + if query is not None or fragment is not None: + path = "" + else: + path = None + + return Url( + scheme=scheme, + auth=auth, + host=host, + port=port_int, + path=path, + query=query, + fragment=fragment, + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/util.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/util.py new file mode 100644 index 0000000000000000000000000000000000000000..35c77e4025842f548565334a3c04cba90f9283d6 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/util.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import typing +from types import TracebackType + + +def to_bytes( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> bytes: + if isinstance(x, bytes): + return x + elif not isinstance(x, str): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.encode(encoding or "utf-8", errors=errors or "strict") + return x.encode() + + +def to_str( + x: str | bytes, encoding: str | None = None, errors: str | None = None +) -> str: + if isinstance(x, str): + return x + elif not isinstance(x, bytes): + raise TypeError(f"not expecting type {type(x).__name__}") + if encoding or errors: + return x.decode(encoding or "utf-8", errors=errors or "strict") + return x.decode() + + +def reraise( + tp: type[BaseException] | None, + value: BaseException, + tb: TracebackType | None = None, +) -> typing.NoReturn: + try: + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None # type: ignore[assignment] + tb = None diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/wait.py b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/wait.py new file mode 100644 index 0000000000000000000000000000000000000000..aeca0c7ad5b232eeb1ad9c43d315bd1d74eaed9a --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/urllib3/util/wait.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import select +import socket +from functools import partial + +__all__ = ["wait_for_read", "wait_for_write"] + + +# How should we wait on sockets? +# +# There are two types of APIs you can use for waiting on sockets: the fancy +# modern stateful APIs like epoll/kqueue, and the older stateless APIs like +# select/poll. The stateful APIs are more efficient when you have a lots of +# sockets to keep track of, because you can set them up once and then use them +# lots of times. But we only ever want to wait on a single socket at a time +# and don't want to keep track of state, so the stateless APIs are actually +# more efficient. So we want to use select() or poll(). +# +# Now, how do we choose between select() and poll()? On traditional Unixes, +# select() has a strange calling convention that makes it slow, or fail +# altogether, for high-numbered file descriptors. The point of poll() is to fix +# that, so on Unixes, we prefer poll(). +# +# On Windows, there is no poll() (or at least Python doesn't provide a wrapper +# for it), but that's OK, because on Windows, select() doesn't have this +# strange calling convention; plain select() works fine. +# +# So: on Windows we use select(), and everywhere else we use poll(). We also +# fall back to select() in case poll() is somehow broken or missing. + + +def select_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + rcheck = [] + wcheck = [] + if read: + rcheck.append(sock) + if write: + wcheck.append(sock) + # When doing a non-blocking connect, most systems signal success by + # marking the socket writable. Windows, though, signals success by marked + # it as "exceptional". We paper over the difference by checking the write + # sockets for both conditions. (The stdlib selectors module does the same + # thing.) + fn = partial(select.select, rcheck, wcheck, wcheck) + rready, wready, xready = fn(timeout) + return bool(rready or wready or xready) + + +def poll_wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + if not read and not write: + raise RuntimeError("must specify at least one of read=True, write=True") + mask = 0 + if read: + mask |= select.POLLIN + if write: + mask |= select.POLLOUT + poll_obj = select.poll() + poll_obj.register(sock, mask) + + # For some reason, poll() takes timeout in milliseconds + def do_poll(t: float | None) -> list[tuple[int, int]]: + if t is not None: + t *= 1000 + return poll_obj.poll(t) + + return bool(do_poll(timeout)) + + +def _have_working_poll() -> bool: + # Apparently some systems have a select.poll that fails as soon as you try + # to use it, either due to strange configuration or broken monkeypatching + # from libraries like eventlet/greenlet. + try: + poll_obj = select.poll() + poll_obj.poll(0) + except (AttributeError, OSError): + return False + else: + return True + + +def wait_for_socket( + sock: socket.socket, + read: bool = False, + write: bool = False, + timeout: float | None = None, +) -> bool: + # We delay choosing which implementation to use until the first time we're + # called. We could do it at import time, but then we might make the wrong + # decision if someone goes wild with monkeypatching select.poll after + # we're imported. + global wait_for_socket + if _have_working_poll(): + wait_for_socket = poll_wait_for_socket + elif hasattr(select, "select"): + wait_for_socket = select_wait_for_socket + return wait_for_socket(sock, read, write, timeout) + + +def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for reading to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, read=True, timeout=timeout) + + +def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool: + """Waits for writing to be available on a given socket. + Returns True if the socket is readable, or False if the timeout expired. + """ + return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2873d26212df4092156b0900135f577e7cab885a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__main__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..024b3c5fac23ccfd49339287a0530c358b16f28c Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/__main__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_bdist_wheel.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_bdist_wheel.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2ac79fbc3ff8b6570275608be16c127b3c25da5 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_bdist_wheel.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_setuptools_logging.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_setuptools_logging.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ab9649c6b3b79799adffc7517f18488435d4d19 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/_setuptools_logging.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/bdist_wheel.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/bdist_wheel.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbebef0bfe7294fca316f65ad22d5519291fafa7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/bdist_wheel.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/macosx_libfile.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/macosx_libfile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c49ef51a64a02bcc11555381e67d1345efc0022e Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/macosx_libfile.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/metadata.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/metadata.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a279e60f50f82191fc602395c89a4c9a429078ec Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/metadata.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/util.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..274d33c2dc2dd9af7c726311b2787e41c5f3f24a Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/util.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/wheelfile.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/wheelfile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1a3ad852897c10ea7bbe25d330e7eb5835944a7 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/__pycache__/wheelfile.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ba1217f5bdb6106c37ecc2be74d53ef2237b717 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__init__.py @@ -0,0 +1,155 @@ +""" +Wheel command-line utility. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from argparse import ArgumentTypeError + + +class WheelError(Exception): + pass + + +def unpack_f(args: argparse.Namespace) -> None: + from .unpack import unpack + + unpack(args.wheelfile, args.dest) + + +def pack_f(args: argparse.Namespace) -> None: + from .pack import pack + + pack(args.directory, args.dest_dir, args.build_number) + + +def convert_f(args: argparse.Namespace) -> None: + from .convert import convert + + convert(args.files, args.dest_dir, args.verbose) + + +def tags_f(args: argparse.Namespace) -> None: + from .tags import tags + + names = ( + tags( + wheel, + args.python_tag, + args.abi_tag, + args.platform_tag, + args.build, + args.remove, + ) + for wheel in args.wheel + ) + + for name in names: + print(name) + + +def version_f(args: argparse.Namespace) -> None: + from .. import __version__ + + print(f"wheel {__version__}") + + +def parse_build_tag(build_tag: str) -> str: + if build_tag and not build_tag[0].isdigit(): + raise ArgumentTypeError("build tag must begin with a digit") + elif "-" in build_tag: + raise ArgumentTypeError("invalid character ('-') in build tag") + + return build_tag + + +TAGS_HELP = """\ +Make a new wheel with given tags. Any tags unspecified will remain the same. +Starting the tags with a "+" will append to the existing tags. Starting with a +"-" will remove a tag (use --option=-TAG syntax). Multiple tags can be +separated by ".". The original file will remain unless --remove is given. The +output filename(s) will be displayed on stdout for further processing. +""" + + +def parser(): + p = argparse.ArgumentParser() + s = p.add_subparsers(help="commands") + + unpack_parser = s.add_parser("unpack", help="Unpack wheel") + unpack_parser.add_argument( + "--dest", "-d", help="Destination directory", default="." + ) + unpack_parser.add_argument("wheelfile", help="Wheel file") + unpack_parser.set_defaults(func=unpack_f) + + repack_parser = s.add_parser("pack", help="Repack wheel") + repack_parser.add_argument("directory", help="Root directory of the unpacked wheel") + repack_parser.add_argument( + "--dest-dir", + "-d", + default=os.path.curdir, + help="Directory to store the wheel (default %(default)s)", + ) + repack_parser.add_argument( + "--build-number", help="Build tag to use in the wheel name" + ) + repack_parser.set_defaults(func=pack_f) + + convert_parser = s.add_parser("convert", help="Convert egg or wininst to wheel") + convert_parser.add_argument("files", nargs="*", help="Files to convert") + convert_parser.add_argument( + "--dest-dir", + "-d", + default=os.path.curdir, + help="Directory to store wheels (default %(default)s)", + ) + convert_parser.add_argument("--verbose", "-v", action="store_true") + convert_parser.set_defaults(func=convert_f) + + tags_parser = s.add_parser( + "tags", help="Add or replace the tags on a wheel", description=TAGS_HELP + ) + tags_parser.add_argument("wheel", nargs="*", help="Existing wheel(s) to retag") + tags_parser.add_argument( + "--remove", + action="store_true", + help="Remove the original files, keeping only the renamed ones", + ) + tags_parser.add_argument( + "--python-tag", metavar="TAG", help="Specify an interpreter tag(s)" + ) + tags_parser.add_argument("--abi-tag", metavar="TAG", help="Specify an ABI tag(s)") + tags_parser.add_argument( + "--platform-tag", metavar="TAG", help="Specify a platform tag(s)" + ) + tags_parser.add_argument( + "--build", type=parse_build_tag, metavar="BUILD", help="Specify a build tag" + ) + tags_parser.set_defaults(func=tags_f) + + version_parser = s.add_parser("version", help="Print version and exit") + version_parser.set_defaults(func=version_f) + + help_parser = s.add_parser("help", help="Show this help") + help_parser.set_defaults(func=lambda args: p.print_help()) + + return p + + +def main(): + p = parser() + args = p.parse_args() + if not hasattr(args, "func"): + p.print_help() + else: + try: + args.func(args) + return 0 + except WheelError as e: + print(e, file=sys.stderr) + + return 1 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fcb78a3c821dedbead5eb51ac494175d3475f64 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/convert.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/convert.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ee7733fcc04a02e741b442e8e044aed230aa251 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/convert.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/pack.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/pack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..246800340225f3618c3a89bbb9a5cbc525e0a790 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/pack.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/tags.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/tags.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..328af8cf1dc870350548fb5395ab1a2846e5fb7f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/tags.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/unpack.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/unpack.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a10f0436412d76764da89bd0979b95182546d1b8 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/__pycache__/unpack.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/convert.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/convert.py new file mode 100644 index 0000000000000000000000000000000000000000..61d4775c58596ec12a0c4d4a548de430dee579bf --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/convert.py @@ -0,0 +1,332 @@ +from __future__ import annotations + +import os.path +import re +from abc import ABCMeta, abstractmethod +from collections import defaultdict +from collections.abc import Iterator +from email.message import Message +from email.parser import Parser +from email.policy import EmailPolicy +from glob import iglob +from pathlib import Path +from textwrap import dedent +from zipfile import ZipFile + +from .. import __version__ +from ..metadata import generate_requirements +from ..vendored.packaging.tags import parse_tag +from ..wheelfile import WheelFile + +egg_filename_re = re.compile( + r""" + (?P.+?)-(?P.+?) + (-(?Ppy\d\.\d+) + (-(?P.+?))? + )?.egg$""", + re.VERBOSE, +) +egg_info_re = re.compile( + r""" + ^(?P.+?)-(?P.+?) + (-(?Ppy\d\.\d+) + )?.egg-info/""", + re.VERBOSE, +) +wininst_re = re.compile( + r"\.(?Pwin32|win-amd64)(?:-(?Ppy\d\.\d))?\.exe$" +) +pyd_re = re.compile(r"\.(?P[a-z0-9]+)-(?Pwin32|win_amd64)\.pyd$") +serialization_policy = EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, +) +GENERATOR = f"wheel {__version__}" + + +def convert_requires(requires: str, metadata: Message) -> None: + extra: str | None = None + requirements: dict[str | None, list[str]] = defaultdict(list) + for line in requires.splitlines(): + line = line.strip() + if not line: + continue + + if line.startswith("[") and line.endswith("]"): + extra = line[1:-1] + continue + + requirements[extra].append(line) + + for key, value in generate_requirements(requirements): + metadata.add_header(key, value) + + +def convert_pkg_info(pkginfo: str, metadata: Message): + parsed_message = Parser().parsestr(pkginfo) + for key, value in parsed_message.items(): + key_lower = key.lower() + if value == "UNKNOWN": + continue + + if key_lower == "description": + description_lines = value.splitlines() + value = "\n".join( + ( + description_lines[0].lstrip(), + dedent("\n".join(description_lines[1:])), + "\n", + ) + ) + metadata.set_payload(value) + elif key_lower == "home-page": + metadata.add_header("Project-URL", f"Homepage, {value}") + elif key_lower == "download-url": + metadata.add_header("Project-URL", f"Download, {value}") + else: + metadata.add_header(key, value) + + metadata.replace_header("Metadata-Version", "2.4") + + +def normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_") + + +class ConvertSource(metaclass=ABCMeta): + name: str + version: str + pyver: str = "py2.py3" + abi: str = "none" + platform: str = "any" + metadata: Message + + @property + def dist_info_dir(self) -> str: + return f"{self.name}-{self.version}.dist-info" + + @abstractmethod + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + pass + + +class EggFileSource(ConvertSource): + def __init__(self, path: Path): + if not (match := egg_filename_re.match(path.name)): + raise ValueError(f"Invalid egg file name: {path.name}") + + # Binary wheels are assumed to be for CPython + self.path = path + self.name = normalize(match.group("name")) + self.version = match.group("ver") + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + if arch := match.group("arch"): + self.abi = self.pyver.replace("py", "cp") + self.platform = normalize(arch) + + self.metadata = Message() + + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + with ZipFile(self.path, "r") as zip_file: + for filename in sorted(zip_file.namelist()): + # Skip pure directory entries + if filename.endswith("/"): + continue + + # Handle files in the egg-info directory specially, selectively moving + # them to the dist-info directory while converting as needed + if filename.startswith("EGG-INFO/"): + if filename == "EGG-INFO/requires.txt": + requires = zip_file.read(filename).decode("utf-8") + convert_requires(requires, self.metadata) + elif filename == "EGG-INFO/PKG-INFO": + pkginfo = zip_file.read(filename).decode("utf-8") + convert_pkg_info(pkginfo, self.metadata) + elif filename == "EGG-INFO/entry_points.txt": + yield ( + f"{self.dist_info_dir}/entry_points.txt", + zip_file.read(filename), + ) + + continue + + # For any other file, just pass it through + yield filename, zip_file.read(filename) + + +class EggDirectorySource(EggFileSource): + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + for dirpath, _, filenames in os.walk(self.path): + for filename in sorted(filenames): + path = Path(dirpath, filename) + if path.parent.name == "EGG-INFO": + if path.name == "requires.txt": + requires = path.read_text("utf-8") + convert_requires(requires, self.metadata) + elif path.name == "PKG-INFO": + pkginfo = path.read_text("utf-8") + convert_pkg_info(pkginfo, self.metadata) + if name := self.metadata.get("Name"): + self.name = normalize(name) + + if version := self.metadata.get("Version"): + self.version = version + elif path.name == "entry_points.txt": + yield ( + f"{self.dist_info_dir}/entry_points.txt", + path.read_bytes(), + ) + + continue + + # For any other file, just pass it through + yield str(path.relative_to(self.path)), path.read_bytes() + + +class WininstFileSource(ConvertSource): + """ + Handles distributions created with ``bdist_wininst``. + + The egginfo filename has the format:: + + name-ver(-pyver)(-arch).egg-info + + The installer filename has the format:: + + name-ver.arch(-pyver).exe + + Some things to note: + + 1. The installer filename is not definitive. An installer can be renamed + and work perfectly well as an installer. So more reliable data should + be used whenever possible. + 2. The egg-info data should be preferred for the name and version, because + these come straight from the distutils metadata, and are mandatory. + 3. The pyver from the egg-info data should be ignored, as it is + constructed from the version of Python used to build the installer, + which is irrelevant - the installer filename is correct here (even to + the point that when it's not there, any version is implied). + 4. The architecture must be taken from the installer filename, as it is + not included in the egg-info data. + 5. Architecture-neutral installers still have an architecture because the + installer format itself (being executable) is architecture-specific. We + should therefore ignore the architecture if the content is pure-python. + """ + + def __init__(self, path: Path): + self.path = path + self.metadata = Message() + + # Determine the initial architecture and Python version from the file name + # (if possible) + if match := wininst_re.search(path.name): + self.platform = normalize(match.group("platform")) + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + + # Look for an .egg-info directory and any .pyd files for more precise info + egg_info_found = pyd_found = False + with ZipFile(self.path) as zip_file: + for filename in zip_file.namelist(): + prefix, filename = filename.split("/", 1) + if not egg_info_found and (match := egg_info_re.match(filename)): + egg_info_found = True + self.name = normalize(match.group("name")) + self.version = match.group("ver") + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + elif not pyd_found and (match := pyd_re.search(filename)): + pyd_found = True + self.abi = match.group("abi") + self.platform = match.group("platform") + + if egg_info_found and pyd_found: + break + + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + dist_info_dir = f"{self.name}-{self.version}.dist-info" + data_dir = f"{self.name}-{self.version}.data" + with ZipFile(self.path, "r") as zip_file: + for filename in sorted(zip_file.namelist()): + # Skip pure directory entries + if filename.endswith("/"): + continue + + # Handle files in the egg-info directory specially, selectively moving + # them to the dist-info directory while converting as needed + prefix, target_filename = filename.split("/", 1) + if egg_info_re.search(target_filename): + basename = target_filename.rsplit("/", 1)[-1] + if basename == "requires.txt": + requires = zip_file.read(filename).decode("utf-8") + convert_requires(requires, self.metadata) + elif basename == "PKG-INFO": + pkginfo = zip_file.read(filename).decode("utf-8") + convert_pkg_info(pkginfo, self.metadata) + elif basename == "entry_points.txt": + yield ( + f"{dist_info_dir}/entry_points.txt", + zip_file.read(filename), + ) + + continue + elif prefix == "SCRIPTS": + target_filename = f"{data_dir}/scripts/{target_filename}" + + # For any other file, just pass it through + yield target_filename, zip_file.read(filename) + + +def convert(files: list[str], dest_dir: str, verbose: bool) -> None: + for pat in files: + for archive in iglob(pat): + path = Path(archive) + if path.suffix == ".egg": + if path.is_dir(): + source: ConvertSource = EggDirectorySource(path) + else: + source = EggFileSource(path) + else: + source = WininstFileSource(path) + + if verbose: + print(f"{archive}...", flush=True, end="") + + dest_path = Path(dest_dir) / ( + f"{source.name}-{source.version}-{source.pyver}-{source.abi}" + f"-{source.platform}.whl" + ) + with WheelFile(dest_path, "w") as wheelfile: + for name_or_zinfo, contents in source.generate_contents(): + wheelfile.writestr(name_or_zinfo, contents) + + # Write the METADATA file + wheelfile.writestr( + f"{source.dist_info_dir}/METADATA", + source.metadata.as_string(policy=serialization_policy).encode( + "utf-8" + ), + ) + + # Write the WHEEL file + wheel_message = Message() + wheel_message.add_header("Wheel-Version", "1.0") + wheel_message.add_header("Generator", GENERATOR) + wheel_message.add_header( + "Root-Is-Purelib", str(source.platform == "any").lower() + ) + tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}") + for tag in sorted(tags, key=lambda tag: tag.interpreter): + wheel_message.add_header("Tag", str(tag)) + + wheelfile.writestr( + f"{source.dist_info_dir}/WHEEL", + wheel_message.as_string(policy=serialization_policy).encode( + "utf-8" + ), + ) + + if verbose: + print("OK") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/pack.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/pack.py new file mode 100644 index 0000000000000000000000000000000000000000..64469c0c730c24449e77c23f46d0e3f68d647d67 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/pack.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import email.policy +import os.path +import re +from email.generator import BytesGenerator +from email.parser import BytesParser + +from wheel.cli import WheelError +from wheel.wheelfile import WheelFile + +DIST_INFO_RE = re.compile(r"^(?P(?P.+?)-(?P\d.*?))\.dist-info$") + + +def pack(directory: str, dest_dir: str, build_number: str | None) -> None: + """Repack a previously unpacked wheel directory into a new wheel file. + + The .dist-info/WHEEL file must contain one or more tags so that the target + wheel file name can be determined. + + :param directory: The unpacked wheel directory + :param dest_dir: Destination directory (defaults to the current directory) + """ + # Find the .dist-info directory + dist_info_dirs = [ + fn + for fn in os.listdir(directory) + if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn) + ] + if len(dist_info_dirs) > 1: + raise WheelError(f"Multiple .dist-info directories found in {directory}") + elif not dist_info_dirs: + raise WheelError(f"No .dist-info directories found in {directory}") + + # Determine the target wheel filename + dist_info_dir = dist_info_dirs[0] + name_version = DIST_INFO_RE.match(dist_info_dir).group("namever") + + # Read the tags and the existing build number from .dist-info/WHEEL + wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") + with open(wheel_file_path, "rb") as f: + info = BytesParser(policy=email.policy.compat32).parse(f) + tags: list[str] = info.get_all("Tag", []) + existing_build_number = info.get("Build") + + if not tags: + raise WheelError( + f"No tags present in {dist_info_dir}/WHEEL; cannot determine target " + f"wheel filename" + ) + + # Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL + build_number = build_number if build_number is not None else existing_build_number + if build_number is not None: + del info["Build"] + if build_number: + info["Build"] = build_number + name_version += "-" + build_number + + if build_number != existing_build_number: + with open(wheel_file_path, "wb") as f: + BytesGenerator(f, maxheaderlen=0).flatten(info) + + # Reassemble the tags for the wheel file + tagline = compute_tagline(tags) + + # Repack the wheel + wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl") + with WheelFile(wheel_path, "w") as wf: + print(f"Repacking wheel as {wheel_path}...", end="", flush=True) + wf.write_files(directory) + + print("OK") + + +def compute_tagline(tags: list[str]) -> str: + """Compute a tagline from a list of tags. + + :param tags: A list of tags + :return: A tagline + """ + impls = sorted({tag.split("-")[0] for tag in tags}) + abivers = sorted({tag.split("-")[1] for tag in tags}) + platforms = sorted({tag.split("-")[2] for tag in tags}) + return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)]) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/tags.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/tags.py new file mode 100644 index 0000000000000000000000000000000000000000..88da72e9ec4a31e2427bdb2bcf2b3e0ce3c0beb0 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/tags.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import email.policy +import itertools +import os +from collections.abc import Iterable +from email.parser import BytesParser + +from ..wheelfile import WheelFile + + +def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]: + """Add or replace tags. Supports dot-separated tags""" + if new_tags is None: + return set(original_tags) + + if new_tags.startswith("+"): + return {*original_tags, *new_tags[1:].split(".")} + + if new_tags.startswith("-"): + return set(original_tags) - set(new_tags[1:].split(".")) + + return set(new_tags.split(".")) + + +def tags( + wheel: str, + python_tags: str | None = None, + abi_tags: str | None = None, + platform_tags: str | None = None, + build_tag: str | None = None, + remove: bool = False, +) -> str: + """Change the tags on a wheel file. + + The tags are left unchanged if they are not specified. To specify "none", + use ["none"]. To append to the previous tags, a tag should start with a + "+". If a tag starts with "-", it will be removed from existing tags. + Processing is done left to right. + + :param wheel: The paths to the wheels + :param python_tags: The Python tags to set + :param abi_tags: The ABI tags to set + :param platform_tags: The platform tags to set + :param build_tag: The build tag to set + :param remove: Remove the original wheel + """ + with WheelFile(wheel, "r") as f: + assert f.filename, f"{f.filename} must be available" + + wheel_info = f.read(f.dist_info_path + "/WHEEL") + info = BytesParser(policy=email.policy.compat32).parsebytes(wheel_info) + + original_wheel_name = os.path.basename(f.filename) + namever = f.parsed_filename.group("namever") + build = f.parsed_filename.group("build") + original_python_tags = f.parsed_filename.group("pyver").split(".") + original_abi_tags = f.parsed_filename.group("abi").split(".") + original_plat_tags = f.parsed_filename.group("plat").split(".") + + tags: list[str] = info.get_all("Tag", []) + existing_build_tag = info.get("Build") + + impls = {tag.split("-")[0] for tag in tags} + abivers = {tag.split("-")[1] for tag in tags} + platforms = {tag.split("-")[2] for tag in tags} + + if impls != set(original_python_tags): + msg = f"Wheel internal tags {impls!r} != filename tags {original_python_tags!r}" + raise AssertionError(msg) + + if abivers != set(original_abi_tags): + msg = f"Wheel internal tags {abivers!r} != filename tags {original_abi_tags!r}" + raise AssertionError(msg) + + if platforms != set(original_plat_tags): + msg = ( + f"Wheel internal tags {platforms!r} != filename tags {original_plat_tags!r}" + ) + raise AssertionError(msg) + + if existing_build_tag != build: + msg = ( + f"Incorrect filename '{build}' " + f"& *.dist-info/WHEEL '{existing_build_tag}' build numbers" + ) + raise AssertionError(msg) + + # Start changing as needed + if build_tag is not None: + build = build_tag + + final_python_tags = sorted(_compute_tags(original_python_tags, python_tags)) + final_abi_tags = sorted(_compute_tags(original_abi_tags, abi_tags)) + final_plat_tags = sorted(_compute_tags(original_plat_tags, platform_tags)) + + final_tags = [ + namever, + ".".join(final_python_tags), + ".".join(final_abi_tags), + ".".join(final_plat_tags), + ] + if build: + final_tags.insert(1, build) + + final_wheel_name = "-".join(final_tags) + ".whl" + + if original_wheel_name != final_wheel_name: + del info["Tag"], info["Build"] + for a, b, c in itertools.product( + final_python_tags, final_abi_tags, final_plat_tags + ): + info["Tag"] = f"{a}-{b}-{c}" + if build: + info["Build"] = build + + original_wheel_path = os.path.join( + os.path.dirname(f.filename), original_wheel_name + ) + final_wheel_path = os.path.join(os.path.dirname(f.filename), final_wheel_name) + + with WheelFile(original_wheel_path, "r") as fin, WheelFile( + final_wheel_path, "w" + ) as fout: + fout.comment = fin.comment # preserve the comment + for item in fin.infolist(): + if item.is_dir(): + continue + if item.filename == f.dist_info_path + "/RECORD": + continue + if item.filename == f.dist_info_path + "/WHEEL": + fout.writestr(item, info.as_bytes()) + else: + fout.writestr(item, fin.read(item)) + + if remove: + os.remove(original_wheel_path) + + return final_wheel_name diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/unpack.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/unpack.py new file mode 100644 index 0000000000000000000000000000000000000000..d48840e6ec0512225233bf02d1d7ce203415b04c --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/cli/unpack.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + +from ..wheelfile import WheelFile + + +def unpack(path: str, dest: str = ".") -> None: + """Unpack a wheel. + + Wheel content will be unpacked to {dest}/{name}-{ver}, where {name} + is the package name and {ver} its version. + + :param path: The path to the wheel. + :param dest: Destination directory (default to current directory). + """ + with WheelFile(path) as wf: + namever = wf.parsed_filename.group("namever") + destination = Path(dest) / namever + print(f"Unpacking to: {destination}...", end="", flush=True) + for zinfo in wf.filelist: + wf.extract(zinfo, destination) + + # Set permissions to the same values as they were set in the archive + # We have to do this manually due to + # https://github.com/python/cpython/issues/59999 + permissions = zinfo.external_attr >> 16 & 0o777 + destination.joinpath(zinfo.filename).chmod(permissions) + + print("OK") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b5ad4d4d9e5ab134d11c2f004ddec3ae2a6c9d3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6f62d44e4ef733c0e713afcd2371fed7f2b3de67 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.APACHE b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.APACHE new file mode 100644 index 0000000000000000000000000000000000000000..f433b1a53f5b830a205fd2df78e2b34974656c7b --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.BSD b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.BSD new file mode 100644 index 0000000000000000000000000000000000000000..42ce7b75c92fb01a3f6ed17eea363f756b7da582 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. 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. + +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/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__init__.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/__init__.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f563bc22de1fa6ff256493af038bc9de52e124fa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/__init__.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_elffile.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_elffile.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..916a9d54e3229ae5b70f076daf2d76b016fa08b6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_elffile.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_manylinux.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_manylinux.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6df0da73a053a90dcdda3c3d755b3231a5ec4272 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_manylinux.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_musllinux.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_musllinux.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ec81c9267abb92be58ea0446d04fa392927cc0d Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_musllinux.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_parser.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56fa1276d959deb2d206a442066ce520415ad37f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_parser.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_structures.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_structures.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcad58678df20b8260151d2bbf706ac023bb1fab Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_structures.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_tokenizer.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_tokenizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f03c706f29a5f1f81728dd6db7d65cfecbd1bc3 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/_tokenizer.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/markers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/markers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..551ff9c38bd3287385804d0daa913ec147886bdb Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/markers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/requirements.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/requirements.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7445afb016a1063df1d051cb01fe3e12c2ef3952 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/requirements.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/specifiers.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/specifiers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..692d7b0c9446a642d06067832a726f148a4421c6 Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/specifiers.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/tags.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/tags.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06fa12f1edcc5ee0964d51237acc3643d30016fa Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/tags.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/utils.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3ccf0d3dbdd8beaa0400994dfa7ddc34928a17f Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/utils.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/version.cpython-312.pyc b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67ce04fdbc9f996ce082e4682acd960f14d8d2bd Binary files /dev/null and b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/__pycache__/version.cpython-312.pyc differ diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_elffile.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_elffile.py new file mode 100644 index 0000000000000000000000000000000000000000..6fb19b30bb53c18f38a9ef02dd7c4478670fb962 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_elffile.py @@ -0,0 +1,108 @@ +""" +ELF file parser. + +This provides a class ``ELFFile`` that parses an ELF executable in a similar +interface to ``ZipFile``. Only the read interface is implemented. + +Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca +ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html +""" + +import enum +import os +import struct +from typing import IO, Optional, Tuple + + +class ELFInvalid(ValueError): + pass + + +class EIClass(enum.IntEnum): + C32 = 1 + C64 = 2 + + +class EIData(enum.IntEnum): + Lsb = 1 + Msb = 2 + + +class EMachine(enum.IntEnum): + I386 = 3 + S390 = 22 + Arm = 40 + X8664 = 62 + AArc64 = 183 + + +class ELFFile: + """ + Representation of an ELF executable. + """ + + def __init__(self, f: IO[bytes]) -> None: + self._f = f + + try: + ident = self._read("16B") + except struct.error: + raise ELFInvalid("unable to parse identification") + magic = bytes(ident[:4]) + if magic != b"\x7fELF": + raise ELFInvalid(f"invalid magic: {magic!r}") + + self.capacity = ident[4] # Format for program header (bitness). + self.encoding = ident[5] # Data structure encoding (endianness). + + try: + # e_fmt: Format for program header. + # p_fmt: Format for section header. + # p_idx: Indexes to find p_type, p_offset, and p_filesz. + e_fmt, self._p_fmt, self._p_idx = { + (1, 1): ("HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. + (2, 1): ("HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. + }[(self.capacity, self.encoding)] + except KeyError: + raise ELFInvalid( + f"unrecognized capacity ({self.capacity}) or " + f"encoding ({self.encoding})" + ) + + try: + ( + _, + self.machine, # Architecture type. + _, + _, + self._e_phoff, # Offset of program header. + _, + self.flags, # Processor-specific flags. + _, + self._e_phentsize, # Size of section. + self._e_phnum, # Number of sections. + ) = self._read(e_fmt) + except struct.error as e: + raise ELFInvalid("unable to parse machine and section information") from e + + def _read(self, fmt: str) -> Tuple[int, ...]: + return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) + + @property + def interpreter(self) -> Optional[str]: + """ + The path recorded in the ``PT_INTERP`` section header. + """ + for index in range(self._e_phnum): + self._f.seek(self._e_phoff + self._e_phentsize * index) + try: + data = self._read(self._p_fmt) + except struct.error: + continue + if data[self._p_idx[0]] != 3: # Not PT_INTERP. + continue + self._f.seek(data[self._p_idx[1]]) + return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") + return None diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_manylinux.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_manylinux.py new file mode 100644 index 0000000000000000000000000000000000000000..1f5f4ab3e514d1846d3bd189bf081fdb1528ba08 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_manylinux.py @@ -0,0 +1,260 @@ +import collections +import contextlib +import functools +import os +import re +import sys +import warnings +from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple + +from ._elffile import EIClass, EIData, ELFFile, EMachine + +EF_ARM_ABIMASK = 0xFF000000 +EF_ARM_ABI_VER5 = 0x05000000 +EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + +# `os.PathLike` not a generic type until Python 3.9, so sticking with `str` +# as the type for `path` until then. +@contextlib.contextmanager +def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: + try: + with open(path, "rb") as f: + yield ELFFile(f) + except (OSError, TypeError, ValueError): + yield None + + +def _is_linux_armhf(executable: str) -> bool: + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.Arm + and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 + and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD + ) + + +def _is_linux_i686(executable: str) -> bool: + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.I386 + ) + + +def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: + if "armv7l" in archs: + return _is_linux_armhf(executable) + if "i686" in archs: + return _is_linux_i686(executable) + allowed_archs = { + "x86_64", + "aarch64", + "ppc64", + "ppc64le", + "s390x", + "loongarch64", + "riscv64", + } + return any(arch in allowed_archs for arch in archs) + + +# If glibc ever changes its major version, we need to know what the last +# minor version was, so we can build the complete list of all versions. +# For now, guess what the highest minor version might be, assume it will +# be 50 for testing. Once this actually happens, update the dictionary +# with the actual value. +_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) + + +class _GLibCVersion(NamedTuple): + major: int + minor: int + + +def _glibc_version_string_confstr() -> Optional[str]: + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 + try: + # Should be a string like "glibc 2.17". + version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION") + assert version_string is not None + _, version = version_string.rsplit() + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes() -> Optional[str]: + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + # + # We must also handle the special case where the executable is not a + # dynamically linked executable. This can occur when using musl libc, + # for example. In this situation, dlopen() will error, leading to an + # OSError. Interestingly, at least in the case of musl, there is no + # errno set on the OSError. The single string argument used to construct + # OSError comes from libc itself and is therefore not portable to + # hard code here. In any case, failure to call dlopen() means we + # can proceed, so we bail on our attempt. + try: + process_namespace = ctypes.CDLL(None) + except OSError: + return None + + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str: str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +def _glibc_version_string() -> Optional[str]: + """Returns glibc version string, or None if not using glibc.""" + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _parse_glibc_version(version_str: str) -> Tuple[int, int]: + """Parse glibc version. + + We use a regexp instead of str.split because we want to discard any + random junk that might come after the minor version -- this might happen + in patched/forked versions of glibc (e.g. Linaro's version of glibc + uses version strings like "2.20-2014.11"). See gh-3588. + """ + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + f"Expected glibc version with 2 components major.minor," + f" got: {version_str}", + RuntimeWarning, + ) + return -1, -1 + return int(m.group("major")), int(m.group("minor")) + + +@functools.lru_cache +def _get_glibc_version() -> Tuple[int, int]: + version_str = _glibc_version_string() + if version_str is None: + return (-1, -1) + return _parse_glibc_version(version_str) + + +# From PEP 513, PEP 600 +def _is_compatible(arch: str, version: _GLibCVersion) -> bool: + sys_glibc = _get_glibc_version() + if sys_glibc < version: + return False + # Check for presence of _manylinux module. + try: + import _manylinux + except ImportError: + return True + if hasattr(_manylinux, "manylinux_compatible"): + result = _manylinux.manylinux_compatible(version[0], version[1], arch) + if result is not None: + return bool(result) + return True + if version == _GLibCVersion(2, 5): + if hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if version == _GLibCVersion(2, 12): + if hasattr(_manylinux, "manylinux2010_compatible"): + return bool(_manylinux.manylinux2010_compatible) + if version == _GLibCVersion(2, 17): + if hasattr(_manylinux, "manylinux2014_compatible"): + return bool(_manylinux.manylinux2014_compatible) + return True + + +_LEGACY_MANYLINUX_MAP = { + # CentOS 7 w/ glibc 2.17 (PEP 599) + (2, 17): "manylinux2014", + # CentOS 6 w/ glibc 2.12 (PEP 571) + (2, 12): "manylinux2010", + # CentOS 5 w/ glibc 2.5 (PEP 513) + (2, 5): "manylinux1", +} + + +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate manylinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be manylinux-compatible. + + :returns: An iterator of compatible manylinux tags. + """ + if not _have_compatible_abi(sys.executable, archs): + return + # Oldest glibc to be supported regardless of architecture is (2, 17). + too_old_glibc2 = _GLibCVersion(2, 16) + if set(archs) & {"x86_64", "i686"}: + # On x86/i686 also oldest glibc to be supported is (2, 5). + too_old_glibc2 = _GLibCVersion(2, 4) + current_glibc = _GLibCVersion(*_get_glibc_version()) + glibc_max_list = [current_glibc] + # We can assume compatibility across glibc major versions. + # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 + # + # Build a list of maximum glibc versions so that we can + # output the canonical list of all glibc from current_glibc + # down to too_old_glibc2, including all intermediary versions. + for glibc_major in range(current_glibc.major - 1, 1, -1): + glibc_minor = _LAST_GLIBC_MINOR[glibc_major] + glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) + for arch in archs: + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_compatible(arch, glibc_version): + yield f"{tag}_{arch}" + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_compatible(arch, glibc_version): + yield f"{legacy_tag}_{arch}" diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_musllinux.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_musllinux.py new file mode 100644 index 0000000000000000000000000000000000000000..eb4251b5c1e82772b2b0ea539943da5141fd55ec --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_musllinux.py @@ -0,0 +1,83 @@ +"""PEP 656 support. + +This module implements logic to detect if the currently running Python is +linked against musl, and what musl version is used. +""" + +import functools +import re +import subprocess +import sys +from typing import Iterator, NamedTuple, Optional, Sequence + +from ._elffile import ELFFile + + +class _MuslVersion(NamedTuple): + major: int + minor: int + + +def _parse_musl_version(output: str) -> Optional[_MuslVersion]: + lines = [n for n in (n.strip() for n in output.splitlines()) if n] + if len(lines) < 2 or lines[0][:4] != "musl": + return None + m = re.match(r"Version (\d+)\.(\d+)", lines[1]) + if not m: + return None + return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) + + +@functools.lru_cache +def _get_musl_version(executable: str) -> Optional[_MuslVersion]: + """Detect currently-running musl runtime version. + + This is done by checking the specified executable's dynamic linking + information, and invoking the loader to parse its output for a version + string. If the loader is musl, the output would be something like:: + + musl libc (x86_64) + Version 1.2.2 + Dynamic Program Loader + """ + try: + with open(executable, "rb") as f: + ld = ELFFile(f).interpreter + except (OSError, TypeError, ValueError): + return None + if ld is None or "musl" not in ld: + return None + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) + return _parse_musl_version(proc.stderr) + + +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate musllinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be musllinux-compatible. + + :returns: An iterator of compatible musllinux tags. + """ + sys_musl = _get_musl_version(sys.executable) + if sys_musl is None: # Python not dynamically linked against musl. + return + for arch in archs: + for minor in range(sys_musl.minor, -1, -1): + yield f"musllinux_{sys_musl.major}_{minor}_{arch}" + + +if __name__ == "__main__": # pragma: no cover + import sysconfig + + plat = sysconfig.get_platform() + assert plat.startswith("linux-"), "not linux" + + print("plat:", plat) + print("musl:", _get_musl_version(sys.executable)) + print("tags:", end=" ") + for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): + print(t, end="\n ") diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_parser.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..513686a2190f9911c08ca1ca263f37b799f44702 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_parser.py @@ -0,0 +1,356 @@ +"""Handwritten parser of dependency specifiers. + +The docstring for each __parse_* function contains EBNF-inspired grammar representing +the implementation. +""" + +import ast +from typing import Any, List, NamedTuple, Optional, Tuple, Union + +from ._tokenizer import DEFAULT_RULES, Tokenizer + + +class Node: + def __init__(self, value: str) -> None: + self.value = value + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}('{self}')>" + + def serialize(self) -> str: + raise NotImplementedError + + +class Variable(Node): + def serialize(self) -> str: + return str(self) + + +class Value(Node): + def serialize(self) -> str: + return f'"{self}"' + + +class Op(Node): + def serialize(self) -> str: + return str(self) + + +MarkerVar = Union[Variable, Value] +MarkerItem = Tuple[MarkerVar, Op, MarkerVar] +# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]] +# MarkerList = List[Union["MarkerList", MarkerAtom, str]] +# mypy does not support recursive type definition +# https://github.com/python/mypy/issues/731 +MarkerAtom = Any +MarkerList = List[Any] + + +class ParsedRequirement(NamedTuple): + name: str + url: str + extras: List[str] + specifier: str + marker: Optional[MarkerList] + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for dependency specifier +# -------------------------------------------------------------------------------------- +def parse_requirement(source: str) -> ParsedRequirement: + return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement: + """ + requirement = WS? IDENTIFIER WS? extras WS? requirement_details + """ + tokenizer.consume("WS") + + name_token = tokenizer.expect( + "IDENTIFIER", expected="package name at the start of dependency specifier" + ) + name = name_token.text + tokenizer.consume("WS") + + extras = _parse_extras(tokenizer) + tokenizer.consume("WS") + + url, specifier, marker = _parse_requirement_details(tokenizer) + tokenizer.expect("END", expected="end of dependency specifier") + + return ParsedRequirement(name, url, extras, specifier, marker) + + +def _parse_requirement_details( + tokenizer: Tokenizer, +) -> Tuple[str, str, Optional[MarkerList]]: + """ + requirement_details = AT URL (WS requirement_marker?)? + | specifier WS? (requirement_marker)? + """ + + specifier = "" + url = "" + marker = None + + if tokenizer.check("AT"): + tokenizer.read() + tokenizer.consume("WS") + + url_start = tokenizer.position + url = tokenizer.expect("URL", expected="URL after @").text + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + tokenizer.expect("WS", expected="whitespace after URL") + + # The input might end after whitespace. + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, span_start=url_start, after="URL and whitespace" + ) + else: + specifier_start = tokenizer.position + specifier = _parse_specifier(tokenizer) + tokenizer.consume("WS") + + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, + span_start=specifier_start, + after=( + "version specifier" + if specifier + else "name and no valid version specifier" + ), + ) + + return (url, specifier, marker) + + +def _parse_requirement_marker( + tokenizer: Tokenizer, *, span_start: int, after: str +) -> MarkerList: + """ + requirement_marker = SEMICOLON marker WS? + """ + + if not tokenizer.check("SEMICOLON"): + tokenizer.raise_syntax_error( + f"Expected end or semicolon (after {after})", + span_start=span_start, + ) + tokenizer.read() + + marker = _parse_marker(tokenizer) + tokenizer.consume("WS") + + return marker + + +def _parse_extras(tokenizer: Tokenizer) -> List[str]: + """ + extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)? + """ + if not tokenizer.check("LEFT_BRACKET", peek=True): + return [] + + with tokenizer.enclosing_tokens( + "LEFT_BRACKET", + "RIGHT_BRACKET", + around="extras", + ): + tokenizer.consume("WS") + extras = _parse_extras_list(tokenizer) + tokenizer.consume("WS") + + return extras + + +def _parse_extras_list(tokenizer: Tokenizer) -> List[str]: + """ + extras_list = identifier (wsp* ',' wsp* identifier)* + """ + extras: List[str] = [] + + if not tokenizer.check("IDENTIFIER"): + return extras + + extras.append(tokenizer.read().text) + + while True: + tokenizer.consume("WS") + if tokenizer.check("IDENTIFIER", peek=True): + tokenizer.raise_syntax_error("Expected comma between extra names") + elif not tokenizer.check("COMMA"): + break + + tokenizer.read() + tokenizer.consume("WS") + + extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma") + extras.append(extra_token.text) + + return extras + + +def _parse_specifier(tokenizer: Tokenizer) -> str: + """ + specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS + | WS? version_many WS? + """ + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="version specifier", + ): + tokenizer.consume("WS") + parsed_specifiers = _parse_version_many(tokenizer) + tokenizer.consume("WS") + + return parsed_specifiers + + +def _parse_version_many(tokenizer: Tokenizer) -> str: + """ + version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)? + """ + parsed_specifiers = "" + while tokenizer.check("SPECIFIER"): + span_start = tokenizer.position + parsed_specifiers += tokenizer.read().text + if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True): + tokenizer.raise_syntax_error( + ".* suffix can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position + 1, + ) + if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True): + tokenizer.raise_syntax_error( + "Local version label can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position, + ) + tokenizer.consume("WS") + if not tokenizer.check("COMMA"): + break + parsed_specifiers += tokenizer.read().text + tokenizer.consume("WS") + + return parsed_specifiers + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for marker expression +# -------------------------------------------------------------------------------------- +def parse_marker(source: str) -> MarkerList: + return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList: + retval = _parse_marker(tokenizer) + tokenizer.expect("END", expected="end of marker expression") + return retval + + +def _parse_marker(tokenizer: Tokenizer) -> MarkerList: + """ + marker = marker_atom (BOOLOP marker_atom)+ + """ + expression = [_parse_marker_atom(tokenizer)] + while tokenizer.check("BOOLOP"): + token = tokenizer.read() + expr_right = _parse_marker_atom(tokenizer) + expression.extend((token.text, expr_right)) + return expression + + +def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom: + """ + marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS? + | WS? marker_item WS? + """ + + tokenizer.consume("WS") + if tokenizer.check("LEFT_PARENTHESIS", peek=True): + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="marker expression", + ): + tokenizer.consume("WS") + marker: MarkerAtom = _parse_marker(tokenizer) + tokenizer.consume("WS") + else: + marker = _parse_marker_item(tokenizer) + tokenizer.consume("WS") + return marker + + +def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem: + """ + marker_item = WS? marker_var WS? marker_op WS? marker_var WS? + """ + tokenizer.consume("WS") + marker_var_left = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + marker_op = _parse_marker_op(tokenizer) + tokenizer.consume("WS") + marker_var_right = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + return (marker_var_left, marker_op, marker_var_right) + + +def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: + """ + marker_var = VARIABLE | QUOTED_STRING + """ + if tokenizer.check("VARIABLE"): + return process_env_var(tokenizer.read().text.replace(".", "_")) + elif tokenizer.check("QUOTED_STRING"): + return process_python_str(tokenizer.read().text) + else: + tokenizer.raise_syntax_error( + message="Expected a marker variable or quoted string" + ) + + +def process_env_var(env_var: str) -> Variable: + if env_var in ("platform_python_implementation", "python_implementation"): + return Variable("platform_python_implementation") + else: + return Variable(env_var) + + +def process_python_str(python_str: str) -> Value: + value = ast.literal_eval(python_str) + return Value(str(value)) + + +def _parse_marker_op(tokenizer: Tokenizer) -> Op: + """ + marker_op = IN | NOT IN | OP + """ + if tokenizer.check("IN"): + tokenizer.read() + return Op("in") + elif tokenizer.check("NOT"): + tokenizer.read() + tokenizer.expect("WS", expected="whitespace after 'not'") + tokenizer.expect("IN", expected="'in' after 'not'") + return Op("not in") + elif tokenizer.check("OP"): + return Op(tokenizer.read().text) + else: + return tokenizer.raise_syntax_error( + "Expected marker operator, one of " + "<=, <, !=, ==, >=, >, ~=, ===, in, not in" + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_structures.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_structures.py new file mode 100644 index 0000000000000000000000000000000000000000..90a6465f9682c886363eea5327dac64bf623a6ff --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_structures.py @@ -0,0 +1,61 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + + +class InfinityType: + def __repr__(self) -> str: + return "Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return False + + def __le__(self, other: object) -> bool: + return False + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return True + + def __ge__(self, other: object) -> bool: + return True + + def __neg__(self: object) -> "NegativeInfinityType": + return NegativeInfinity + + +Infinity = InfinityType() + + +class NegativeInfinityType: + def __repr__(self) -> str: + return "-Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return True + + def __le__(self, other: object) -> bool: + return True + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return False + + def __ge__(self, other: object) -> bool: + return False + + def __neg__(self: object) -> InfinityType: + return Infinity + + +NegativeInfinity = NegativeInfinityType() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_tokenizer.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_tokenizer.py new file mode 100644 index 0000000000000000000000000000000000000000..dd0d648d49a7c1a62d25ce5c9107aa448a8a22d1 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/_tokenizer.py @@ -0,0 +1,192 @@ +import contextlib +import re +from dataclasses import dataclass +from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union + +from .specifiers import Specifier + + +@dataclass +class Token: + name: str + text: str + position: int + + +class ParserSyntaxError(Exception): + """The provided source text could not be parsed correctly.""" + + def __init__( + self, + message: str, + *, + source: str, + span: Tuple[int, int], + ) -> None: + self.span = span + self.message = message + self.source = source + + super().__init__() + + def __str__(self) -> str: + marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" + return "\n ".join([self.message, self.source, marker]) + + +DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { + "LEFT_PARENTHESIS": r"\(", + "RIGHT_PARENTHESIS": r"\)", + "LEFT_BRACKET": r"\[", + "RIGHT_BRACKET": r"\]", + "SEMICOLON": r";", + "COMMA": r",", + "QUOTED_STRING": re.compile( + r""" + ( + ('[^']*') + | + ("[^"]*") + ) + """, + re.VERBOSE, + ), + "OP": r"(===|==|~=|!=|<=|>=|<|>)", + "BOOLOP": r"\b(or|and)\b", + "IN": r"\bin\b", + "NOT": r"\bnot\b", + "VARIABLE": re.compile( + r""" + \b( + python_version + |python_full_version + |os[._]name + |sys[._]platform + |platform_(release|system) + |platform[._](version|machine|python_implementation) + |python_implementation + |implementation_(name|version) + |extra + )\b + """, + re.VERBOSE, + ), + "SPECIFIER": re.compile( + Specifier._operator_regex_str + Specifier._version_regex_str, + re.VERBOSE | re.IGNORECASE, + ), + "AT": r"\@", + "URL": r"[^ \t]+", + "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", + "VERSION_PREFIX_TRAIL": r"\.\*", + "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", + "WS": r"[ \t]+", + "END": r"$", +} + + +class Tokenizer: + """Context-sensitive token parsing. + + Provides methods to examine the input stream to check whether the next token + matches. + """ + + def __init__( + self, + source: str, + *, + rules: "Dict[str, Union[str, re.Pattern[str]]]", + ) -> None: + self.source = source + self.rules: Dict[str, re.Pattern[str]] = { + name: re.compile(pattern) for name, pattern in rules.items() + } + self.next_token: Optional[Token] = None + self.position = 0 + + def consume(self, name: str) -> None: + """Move beyond provided token name, if at current position.""" + if self.check(name): + self.read() + + def check(self, name: str, *, peek: bool = False) -> bool: + """Check whether the next token has the provided name. + + By default, if the check succeeds, the token *must* be read before + another check. If `peek` is set to `True`, the token is not loaded and + would need to be checked again. + """ + assert ( + self.next_token is None + ), f"Cannot check for {name!r}, already have {self.next_token!r}" + assert name in self.rules, f"Unknown token name: {name!r}" + + expression = self.rules[name] + + match = expression.match(self.source, self.position) + if match is None: + return False + if not peek: + self.next_token = Token(name, match[0], self.position) + return True + + def expect(self, name: str, *, expected: str) -> Token: + """Expect a certain token name next, failing with a syntax error otherwise. + + The token is *not* read. + """ + if not self.check(name): + raise self.raise_syntax_error(f"Expected {expected}") + return self.read() + + def read(self) -> Token: + """Consume the next token and return it.""" + token = self.next_token + assert token is not None + + self.position += len(token.text) + self.next_token = None + + return token + + def raise_syntax_error( + self, + message: str, + *, + span_start: Optional[int] = None, + span_end: Optional[int] = None, + ) -> NoReturn: + """Raise ParserSyntaxError at the given position.""" + span = ( + self.position if span_start is None else span_start, + self.position if span_end is None else span_end, + ) + raise ParserSyntaxError( + message, + source=self.source, + span=span, + ) + + @contextlib.contextmanager + def enclosing_tokens( + self, open_token: str, close_token: str, *, around: str + ) -> Iterator[None]: + if self.check(open_token): + open_position = self.position + self.read() + else: + open_position = None + + yield + + if open_position is None: + return + + if not self.check(close_token): + self.raise_syntax_error( + f"Expected matching {close_token} for {open_token}, after {around}", + span_start=open_position, + ) + + self.read() diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/markers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/markers.py new file mode 100644 index 0000000000000000000000000000000000000000..c96d22a5a445e7353cd2454dec4255d3785c07b3 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/markers.py @@ -0,0 +1,253 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import operator +import os +import platform +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from ._parser import ( + MarkerAtom, + MarkerList, + Op, + Value, + Variable, +) +from ._parser import ( + parse_marker as _parse_marker, +) +from ._tokenizer import ParserSyntaxError +from .specifiers import InvalidSpecifier, Specifier +from .utils import canonicalize_name + +__all__ = [ + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", +] + +Operator = Callable[[str, str], bool] + + +class InvalidMarker(ValueError): + """ + An invalid marker was found, users should refer to PEP 508. + """ + + +class UndefinedComparison(ValueError): + """ + An invalid operation was attempted on a value that doesn't support it. + """ + + +class UndefinedEnvironmentName(ValueError): + """ + A name was attempted to be used that does not exist inside of the + environment. + """ + + +def _normalize_extra_values(results: Any) -> Any: + """ + Normalize extra values. + """ + if isinstance(results[0], tuple): + lhs, op, rhs = results[0] + if isinstance(lhs, Variable) and lhs.value == "extra": + normalized_extra = canonicalize_name(rhs.value) + rhs = Value(normalized_extra) + elif isinstance(rhs, Variable) and rhs.value == "extra": + normalized_extra = canonicalize_name(lhs.value) + lhs = Value(normalized_extra) + results[0] = lhs, op, rhs + return results + + +def _format_marker( + marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True +) -> str: + assert isinstance(marker, (list, tuple, str)) + + # Sometimes we have a structure like [[...]] which is a single item list + # where the single item is itself it's own list. In that case we want skip + # the rest of this function so that we don't get extraneous () on the + # outside. + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): + return _format_marker(marker[0]) + + if isinstance(marker, list): + inner = (_format_marker(m, first=False) for m in marker) + if first: + return " ".join(inner) + else: + return "(" + " ".join(inner) + ")" + elif isinstance(marker, tuple): + return " ".join([m.serialize() for m in marker]) + else: + return marker + + +_operators: Dict[str, Operator] = { + "in": lambda lhs, rhs: lhs in rhs, + "not in": lambda lhs, rhs: lhs not in rhs, + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">=": operator.ge, + ">": operator.gt, +} + + +def _eval_op(lhs: str, op: Op, rhs: str) -> bool: + try: + spec = Specifier("".join([op.serialize(), rhs])) + except InvalidSpecifier: + pass + else: + return spec.contains(lhs, prereleases=True) + + oper: Optional[Operator] = _operators.get(op.serialize()) + if oper is None: + raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") + + return oper(lhs, rhs) + + +def _normalize(*values: str, key: str) -> Tuple[str, ...]: + # PEP 685 – Comparison of extra names for optional distribution dependencies + # https://peps.python.org/pep-0685/ + # > When comparing extra names, tools MUST normalize the names being + # > compared using the semantics outlined in PEP 503 for names + if key == "extra": + return tuple(canonicalize_name(v) for v in values) + + # other environment markers don't have such standards + return values + + +def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: + groups: List[List[bool]] = [[]] + + for marker in markers: + assert isinstance(marker, (list, tuple, str)) + + if isinstance(marker, list): + groups[-1].append(_evaluate_markers(marker, environment)) + elif isinstance(marker, tuple): + lhs, op, rhs = marker + + if isinstance(lhs, Variable): + environment_key = lhs.value + lhs_value = environment[environment_key] + rhs_value = rhs.value + else: + lhs_value = lhs.value + environment_key = rhs.value + rhs_value = environment[environment_key] + + lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) + groups[-1].append(_eval_op(lhs_value, op, rhs_value)) + else: + assert marker in ["and", "or"] + if marker == "or": + groups.append([]) + + return any(all(item) for item in groups) + + +def format_full_version(info: "sys._version_info") -> str: + version = "{0.major}.{0.minor}.{0.micro}".format(info) + kind = info.releaselevel + if kind != "final": + version += kind[0] + str(info.serial) + return version + + +def default_environment() -> Dict[str, str]: + iver = format_full_version(sys.implementation.version) + implementation_name = sys.implementation.name + return { + "implementation_name": implementation_name, + "implementation_version": iver, + "os_name": os.name, + "platform_machine": platform.machine(), + "platform_release": platform.release(), + "platform_system": platform.system(), + "platform_version": platform.version(), + "python_full_version": platform.python_version(), + "platform_python_implementation": platform.python_implementation(), + "python_version": ".".join(platform.python_version_tuple()[:2]), + "sys_platform": sys.platform, + } + + +class Marker: + def __init__(self, marker: str) -> None: + # Note: We create a Marker object without calling this constructor in + # packaging.requirements.Requirement. If any additional logic is + # added here, make sure to mirror/adapt Requirement. + try: + self._markers = _normalize_extra_values(_parse_marker(marker)) + # The attribute `_markers` can be described in terms of a recursive type: + # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] + # + # For example, the following expression: + # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") + # + # is parsed into: + # [ + # (, ')>, ), + # 'and', + # [ + # (, , ), + # 'or', + # (, , ) + # ] + # ] + except ParserSyntaxError as e: + raise InvalidMarker(str(e)) from e + + def __str__(self) -> str: + return _format_marker(self._markers) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash((self.__class__.__name__, str(self))) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Marker): + return NotImplemented + + return str(self) == str(other) + + def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: + """Evaluate a marker. + + Return the boolean from evaluating the given marker against the + environment. environment is an optional argument to override all or + part of the determined environment. + + The environment is determined from the current Python process. + """ + current_environment = default_environment() + current_environment["extra"] = "" + if environment is not None: + current_environment.update(environment) + # The API used to allow setting extra to None. We need to handle this + # case for backwards compatibility. + if current_environment["extra"] is None: + current_environment["extra"] = "" + + return _evaluate_markers(self._markers, current_environment) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/requirements.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/requirements.py new file mode 100644 index 0000000000000000000000000000000000000000..bdc43a7e98d87dba0c2069bfb4554f71d228cad4 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/requirements.py @@ -0,0 +1,90 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from typing import Any, Iterator, Optional, Set + +from ._parser import parse_requirement as _parse_requirement +from ._tokenizer import ParserSyntaxError +from .markers import Marker, _normalize_extra_values +from .specifiers import SpecifierSet +from .utils import canonicalize_name + + +class InvalidRequirement(ValueError): + """ + An invalid requirement was found, users should refer to PEP 508. + """ + + +class Requirement: + """Parse a requirement. + + Parse a given requirement string into its parts, such as name, specifier, + URL, and extras. Raises InvalidRequirement on a badly-formed requirement + string. + """ + + # TODO: Can we test whether something is contained within a requirement? + # If so how do we do that? Do we need to test against the _name_ of + # the thing as well as the version? What about the markers? + # TODO: Can we normalize the name and extra name? + + def __init__(self, requirement_string: str) -> None: + try: + parsed = _parse_requirement(requirement_string) + except ParserSyntaxError as e: + raise InvalidRequirement(str(e)) from e + + self.name: str = parsed.name + self.url: Optional[str] = parsed.url or None + self.extras: Set[str] = set(parsed.extras or []) + self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) + self.marker: Optional[Marker] = None + if parsed.marker is not None: + self.marker = Marker.__new__(Marker) + self.marker._markers = _normalize_extra_values(parsed.marker) + + def _iter_parts(self, name: str) -> Iterator[str]: + yield name + + if self.extras: + formatted_extras = ",".join(sorted(self.extras)) + yield f"[{formatted_extras}]" + + if self.specifier: + yield str(self.specifier) + + if self.url: + yield f"@ {self.url}" + if self.marker: + yield " " + + if self.marker: + yield f"; {self.marker}" + + def __str__(self) -> str: + return "".join(self._iter_parts(self.name)) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash( + ( + self.__class__.__name__, + *self._iter_parts(canonicalize_name(self.name)), + ) + ) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Requirement): + return NotImplemented + + return ( + canonicalize_name(self.name) == canonicalize_name(other.name) + and self.extras == other.extras + and self.specifier == other.specifier + and self.url == other.url + and self.marker == other.marker + ) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/specifiers.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/specifiers.py new file mode 100644 index 0000000000000000000000000000000000000000..6d4066ae2770a3112f37d0e30d9a98fe59c4861f --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/specifiers.py @@ -0,0 +1,1011 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier + from packaging.version import Version +""" + +import abc +import itertools +import re +from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union + +from .utils import canonicalize_version +from .version import Version + +UnparsedVersion = Union[Version, str] +UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) +CallableOperator = Callable[[Version, str], bool] + + +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version + + +class InvalidSpecifier(ValueError): + """ + Raised when attempting to create a :class:`Specifier` with a specifier + string that is invalid. + + >>> Specifier("lolwat") + Traceback (most recent call last): + ... + packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' + """ + + +class BaseSpecifier(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __str__(self) -> str: + """ + Returns the str representation of this Specifier-like object. This + should be representative of the Specifier itself. + """ + + @abc.abstractmethod + def __hash__(self) -> int: + """ + Returns a hash value for this Specifier-like object. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Returns a boolean representing whether or not the two Specifier-like + objects are equal. + + :param other: The other object to check against. + """ + + @property + @abc.abstractmethod + def prereleases(self) -> Optional[bool]: + """Whether or not pre-releases as a whole are allowed. + + This can be set to either ``True`` or ``False`` to explicitly enable or disable + prereleases or it can be set to ``None`` (the default) to use default semantics. + """ + + @prereleases.setter + def prereleases(self, value: bool) -> None: + """Setter for :attr:`prereleases`. + + :param value: The value to set. + """ + + @abc.abstractmethod + def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: + """ + Determines if the given item is contained within this specifier. + """ + + @abc.abstractmethod + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """ + Takes an iterable of items and filters them so that only items which + are contained within this specifier are allowed in it. + """ + + +class Specifier(BaseSpecifier): + """This class abstracts handling of version specifiers. + + .. tip:: + + It is generally not required to instantiate this manually. You should instead + prefer to work with :class:`SpecifierSet` instead, which can parse + comma-separated version specifiers (which is what package metadata contains). + """ + + _operator_regex_str = r""" + (?P(~=|==|!=|<=|>=|<|>|===)) + """ + _version_regex_str = r""" + (?P + (?: + # The identity operators allow for an escape hatch that will + # do an exact string match of the version you wish to install. + # This will not be parsed by PEP 440 and we cannot determine + # any semantic meaning from it. This operator is discouraged + # but included entirely as an escape hatch. + (?<====) # Only match for the identity operator + \s* + [^\s;)]* # The arbitrary version can be just about anything, + # we match everything except for whitespace, a + # semi-colon for marker support, and a closing paren + # since versions can be enclosed in them. + ) + | + (?: + # The (non)equality operators allow for wild card and local + # versions to be specified so we have to define these two + # operators separately to enable that. + (?<===|!=) # Only match for equals and not equals + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)* # release + + # You cannot use a wild card and a pre-release, post-release, a dev or + # local version together so group them with a | and make them optional. + (?: + \.\* # Wild card syntax of .* + | + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local + )? + ) + | + (?: + # The compatible operator requires at least two digits in the + # release segment. + (?<=~=) # Only match for the compatible operator + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + ) + | + (?: + # All other operators only allow a sub set of what the + # (non)equality operators do. Specifically they do not allow + # local versions to be specified nor do they allow the prefix + # matching wild cards. + (?=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + "===": "arbitrary", + } + + def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: + """Initialize a Specifier instance. + + :param spec: + The string representation of a specifier which will be parsed and + normalized before use. + :param prereleases: + This tells the specifier if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + :raises InvalidSpecifier: + If the given specifier is invalid (i.e. bad syntax). + """ + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier(f"Invalid specifier: '{spec}'") + + self._spec: Tuple[str, str] = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 + @property # type: ignore[override] + def prereleases(self) -> bool: + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~=", "==="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if Version(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + @property + def operator(self) -> str: + """The operator of this specifier. + + >>> Specifier("==1.2.3").operator + '==' + """ + return self._spec[0] + + @property + def version(self) -> str: + """The version of this specifier. + + >>> Specifier("==1.2.3").version + '1.2.3' + """ + return self._spec[1] + + def __repr__(self) -> str: + """A representation of the Specifier that shows all internal state. + + >>> Specifier('>=1.0.0') + =1.0.0')> + >>> Specifier('>=1.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> Specifier('>=1.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" + + def __str__(self) -> str: + """A string representation of the Specifier that can be round-tripped. + + >>> str(Specifier('>=1.0.0')) + '>=1.0.0' + >>> str(Specifier('>=1.0.0', prereleases=False)) + '>=1.0.0' + """ + return "{}{}".format(*self._spec) + + @property + def _canonical_spec(self) -> Tuple[str, str]: + canonical_version = canonicalize_version( + self._spec[1], + strip_trailing_zero=(self._spec[0] != "~="), + ) + return self._spec[0], canonical_version + + def __hash__(self) -> int: + return hash(self._canonical_spec) + + def __eq__(self, other: object) -> bool: + """Whether or not the two Specifier-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") + True + >>> (Specifier("==1.2.3", prereleases=False) == + ... Specifier("==1.2.3", prereleases=True)) + True + >>> Specifier("==1.2.3") == "==1.2.3" + True + >>> Specifier("==1.2.3") == Specifier("==1.2.4") + False + >>> Specifier("==1.2.3") == Specifier("~=1.2.3") + False + """ + if isinstance(other, str): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._canonical_spec == other._canonical_spec + + def _get_operator(self, op: str) -> CallableOperator: + operator_callable: CallableOperator = getattr( + self, f"_compare_{self._operators[op]}" + ) + return operator_callable + + def _compare_compatible(self, prospective: Version, spec: str) -> bool: + # Compatible releases have an equivalent combination of >= and ==. That + # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to + # implement this in terms of the other specifiers instead of + # implementing it ourselves. The only thing we need to do is construct + # the other specifiers. + + # We want everything but the last item in the version, but we want to + # ignore suffix segments. + prefix = _version_join( + list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] + ) + + # Add the prefix notation to the end of our string + prefix += ".*" + + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) + + def _compare_equal(self, prospective: Version, spec: str) -> bool: + # We need special logic to handle prefix matching + if spec.endswith(".*"): + # In the case of prefix matching we want to ignore local segment. + normalized_prospective = canonicalize_version( + prospective.public, strip_trailing_zero=False + ) + # Get the normalized version string ignoring the trailing .* + normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) + # Split the spec out by bangs and dots, and pretend that there is + # an implicit dot in between a release segment and a pre-release segment. + split_spec = _version_split(normalized_spec) + + # Split the prospective version out by bangs and dots, and pretend + # that there is an implicit dot in between a release segment and + # a pre-release segment. + split_prospective = _version_split(normalized_prospective) + + # 0-pad the prospective version before shortening it to get the correct + # shortened version. + padded_prospective, _ = _pad_version(split_prospective, split_spec) + + # Shorten the prospective version to be the same length as the spec + # so that we can determine if the specifier is a prefix of the + # prospective version or not. + shortened_prospective = padded_prospective[: len(split_spec)] + + return shortened_prospective == split_spec + else: + # Convert our spec string into a Version + spec_version = Version(spec) + + # If the specifier does not have a local segment, then we want to + # act as if the prospective version also does not have a local + # segment. + if not spec_version.local: + prospective = Version(prospective.public) + + return prospective == spec_version + + def _compare_not_equal(self, prospective: Version, spec: str) -> bool: + return not self._compare_equal(prospective, spec) + + def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) + + def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) + + def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is less than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective < spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a pre-release version, that we do not accept pre-release + # versions for the version mentioned in the specifier (e.g. <3.1 should + # not match 3.1.dev0, but should match 3.0.dev0). + if not spec.is_prerelease and prospective.is_prerelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # less than the spec version *and* it's not a pre-release of the same + # version in the spec. + return True + + def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is greater than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective > spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a post-release version, that we do not accept + # post-release versions for the version mentioned in the specifier + # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). + if not spec.is_postrelease and prospective.is_postrelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # Ensure that we do not allow a local version of the version mentioned + # in the specifier, which is technically greater than, to match. + if prospective.local is not None: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # greater than the spec version *and* it's not a pre-release of the + # same version in the spec. + return True + + def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: + return str(prospective).lower() == str(spec).lower() + + def __contains__(self, item: Union[str, Version]) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in Specifier(">=1.2.3") + True + >>> Version("1.2.3") in Specifier(">=1.2.3") + True + >>> "1.0.0" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, item: UnparsedVersion, prereleases: Optional[bool] = None + ) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this Specifier. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> Specifier(">=1.2.3").contains("1.2.3") + True + >>> Specifier(">=1.2.3").contains(Version("1.2.3")) + True + >>> Specifier(">=1.2.3").contains("1.0.0") + False + >>> Specifier(">=1.2.3").contains("1.3.0a1") + False + >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") + True + >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) + True + """ + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") + normalized_item = _coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable: CallableOperator = self._get_operator(self.operator) + return operator_callable(normalized_item, self.version) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifier. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(Specifier().contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) + ['1.2.3', '1.3', ] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) + ['1.5a1'] + >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + """ + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = _coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later in case nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + + +_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") + + +def _version_split(version: str) -> List[str]: + """Split version into components. + + The split components are intended for version comparison. The logic does + not attempt to retain the original version string, so joining the + components back with :func:`_version_join` may not produce the original + version string. + """ + result: List[str] = [] + + epoch, _, rest = version.rpartition("!") + result.append(epoch or "0") + + for item in rest.split("."): + match = _prefix_regex.search(item) + if match: + result.extend(match.groups()) + else: + result.append(item) + return result + + +def _version_join(components: List[str]) -> str: + """Join split version components into a version string. + + This function assumes the input came from :func:`_version_split`, where the + first component must be the epoch (either empty or numeric), and all other + components numeric. + """ + epoch, *rest = components + return f"{epoch}!{'.'.join(rest)}" + + +def _is_not_suffix(segment: str) -> bool: + return not any( + segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") + ) + + +def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: + left_split, right_split = [], [] + + # Get the release segment of our versions + left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) + right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) + + # Get the rest of our versions + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) + + # Insert our padding + left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) + right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) + + return ( + list(itertools.chain.from_iterable(left_split)), + list(itertools.chain.from_iterable(right_split)), + ) + + +class SpecifierSet(BaseSpecifier): + """This class abstracts handling of a set of version specifiers. + + It can be passed a single specifier (``>=3.0``), a comma-separated list of + specifiers (``>=3.0,!=3.1``), or no specifier at all. + """ + + def __init__( + self, specifiers: str = "", prereleases: Optional[bool] = None + ) -> None: + """Initialize a SpecifierSet instance. + + :param specifiers: + The string representation of a specifier or a comma-separated list of + specifiers which will be parsed and normalized before use. + :param prereleases: + This tells the SpecifierSet if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + + :raises InvalidSpecifier: + If the given ``specifiers`` are not parseable than this exception will be + raised. + """ + + # Split on `,` to break each individual specifier into it's own item, and + # strip each item to remove leading/trailing whitespace. + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + + # Make each individual specifier a Specifier and save in a frozen set for later. + self._specs = frozenset(map(Specifier, split_specifiers)) + + # Store our prereleases value so we can use it later to determine if + # we accept prereleases or not. + self._prereleases = prereleases + + @property + def prereleases(self) -> Optional[bool]: + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # If we don't have any specifiers, and we don't have a forced value, + # then we'll just return None since we don't know if this should have + # pre-releases or not. + if not self._specs: + return None + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + def __repr__(self) -> str: + """A representation of the specifier set that shows all internal state. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> SpecifierSet('>=1.0.0,!=2.0.0') + =1.0.0')> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"" + + def __str__(self) -> str: + """A string representation of the specifier set that can be round-tripped. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) + '!=1.0.1,>=1.0.0' + >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) + '!=1.0.1,>=1.0.0' + """ + return ",".join(sorted(str(s) for s in self._specs)) + + def __hash__(self) -> int: + return hash(self._specs) + + def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": + """Return a SpecifierSet which is a combination of the two sets. + + :param other: The other object to combine with. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' + =1.0.0')> + >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') + =1.0.0')> + """ + if isinstance(other, str): + other = SpecifierSet(other) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + specifier = SpecifierSet() + specifier._specs = frozenset(self._specs | other._specs) + + if self._prereleases is None and other._prereleases is not None: + specifier._prereleases = other._prereleases + elif self._prereleases is not None and other._prereleases is None: + specifier._prereleases = self._prereleases + elif self._prereleases == other._prereleases: + specifier._prereleases = self._prereleases + else: + raise ValueError( + "Cannot combine SpecifierSets with True and False prerelease " + "overrides." + ) + + return specifier + + def __eq__(self, other: object) -> bool: + """Whether or not the two SpecifierSet-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == + ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") + False + """ + if isinstance(other, (str, Specifier)): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs == other._specs + + def __len__(self) -> int: + """Returns the number of specifiers in this specifier set.""" + return len(self._specs) + + def __iter__(self) -> Iterator[Specifier]: + """ + Returns an iterator over all the underlying :class:`Specifier` instances + in this specifier set. + + >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) + [, =1.0.0')>] + """ + return iter(self._specs) + + def __contains__(self, item: UnparsedVersion) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, + item: UnparsedVersion, + prereleases: Optional[bool] = None, + installed: Optional[bool] = None, + ) -> bool: + """Return whether or not the item is contained in this SpecifierSet. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this SpecifierSet. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) + True + """ + # Ensure that our item is a Version instance. + if not isinstance(item, Version): + item = Version(item) + + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # We can determine if we're going to allow pre-releases by looking to + # see if any of the underlying items supports them. If none of them do + # and this item is a pre-release then we do not allow it and we can + # short circuit that here. + # Note: This means that 1.0.dev1 would not be contained in something + # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 + if not prereleases and item.is_prerelease: + return False + + if installed and item.is_prerelease: + item = Version(item.base_version) + + # We simply dispatch to the underlying specs here to make sure that the + # given version is contained within all of them. + # Note: This use of all() here means that an empty set of specifiers + # will always return True, this is an explicit design decision. + return all(s.contains(item, prereleases=prereleases) for s in self._specs) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifiers in this set. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) + ['1.3', ] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) + [] + >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + + An "empty" SpecifierSet will filter items based on the presence of prerelease + versions in the set. + + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet("").filter(["1.5a1"])) + ['1.5a1'] + >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + """ + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # If we have any specifiers, then we want to wrap our iterable in the + # filter method for each one, this will act as a logical AND amongst + # each specifier. + if self._specs: + for spec in self._specs: + iterable = spec.filter(iterable, prereleases=bool(prereleases)) + return iter(iterable) + # If we do not have any specifiers, then we need to have a rough filter + # which will filter out any pre-releases, unless there are no final + # releases. + else: + filtered: List[UnparsedVersionVar] = [] + found_prereleases: List[UnparsedVersionVar] = [] + + for item in iterable: + parsed_version = _coerce_version(item) + + # Store any item which is a pre-release for later unless we've + # already found a final version or we are accepting prereleases + if parsed_version.is_prerelease and not prereleases: + if not filtered: + found_prereleases.append(item) + else: + filtered.append(item) + + # If we've found no items except for pre-releases, then we'll go + # ahead and use the pre-releases + if not filtered and found_prereleases and prereleases is None: + return iter(found_prereleases) + + return iter(filtered) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/tags.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/tags.py new file mode 100644 index 0000000000000000000000000000000000000000..89f1926137dd2d2a6bd63616bf5b9f722fc8d584 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/tags.py @@ -0,0 +1,571 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import logging +import platform +import re +import struct +import subprocess +import sys +import sysconfig +from importlib.machinery import EXTENSION_SUFFIXES +from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) + +from . import _manylinux, _musllinux + +logger = logging.getLogger(__name__) + +PythonVersion = Sequence[int] +MacVersion = Tuple[int, int] + +INTERPRETER_SHORT_NAMES: Dict[str, str] = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = struct.calcsize("P") == 4 + + +class Tag: + """ + A representation of the tag triple for a wheel. + + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ + + __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] + + def __init__(self, interpreter: str, abi: str, platform: str) -> None: + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + # The __hash__ of every single element in a Set[Tag] will be evaluated each time + # that a set calls its `.disjoint()` method, which may be called hundreds of + # times when scanning a page of links for packages with tags matching that + # Set[Tag]. Pre-computing the value here produces significant speedups for + # downstream consumers. + self._hash = hash((self._interpreter, self._abi, self._platform)) + + @property + def interpreter(self) -> str: + return self._interpreter + + @property + def abi(self) -> str: + return self._abi + + @property + def platform(self) -> str: + return self._platform + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Tag): + return NotImplemented + + return ( + (self._hash == other._hash) # Short-circuit ASAP for perf reasons. + and (self._platform == other._platform) + and (self._abi == other._abi) + and (self._interpreter == other._interpreter) + ) + + def __hash__(self) -> int: + return self._hash + + def __str__(self) -> str: + return f"{self._interpreter}-{self._abi}-{self._platform}" + + def __repr__(self) -> str: + return f"<{self} @ {id(self)}>" + + +def parse_tag(tag: str) -> FrozenSet[Tag]: + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: + value: Union[int, str, None] = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + +def _normalize_string(string: str) -> str: + return string.replace(".", "_").replace("-", "_").replace(" ", "_") + + +def _is_threaded_cpython(abis: List[str]) -> bool: + """ + Determine if the ABI corresponds to a threaded (`--disable-gil`) build. + + The threaded builds are indicated by a "t" in the abiflags. + """ + if len(abis) == 0: + return False + # expect e.g., cp313 + m = re.match(r"cp\d+(.*)", abis[0]) + if not m: + return False + abiflags = m.group(1) + return "t" in abiflags + + +def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) + builds do not support abi3. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading + + +def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: + py_version = tuple(py_version) # To allow for version comparison. + abis = [] + version = _version_nodot(py_version[:2]) + threading = debug = pymalloc = ucs4 = "" + with_debug = _get_config_var("Py_DEBUG", warn) + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): + threading = "t" + if py_version < (3, 8): + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append(f"cp{version}{threading}") + abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") + return abis + + +def cpython_tags( + python_version: Optional[PythonVersion] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + if not python_version: + python_version = sys.version_info[:2] + + interpreter = f"cp{_version_nodot(python_version[:2])}" + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or platform_tags()) + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + threading = _is_threaded_cpython(abis) + use_abi3 = _abi3_applies(python_version, threading) + if use_abi3: + yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) + + if use_abi3: + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) + ) + yield Tag(interpreter, "abi3", platform_) + + +def _generic_abi() -> List[str]: + """ + Return the ABI tag based on EXT_SUFFIX. + """ + # The following are examples of `EXT_SUFFIX`. + # We want to keep the parts which are related to the ABI and remove the + # parts which are related to the platform: + # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 + # - mac: '.cpython-310-darwin.so' => cp310 + # - win: '.cp310-win_amd64.pyd' => cp310 + # - win: '.pyd' => cp37 (uses _cpython_abis()) + # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 + # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' + # => graalpy_38_native + + ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) + if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": + raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") + parts = ext_suffix.split(".") + if len(parts) < 3: + # CPython3.7 and earlier uses ".pyd" on Windows. + return _cpython_abis(sys.version_info[:2]) + soabi = parts[1] + if soabi.startswith("cpython"): + # non-windows + abi = "cp" + soabi.split("-")[1] + elif soabi.startswith("cp"): + # windows + abi = soabi.split("-")[0] + elif soabi.startswith("pypy"): + abi = "-".join(soabi.split("-")[:2]) + elif soabi.startswith("graalpy"): + abi = "-".join(soabi.split("-")[:3]) + elif soabi: + # pyston, ironpython, others? + abi = soabi + else: + return [] + return [_normalize_string(abi)] + + +def generic_tags( + interpreter: Optional[str] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a generic interpreter. + + The tags consist of: + - -- + + The "none" ABI will be added if it was not explicitly provided. + """ + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + else: + abis = list(abis) + platforms = list(platforms or platform_tags()) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + +def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: + """ + Yields Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all previous versions of that major version. + """ + if len(py_version) > 1: + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield f"py{_version_nodot((py_version[0], minor))}" + + +def compatible_tags( + python_version: Optional[PythonVersion] = None, + interpreter: Optional[str] = None, + platforms: Optional[Iterable[str]] = None, +) -> Iterator[Tag]: + """ + Yields the sequence of tags that are compatible with a specific version of Python. + + The tags consist of: + - py*-none- + - -none-any # ... if `interpreter` is provided. + - py*-none-any + """ + if not python_version: + python_version = sys.version_info[:2] + platforms = list(platforms or platform_tags()) + for version in _py_interpreter_range(python_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + if cpu_arch in {"arm64", "x86_64"}: + formats.append("universal2") + + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: + formats.append("universal") + + return formats + + +def mac_platforms( + version: Optional[MacVersion] = None, arch: Optional[str] = None +) -> Iterator[str]: + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + if version == (10, 16): + # When built against an older macOS SDK, Python will report macOS 10.16 + # instead of the real version. + version_str = subprocess.run( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + check=True, + env={"SYSTEM_VERSION_COMPAT": "0"}, + stdout=subprocess.PIPE, + text=True, + ).stdout + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version + if arch is None: + arch = _mac_arch(cpu_arch) + else: + arch = arch + + if (10, 0) <= version and version < (11, 0): + # Prior to Mac OS 11, each yearly release of Mac OS bumped the + # "minor" version number. The major version was always 10. + for minor_version in range(version[1], -1, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=10, minor=minor_version, binary_format=binary_format + ) + + if version >= (11, 0): + # Starting with Mac OS 11, each yearly release bumps the major version + # number. The minor versions are now the midyear updates. + for major_version in range(version[0], 10, -1): + compat_version = major_version, 0 + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=major_version, minor=0, binary_format=binary_format + ) + + if version >= (11, 0): + # Mac OS 11 on x86_64 is compatible with binaries from previous releases. + # Arm64 support was introduced in 11.0, so no Arm binaries from previous + # releases exist. + # + # However, the "universal2" binary format can have a + # macOS version earlier than 11.0 when the x86_64 part of the binary supports + # that version of macOS. + if arch == "x86_64": + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + else: + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_format = "universal2" + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + + +def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: + linux = _normalize_string(sysconfig.get_platform()) + if not linux.startswith("linux_"): + # we should never be here, just yield the sysconfig one and return + yield linux + return + if is_32bit: + if linux == "linux_x86_64": + linux = "linux_i686" + elif linux == "linux_aarch64": + linux = "linux_armv8l" + _, arch = linux.split("_", 1) + archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) + yield from _manylinux.platform_tags(archs) + yield from _musllinux.platform_tags(archs) + for arch in archs: + yield f"linux_{arch}" + + +def _generic_platforms() -> Iterator[str]: + yield _normalize_string(sysconfig.get_platform()) + + +def platform_tags() -> Iterator[str]: + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() + + +def interpreter_name() -> str: + """ + Returns the name of the running interpreter. + + Some implementations have a reserved, two-letter abbreviation which will + be returned when appropriate. + """ + name = sys.implementation.name + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def interpreter_version(*, warn: bool = False) -> str: + """ + Returns the version of the running interpreter. + """ + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + return version + + +def _version_nodot(version: PythonVersion) -> str: + return "".join(map(str, version)) + + +def sys_tags(*, warn: bool = False) -> Iterator[Tag]: + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + + interp_name = interpreter_name() + if interp_name == "cp": + yield from cpython_tags(warn=warn) + else: + yield from generic_tags() + + if interp_name == "pp": + interp = "pp3" + elif interp_name == "cp": + interp = "cp" + interpreter_version(warn=warn) + else: + interp = None + yield from compatible_tags(interpreter=interp) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/utils.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c2c2f75aa806282d322c76c2117c0f0fdfb09d25 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/utils.py @@ -0,0 +1,172 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import re +from typing import FrozenSet, NewType, Tuple, Union, cast + +from .tags import Tag, parse_tag +from .version import InvalidVersion, Version + +BuildTag = Union[Tuple[()], Tuple[int, str]] +NormalizedName = NewType("NormalizedName", str) + + +class InvalidName(ValueError): + """ + An invalid distribution name; users should refer to the packaging user guide. + """ + + +class InvalidWheelFilename(ValueError): + """ + An invalid wheel filename was found, users should refer to PEP 427. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + + +# Core metadata spec for `Name` +_validate_regex = re.compile( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE +) +_canonicalize_regex = re.compile(r"[-_.]+") +_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") +# PEP 427: The build number must start with a digit. +_build_tag_regex = re.compile(r"(\d+)(.*)") + + +def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: + if validate and not _validate_regex.match(name): + raise InvalidName(f"name is invalid: {name!r}") + # This is taken from PEP 503. + value = _canonicalize_regex.sub("-", name).lower() + return cast(NormalizedName, value) + + +def is_normalized_name(name: str) -> bool: + return _normalized_regex.match(name) is not None + + +def canonicalize_version( + version: Union[Version, str], *, strip_trailing_zero: bool = True +) -> str: + """ + This is very similar to Version.__str__, but has one subtle difference + with the way it handles the release segment. + """ + if isinstance(version, str): + try: + parsed = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + else: + parsed = version + + parts = [] + + # Epoch + if parsed.epoch != 0: + parts.append(f"{parsed.epoch}!") + + # Release segment + release_segment = ".".join(str(x) for x in parsed.release) + if strip_trailing_zero: + # NB: This strips trailing '.0's to normalize + release_segment = re.sub(r"(\.0)+$", "", release_segment) + parts.append(release_segment) + + # Pre-release + if parsed.pre is not None: + parts.append("".join(str(x) for x in parsed.pre)) + + # Post-release + if parsed.post is not None: + parts.append(f".post{parsed.post}") + + # Development release + if parsed.dev is not None: + parts.append(f".dev{parsed.dev}") + + # Local version segment + if parsed.local is not None: + parts.append(f"+{parsed.local}") + + return "".join(parts) + + +def parse_wheel_filename( + filename: str, +) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + f"Invalid wheel filename (extension must be '.whl'): {filename}" + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + f"Invalid wheel filename (wrong number of parts): {filename}" + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # See PEP 427 for the rules on escaping the project name. + if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: + raise InvalidWheelFilename(f"Invalid project name: {filename}") + name = canonicalize_name(name_part) + + try: + version = Version(parts[1]) + except InvalidVersion as e: + raise InvalidWheelFilename( + f"Invalid wheel filename (invalid version): {filename}" + ) from e + + if dashes == 5: + build_part = parts[2] + build_match = _build_tag_regex.match(build_part) + if build_match is None: + raise InvalidWheelFilename( + f"Invalid build number: {build_part} in '{filename}'" + ) + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + else: + build = () + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: + if filename.endswith(".tar.gz"): + file_stem = filename[: -len(".tar.gz")] + elif filename.endswith(".zip"): + file_stem = filename[: -len(".zip")] + else: + raise InvalidSdistFilename( + f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" + f" {filename}" + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = file_stem.rpartition("-") + if not sep: + raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") + + name = canonicalize_name(name_part) + + try: + version = Version(version_part) + except InvalidVersion as e: + raise InvalidSdistFilename( + f"Invalid sdist filename (invalid version): {filename}" + ) from e + + return (name, version) diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/version.py b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/version.py new file mode 100644 index 0000000000000000000000000000000000000000..cda8e99935c8d92010b84437ae83d75031245d61 --- /dev/null +++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/packaging/version.py @@ -0,0 +1,561 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.version import parse, Version +""" + +import itertools +import re +from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union + +from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType + +__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] + +LocalType = Tuple[Union[int, str], ...] + +CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] +CmpLocalType = Union[ + NegativeInfinityType, + Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], +] +CmpKey = Tuple[ + int, + Tuple[int, ...], + CmpPrePostDevType, + CmpPrePostDevType, + CmpPrePostDevType, + CmpLocalType, +] +VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] + + +class _Version(NamedTuple): + epoch: int + release: Tuple[int, ...] + dev: Optional[Tuple[str, int]] + pre: Optional[Tuple[str, int]] + post: Optional[Tuple[str, int]] + local: Optional[LocalType] + + +def parse(version: str) -> "Version": + """Parse the given version string. + + >>> parse('1.0.dev1') + + + :param version: The version string to parse. + :raises InvalidVersion: When the version string is not a valid version. + """ + return Version(version) + + +class InvalidVersion(ValueError): + """Raised when a version string is not a valid version. + + >>> Version("invalid") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'invalid' + """ + + +class _BaseVersion: + _key: Tuple[Any, ...] + + def __hash__(self) -> int: + return hash(self._key) + + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. + def __lt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key < other._key + + def __le__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key <= other._key + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key == other._key + + def __ge__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key >= other._key + + def __gt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key > other._key + + def __ne__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key != other._key + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +_VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?Palpha|a|beta|b|preview|pre|c|rc)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+VERSION_PATTERN = _VERSION_PATTERN
+"""
+A string containing the regular expression used to match a valid version.
+
+The pattern is not anchored at either end, and is intended for embedding in larger
+expressions (for example, matching a version number as part of a file name). The
+regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
+flags set.
+
+:meta hide-value:
+"""
+
+
+class Version(_BaseVersion):
+    """This class abstracts handling of a project's versions.
+
+    A :class:`Version` instance is comparison aware and can be compared and
+    sorted using the standard Python interfaces.
+
+    >>> v1 = Version("1.0a5")
+    >>> v2 = Version("1.0")
+    >>> v1
+    
+    >>> v2
+    
+    >>> v1 < v2
+    True
+    >>> v1 == v2
+    False
+    >>> v1 > v2
+    False
+    >>> v1 >= v2
+    False
+    >>> v1 <= v2
+    True
+    """
+
+    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+    _key: CmpKey
+
+    def __init__(self, version: str) -> None:
+        """Initialize a Version object.
+
+        :param version:
+            The string representation of a version which will be parsed and normalized
+            before use.
+        :raises InvalidVersion:
+            If the ``version`` does not conform to PEP 440 in any way then this
+            exception will be raised.
+        """
+
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion(f"Invalid version: '{version}'")
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+            post=_parse_letter_version(
+                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+            ),
+            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self) -> str:
+        """A representation of the Version that shows all internal state.
+
+        >>> Version('1.0.0')
+        
+        """
+        return f""
+
+    def __str__(self) -> str:
+        """A string representation of the version that can be rounded-tripped.
+
+        >>> str(Version("1.0a5"))
+        '1.0a5'
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        # Pre-release
+        if self.pre is not None:
+            parts.append("".join(str(x) for x in self.pre))
+
+        # Post-release
+        if self.post is not None:
+            parts.append(f".post{self.post}")
+
+        # Development release
+        if self.dev is not None:
+            parts.append(f".dev{self.dev}")
+
+        # Local version segment
+        if self.local is not None:
+            parts.append(f"+{self.local}")
+
+        return "".join(parts)
+
+    @property
+    def epoch(self) -> int:
+        """The epoch of the version.
+
+        >>> Version("2.0.0").epoch
+        0
+        >>> Version("1!2.0.0").epoch
+        1
+        """
+        return self._version.epoch
+
+    @property
+    def release(self) -> Tuple[int, ...]:
+        """The components of the "release" segment of the version.
+
+        >>> Version("1.2.3").release
+        (1, 2, 3)
+        >>> Version("2.0.0").release
+        (2, 0, 0)
+        >>> Version("1!2.0.0.post0").release
+        (2, 0, 0)
+
+        Includes trailing zeroes but not the epoch or any pre-release / development /
+        post-release suffixes.
+        """
+        return self._version.release
+
+    @property
+    def pre(self) -> Optional[Tuple[str, int]]:
+        """The pre-release segment of the version.
+
+        >>> print(Version("1.2.3").pre)
+        None
+        >>> Version("1.2.3a1").pre
+        ('a', 1)
+        >>> Version("1.2.3b1").pre
+        ('b', 1)
+        >>> Version("1.2.3rc1").pre
+        ('rc', 1)
+        """
+        return self._version.pre
+
+    @property
+    def post(self) -> Optional[int]:
+        """The post-release number of the version.
+
+        >>> print(Version("1.2.3").post)
+        None
+        >>> Version("1.2.3.post1").post
+        1
+        """
+        return self._version.post[1] if self._version.post else None
+
+    @property
+    def dev(self) -> Optional[int]:
+        """The development number of the version.
+
+        >>> print(Version("1.2.3").dev)
+        None
+        >>> Version("1.2.3.dev1").dev
+        1
+        """
+        return self._version.dev[1] if self._version.dev else None
+
+    @property
+    def local(self) -> Optional[str]:
+        """The local version segment of the version.
+
+        >>> print(Version("1.2.3").local)
+        None
+        >>> Version("1.2.3+abc").local
+        'abc'
+        """
+        if self._version.local:
+            return ".".join(str(x) for x in self._version.local)
+        else:
+            return None
+
+    @property
+    def public(self) -> str:
+        """The public portion of the version.
+
+        >>> Version("1.2.3").public
+        '1.2.3'
+        >>> Version("1.2.3+abc").public
+        '1.2.3'
+        >>> Version("1.2.3+abc.dev1").public
+        '1.2.3'
+        """
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self) -> str:
+        """The "base version" of the version.
+
+        >>> Version("1.2.3").base_version
+        '1.2.3'
+        >>> Version("1.2.3+abc").base_version
+        '1.2.3'
+        >>> Version("1!1.2.3+abc.dev1").base_version
+        '1!1.2.3'
+
+        The "base version" is the public version of the project without any pre or post
+        release markers.
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        return "".join(parts)
+
+    @property
+    def is_prerelease(self) -> bool:
+        """Whether this version is a pre-release.
+
+        >>> Version("1.2.3").is_prerelease
+        False
+        >>> Version("1.2.3a1").is_prerelease
+        True
+        >>> Version("1.2.3b1").is_prerelease
+        True
+        >>> Version("1.2.3rc1").is_prerelease
+        True
+        >>> Version("1.2.3dev1").is_prerelease
+        True
+        """
+        return self.dev is not None or self.pre is not None
+
+    @property
+    def is_postrelease(self) -> bool:
+        """Whether this version is a post-release.
+
+        >>> Version("1.2.3").is_postrelease
+        False
+        >>> Version("1.2.3.post1").is_postrelease
+        True
+        """
+        return self.post is not None
+
+    @property
+    def is_devrelease(self) -> bool:
+        """Whether this version is a development release.
+
+        >>> Version("1.2.3").is_devrelease
+        False
+        >>> Version("1.2.3.dev1").is_devrelease
+        True
+        """
+        return self.dev is not None
+
+    @property
+    def major(self) -> int:
+        """The first item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").major
+        1
+        """
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self) -> int:
+        """The second item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").minor
+        2
+        >>> Version("1").minor
+        0
+        """
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self) -> int:
+        """The third item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").micro
+        3
+        >>> Version("1").micro
+        0
+        """
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
+) -> Optional[Tuple[str, int]]:
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume if we are given a number, but we are not given a letter
+        # then this is using the implicit post release syntax (e.g. 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+    return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_separators.split(local)
+        )
+    return None
+
+
+def _cmpkey(
+    epoch: int,
+    release: Tuple[int, ...],
+    pre: Optional[Tuple[str, int]],
+    post: Optional[Tuple[str, int]],
+    dev: Optional[Tuple[str, int]],
+    local: Optional[LocalType],
+) -> CmpKey:
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    _release = tuple(
+        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # if there is not a pre or a post segment. If we have one of those then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        _pre: CmpPrePostDevType = NegativeInfinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        _pre = Infinity
+    else:
+        _pre = pre
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        _post: CmpPrePostDevType = NegativeInfinity
+
+    else:
+        _post = post
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        _dev: CmpPrePostDevType = Infinity
+
+    else:
+        _dev = dev
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        _local: CmpLocalType = NegativeInfinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
+
+    return epoch, _release, _pre, _post, _dev, _local
diff --git a/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/vendor.txt b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/vendor.txt
new file mode 100644
index 0000000000000000000000000000000000000000..14666103a82ea7fced3d8cffb8a9b2a9e03fb492
--- /dev/null
+++ b/URSA/.venv_ursa/lib/python3.12/site-packages/wheel/vendored/vendor.txt
@@ -0,0 +1 @@
+packaging==24.0